NPS 客户端深度解析:健康检查与动态注册

引言

在前面的文章中,我们已经剖析了 NPS 客户端的核心逻辑(client.go)和控制模块(control.go)。我们了解到客户端如何与服务端建立连接、如何通过多路复用隧道转发流量,以及如何从配置文件启动。然而,一个成熟的系统不仅需要核心功能,还需要完善的辅助机制来保证其在复杂多变的生产环境中的稳定性和灵活性。本篇文章将深入 NPS 客户端的另外两个重要文件:nps/client/health.gonps/client/register.go,探讨 NPS 是如何实现对后端服务的健康检查以及支持客户端动态注册的。


health.go:主动的服务健康状态监控

内网穿透的目的是将内网服务暴露到公网,但如果内网服务本身宕机了,NPS 客户端与服务端的连接依然是通畅的。这时,用户的请求仍然会被转发到客户端,但客户端无法连接到宕机的内网服务,最终导致用户访问失败。为了避免这种情况,NPS 设计了一套健康检查机制,让客户端能够主动监控其代理的内网服务的状态,并在服务异常时及时通知服务端。

heathCheck():健康检查的启动器

健康检查功能由 client.go 中的 TRPClient.Start() 方法启动,它会作为一个独立的 goroutine 运行 heathCheck() 函数。

// in client.go
func (s *TRPClient) Start() {
    // ...
    if s.cnf != nil && len(s.cnf.Healths) > 0 {
        go heathCheck(s.cnf.Healths, s.signal) // 启动健康检查
    }
    // ...
}

heathCheck() 函数会遍历配置文件中的所有健康检查任务,并为每一个任务启动一个专门的 check goroutine。

// in health.go
func heathCheck(hs []*health.Health, signal *conn.Conn) {
	for _, h := range hs {
		go check(h, signal)
	}
}

check()tcpHealthCheck():执行检查

check() 函数是执行健康检查的核心循环。它会根据配置的检查间隔 (h.IntervalSecond),周期性地调用具体的检查方法。

func check(h *health.Health, signal *conn.Conn) {
	for {
		// ...
		switch h.HealthType {
		case "tcp":
			if err := tcpHealthCheck(h); err != nil {
				// ... 失败处理
			} else {
				// ... 成功处理
			}
		}
		time.Sleep(time.Duration(h.IntervalSecond) * time.Second)
	}
}

目前,NPS 主要支持 TCP 类型的健康检查 (tcpHealthCheck)。其逻辑非常直接:尝试通过 net.DialTimeout 连接到配置的内网目标服务 (h.HealthTarget)。

  • 连接成功:表示服务健康,将连续失败计数器 h.FailCount 清零。
  • 连接失败:表示服务异常,将 h.FailCount 加一。

失败上报机制

当连续失败次数 (h.FailCount) 超过了配置的最大失败次数 (h.MaxFail),并且当前任务尚未被标记为停止时,客户端就会向服务端发送一个健康检查失败的通知。

// in check()
if h.FailCount > h.MaxFail && !h.Stop {
    h.Stop = true
    logs.Warn("The service %s has been stopped", h.HealthTarget)
    signal.WriteHealthData(common.HEALTH_CHECK_FAIL, h.TaskId) // 发送失败通知
}
  • signal.WriteHealthData(): 通过主控制连接 (signal),将 common.HEALTH_CHECK_FAIL 标志和对应的任务 ID (h.TaskId) 发送给服务端。
  • 服务端 (bridge 模块) 接收到这个通知后,就会将对应的隧道或主机标记为离线状态,不再向其转发新的请求,从而实现了服务异常的快速熔断。

register.go:灵活的客户端动态注册

在某些大规模或动态的部署场景中(例如,自动伸缩的容器集群),预先在服务端为每一个客户端都配置好 vkey 是不现实的。为了解决这个问题,NPS 提供了客户端动态注册功能。它允许客户端使用一个公共的、预设的 vkey(在服务端配置中称为 public_vkey)向服务端进行“登记”,服务端会为这个客户端动态生成一个唯一的、全新的 vkey 并返回。

Register() 函数:动态注册的入口

register.go 文件中的 Register() 函数是实现动态注册的唯一入口。

func Register(server, key string) (string, error) {
	// 1. 建立一个类型为 WORK_REGISTER 的连接
	c, err := NewConn("tcp", "", server, common.WORK_REGISTER, "")
	if err != nil {
		return "", err
	}
	// 2. 发送公共 vkey
	if _, err := c.SendInfo(&conn.Link{VerifyKey: key}); err != nil {
		return "", err
	}
	// 3. 等待并接收服务端返回的新 vkey
	link, err := c.GetLinkInfo()
	if err != nil {
		return "", err
	}
	return link.VerifyKey, nil
}

注册流程:

  1. 建立注册连接: 客户端调用 NewConn() 建立一个到服务端的 TCP 连接,但连接类型被特殊地标记为 common.WORK_REGISTER
  2. 发送公共密钥: 客户端将公共 vkey (public_vkey) 发送给服务端进行验证。
  3. 服务端处理: 服务端 bridge 模块在接收到 WORK_REGISTER 类型的连接后,会验证传入的 public_vkey 是否正确。如果正确,服务端会:
    • 创建一个新的 client 记录。
    • 为这个新 client 生成一个唯一的 vkey
    • 将这个新 client 存入数据库。
    • 将新生成的 vkey 发回给客户端。
  4. 接收并返回新密钥: 客户端通过 c.GetLinkInfo() 接收服务端返回的包含新 vkey 的信息,并将其作为 Register 函数的返回值。

之后,客户端就可以使用这个新获取到的 vkey,像一个普通客户端一样连接到 NPS 服务端,进行后续的隧道建立和数据转发。


总结

health.goregister.go 这两个文件虽然代码量不大,但它们为 NPS 提供了两个至关重要的企业级功能:

  • 健康检查 (health.go):将客户端从一个被动的流量转发者,变成了一个能主动监控后端服务状态的“探针”,极大地提升了整个穿透服务的可靠性和健壮性,实现了服务异常的快速熔断。
  • 动态注册 (register.go):提供了一种极其灵活的客户端部署和管理方式,解耦了客户端与服务端配置的强绑定关系,特别适用于云原生、自动化的部署环境。

这些精心设计的功能,使得 NPS 不仅仅是一个简单的内网穿透工具,更是一个能够在复杂生产环境中稳定、高效运行的强大平台。