写在前面
上篇文章一个通过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