对于长时间运行的网络连接,在应用程序级别上可能会经历较长的空闲时间,明智的做法是在节点之间实现心跳,以提前截止日期。这允许您快速识别网络问题并迅速重新建立连接,而不是在应用程序要传输数据时才等待检测网络错误。通过这种方式,您可以确保应用程序在需要时始终有良好的网络连接。
为了达到这个目的,一个心跳消息需要被发送到远端服务并等待回复,我们可以根据回复情况提前终止连接。节点将以一定间隔时间来发送消息,类似心跳。这种方法不仅可以在各种操作系统上移植,而且还可以确保使用网络连接的应用程序立刻响应,因为应用程序实现了心跳。
要实现心跳功能,需要一个goroutine定期到发送ping消息。如果最近收到远程服务的回复,就不需要发送不必要的ping消息,因此需要可以重置ping计时器功能。如下代码所示:
func Pinger(ctx context.Context, w io.Writer, reset <-chan time.Duration) {
var interval time.Duration
select {
case <-ctx.Done():
return
case interval = <-reset: //读取更新的心跳间隔时间
default:
}
if interval < 0 {
interval = defaultPingInterval
}
timer := time.NewTimer(interval)
defer func() {
if !timer.Stop() {
<-timer.C
}
}()
for {
select {
case <-ctx.Done():
return
case newInterval := <-reset:
if !timer.Stop() {
<-timer.C
}
if newInterval > 0 {
interval = newInterval
}
case <-timer.C:
if _, err := w.Write([]byte("ping")); err != nil {
//在此跟踪并执行连续超时
return
}
}
_ = timer.Reset(interval) //重制心跳上报时间间隔
}
}
在心跳例子中使用ping和pong消息格式,即当客户端定期向服务端发送一个ping消息,服务端给客户端发送一个pong消息。这个消息内容可以自定义没有规定的,这里也是使用惯例。
下面对前面代码进行解释:
在上面的Pinger函数中,定期向io.writer对象写入ping消息。因为Pinger函数需要运行在一个单独的goroutine中,所以需要接收一个context作为第一个参数,这样就可以通过context终止goroutine防止泄漏。剩余的参数包括一个io.writer接口和一个channel用于动态接收间隔时间以重置计时器。需要创建一个带buffer的channel将一个间隔时间传入作为计时器初始值。如果interval比0小,就使用默认间隔时间。
然后根据interval初始化计时器,并使用defer来清空计时器channel避免泄露。for循环包含一个select声明,将阻塞直到三个case中的一个匹配:context被取消,reset通道收到重置计时器消息或计时器过期。如果context被取消,函数会退出,不会再发送ping消息。如果reset通道有数据,也不需要发送ping并重置计时器。
如果计时器过期,会写入ping消息到writer,并在下一个for循环之前重置计时器。如果需要,你也可以在这个case里面跟踪写入超时的发生。要实现这个功能,你可以将上下文的cancel函数传入,并在这里调用如果发送超时。
下面代码说明了如何使用Pinger函数,可以按预期的间隔从reader读取ping消息,并以不同的间隔重置ping计时器。
func ExamplePinger() {
ctx, cancelFunc := context.WithCancel(context.Background())
r, w := io.Pipe() //代替网络连接net.Conn
done := make(chan struct{})
resetTimer := make(chan time.Duration, 1)
resetTimer <- time.Second //ping间隔初始值
go func() {
Pinger(ctx, w, resetTimer)
close(done)
}()
receivePing := func(d time.Duration, r io.Reader) {
if d >= 0 {
fmt.Printf("resetting time (%s)\n", d)
resetTimer <- d
}
now := time.Now()
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
fmt.Println(err)
}
fmt.Printf("received %q (%s)\n", buf[:n], time.Since(now).Round(100*time.Millisecond))
}
for i, v := range []int64{0, 200, 300, 0, -1, -1, -1} {
fmt.Printf("Run %d\n", i+1)
receivePing(time.Duration(v)*time.Millisecond, r)
}
cancelFunc() //取消context使pinger退出
<-done
}
输出结果:
Run 1
resetting time (0s)
received "ping" (1s)
Run 2
resetting time (200ms)
received "ping" (200ms)
Run 3
resetting time (300ms)
received "ping" (300ms)
Run 4
resetting time (0s)
received "ping" (300ms)
Run 5
received "ping" (300ms)
Run 6
received "ping" (300ms)
Run 7
received "ping" (300ms)
在这个例子中,创建一个带缓存的channel用于Pinger函数中计时器的初始化。在将该通道传递给Pinger函数之前,在resetTimer通道上设置一个1秒的初始ping间隔。您将使用这个时间初始化Pinger的计时器,并指示何时将ping消息写入writer接口。
在循环2中运行一系列毫秒的间隔时间,将每个间隔时间传递给receivePing函数。这个函数根据给定的间隔时间重置ping计时器,并通过reader等待接收ping消息。最后将接收到的ping消息打印到标准输出。
在for循环第一个迭代时,传入的参数0,这表示告诉Pinger使用之前的间隔时间也就是1s来重置计时器。和预期的一样,在1s后reader接收到ping消息。第二次迭代将ping的计时器重置为200ms。一旦过期,reader就收到ping消息了。第三次重置ping计时器为300ms,并且ping消息在300ms接收到。
在迭代4时传入的是0,将使用之前的间隔时间300ms。可以发现使用间隔时间0,即意味着使用之前的计时器间隔时间,这非常实用,因为我们不需要跟踪初始的定时器间隔时了。迭代5到7仅仅等待接收ping消息,不重置ping计时器。如预期一样,reader每隔300ms接收到ping消息。