Go:实现心跳(Heartbeat)

对于长时间运行的网络连接,在应用程序级别上可能会经历较长的空闲时间,明智的做法是在节点之间实现心跳,以提前截止日期。这允许您快速识别网络问题并迅速重新建立连接,而不是在应用程序要传输数据时才等待检测网络错误。通过这种方式,您可以确保应用程序在需要时始终有良好的网络连接。

为了达到这个目的,一个心跳消息需要被发送到远端服务并等待回复,我们可以根据回复情况提前终止连接。节点将以一定间隔时间来发送消息,类似心跳。这种方法不仅可以在各种操作系统上移植,而且还可以确保使用网络连接的应用程序立刻响应,因为应用程序实现了心跳。

要实现心跳功能,需要一个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消息。

你可能感兴趣的:(Go:实现心跳(Heartbeat))