一个通过websocket通信的go语言实现的即时通讯demo(二)-心跳


写在前面

上篇文章一个通过websocket通信的go语言实现的即时通讯demo介绍即时通信的基本能力,包括服务端监听、接收、广播消息,客户端连接、接收、发送消息。今天在这个基础上再增加心跳。

客户端

在客户端写一个定时器,发送心跳包ws.OpPing

package client

import (
   "github.com/gobwas/ws"
   "github.com/gobwas/ws/wsutil"
   "github.com/sirupsen/logrus"
   "time"
)

// 心跳
func (h *handler) heartbeatloop() error {
   logrus.Info("heartbeatloop started")

   // 每50s发送一次心跳
   tick := time.NewTicker(h.heartbeat) //heartbeat=50s
   for range tick.C {
      logrus.Info("ping...")
      // 发送一个ping的心跳包给服务端
      if err := wsutil.WriteClientMessage(h.conn, ws.OpPing, nil); err != nil {
         return err
      }
   }
   return nil
}

通过go h.heartbeatloop()来启用它

func connect(addr string) (*handler, error) {
   _, err := url.Parse(addr)
   if err != nil {
      return nil, err
   }

   conn, _, _, err := ws.Dial(context.Background(), addr)
   if err != nil {
      return nil, err
   }

   h := handler{
      conn:      conn,
      close:     make(chan struct{}, 1),
      recv:      make(chan []byte, 10),
      heartbeat: 5 * time.Second,
   }

   // 接收并处理服务端发送的消息
   go func() {
      err := h.readloop(conn)
      if err != nil {
         logrus.Warn(err)
      }
      // 通知上层
      h.close <- struct{}{}
   }()

   // 启动心跳
   go func() {
      if err := h.heartbeatloop(); err != nil {
         logrus.Warn(err)
      }
      // 通知上层
      h.close <- struct{}{}
   }()

   return &h, nil
}

同时,客户端也需要检查在指定时间内是否收到pong消息

func (h *handler) readloop(conn net.Conn) error {
   logrus.Info("readloop started")
   //要求在指定时间 heartbeat(50秒)*3内,可以读到数据。也就是在3次心跳内服务器必须响应,否则断开连接
   err := h.conn.SetReadDeadline(time.Now().Add(h.heartbeat * 3))
   if err != nil {
      return err
   }
   for {
      frame, err := ws.ReadFrame(conn)
      if err != nil {
         return err
      }
      if frame.Header.OpCode == ws.OpPong {
         // 如果是心跳消息,则重置读取超时时间
         _ = h.conn.SetReadDeadline(time.Now().Add(h.heartbeat * 3))
         h.recv <- frame.Payload
         continue
      }
      if frame.Header.OpCode == ws.OpClose {
         return errors.New("remote side close the channel")
      }
      if frame.Header.OpCode == ws.OpText {
         h.recv <- frame.Payload
      }
   }
}

在读的方法中,我们新增了两个逻辑,用于设置读超时时间,也就是如果发送了ping包之后,在三个周期内还未收到任何消息,ws.ReadFrame(conn)就会返回一个err。

这里使用的是设置超时读时间,如果在这个时间内未收到服务端消息则认为服务端不可用。没有使用一个新的定时器去定期发送消息,减少了一个goroutine。

服务器端

func (s *Server) readloop(user string, conn net.Conn) error {
   for {
      // 每次接收到消息之后,要求客户端必须在指定时间内发送一条消息过来,可以是ping,也可以是正常数据包
      _ = conn.SetReadDeadline(time.Now().Add(time.Minute * 2))
      frame, err := ws.ReadFrame(conn)
      if err != nil {
         return err
      }
      if frame.Header.OpCode == ws.OpPing {
         // 返回一个pong消息,并且附带发送一个"Pong"文本消息
         _ = wsutil.WriteServerMessage(conn, ws.OpPong, []byte("Pong"))
         continue
      }
      if frame.Header.OpCode == ws.OpClose {
         return errors.New("remote side close the conn")
      }

      if frame.Header.Masked {
         ws.Cipher(frame.Payload, frame.Header.Mask, 0)
      }
      // 接收文本帧内容
      if frame.Header.OpCode == ws.OpText {
         go s.handle(user, string(frame.Payload))
      }
   }
}

在for的第一行代码中,服务端要求客户端必须在指定的时间内发送一条消息,可以是ping包也可以是正常数据消息。这样做的优点就是服务端不用主动发送心跳包检测客户端是否存活,也就少了一个的线程;缺点就是客户端与服务端的心跳相关配置必须达成一致,比如客户端发送心跳的间隔超过了这个值,连接就会被断开。

参考

[1]心跳及重连

[2]一个通过websocket通信的go语言实现的即时通讯demo

[3][7d-im]feat/day2


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录