当客户端发送消息时,我们需要接收消息;而当我们需要给客户端发送消息时,我们通过 readMessage
和 writeMessage
来实现。我们将这些功能分别放入协程中操作,并使用 for
循环持续读取消息。
在 readMessage
中,我们通过连接读取消息。消息类型为二进制,对应客户端发送的消息。为了持续读取,我们将读取操作放入 for
循环中。如果读取过程中出错,我们可以暂时跳出循环。客户端发送的消息类型为 binary
,我们通过定义一个结构体(如 body
)来处理消息内容,并设置一个 channel
用于读取消息。如果消息类型不符合预期,我们打印错误信息,提示不支持该消息类型。
如果出现异常,我们可以通过删除相关信息来处理,例如移除客户端连接。我们还需要判断客户端是否仍然存在,如果不存在,则关闭连接并从列表中移除。
在 writeMessage
中,我们从服务端向客户端发送消息。我们定义一个 writeChannel
,并通过字节数组发送消息。如果读取消息失败,我们发送一个关闭消息通知客户端连接已关闭,并记录日志。
我们还可以设置读取消息的限制(如 readLimit
),根据实际需求调整参数。在写消息时,我们同样需要处理消息类型和内容,确保消息以二进制形式发送。
在 readMessage
和 writeMessage
的实现中,我们需要注意以下几点:
-
缓冲区设置:为了避免阻塞,我们为
channel
设置缓冲区,提高效率。 -
心跳检测:通过定时发送心跳消息(如
ping
)和处理响应(如pong
),检测连接是否正常。 -
消息解析:接收到的消息需要根据协议格式进行解析,提取有效信息。
-
异常处理:在读取或写入过程中,如果出现异常,需要及时处理并记录日志。
在实际开发中,我们还需要通过命令行参数动态设置服务器 ID,而不是写死在代码中。我们可以使用 Cobra 框架来处理命令行参数,方便启动和配置服务。
完成这些功能后,我们可以通过启动服务并连接客户端进行测试。测试过程中,我们需要关注以下几点:
-
握手消息:客户端连接后会发送握手消息,我们需要正确处理并返回响应。
-
心跳消息:客户端会定期发送心跳消息,我们需要响应以保持连接。
-
消息类型处理:根据协议类型,正确解析和处理不同类型的消息。
通过这些步骤,我们可以实现一个基本的 WebSocket 通信框架,支持客户端和服务端的消息交互。
package net
import (
"common/logs"
"fmt"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"sync"
"sync/atomic"
"time"
)
var cidBase uint64 = 10000
var (
pongWait = 10 * time.Second
writeWait = 10 * time.Second
pingInterval = (pongWait * 9) / 10
maxMessageSize int64 = 1024
)
type WsConnection struct {
Cid string
Conn *websocket.Conn
manager *Manager
ReadChan chan *MsgPack
WriteChan chan []byte
Session *Session
pingTicker *time.Ticker
closeChan chan struct{}
closeOnce sync.Once
readChanOnce sync.Once
writeChanOnce sync.Once
}
func (c *WsConnection) GetSession() *Session {
return c.Session
}
func (c *WsConnection) SendMessage(buf []byte) error {
c.WriteChan <- buf
return nil
}
func (c *WsConnection) Close() {
//确保只执行一次
c.closeOnce.Do(func() {
//因为只执行一次 这里不用检查是否已经关闭了
close(c.closeChan)
if c.Conn != nil {
_ = c.Conn.Close()
}
// 停止定时器
if c.pingTicker != nil {
c.pingTicker.Stop()
}
logs.Info("client[%s] connection closed", c.Cid)
})
}
func (c *WsConnection) Run() {
go c.readMessage()
go c.writeMessage()
//做一些心跳检测 websocket中 ping pong机制
c.Conn.SetPongHandler(c.PongHandler)
}
func (c *WsConnection) writeMessage() {
c.pingTicker = time.NewTicker(pingInterval)
defer func() {
// 清理通道
if c.WriteChan != nil {
c.writeChanOnce.Do(func() {
close(c.WriteChan)
})
}
}()
for {
select {
case message, ok := <-c.WriteChan:
if !ok {
if err := c.Conn.WriteMessage(websocket.CloseMessage, nil); err != nil {
logs.Error("connection closed, %v", err)
}
c.Close()
return
}
//logs.Error("%v", stream)
if err := c.Conn.WriteMessage(websocket.BinaryMessage, message); err != nil {
logs.Error("client[%s] write stream err :%v", c.Cid, err)
}
case <-c.pingTicker.C:
if err := c.Conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil {
logs.Error("client[%s] ping SetWriteDeadline err :%v", c.Cid, err)
}
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
logs.Error("client[%s] ping err :%v", c.Cid, err)
c.Close()
}
case <-c.closeChan:
logs.Info("client[%s] writeMessage stopped", c.Cid)
return
}
}
}
func (c *WsConnection) readMessage() {
defer func() {
logs.Info("client[%s] readMessage stopped", c.Cid)
c.manager.removeClient(c)
}()
c.Conn.SetReadLimit(maxMessageSize)
if err := c.Conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
logs.Error("SetReadDeadline err:%v", err)
return
}
for {
select {
case <-c.closeChan:
// 检测到关闭信号,退出协程
logs.Info("client[%s] received close signal", c.Cid)
return
default:
messageType, message, err := c.Conn.ReadMessage()
if err != nil {
// 检测到错误或连接关闭,退出循环
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
logs.Error("client[%s] unexpected close error: %v", c.Cid, err)
}
return
}
//客户端发来的消息是二进制消息
if messageType == websocket.BinaryMessage {
select {
case c.ReadChan <- &MsgPack{Cid: c.Cid, Body: message}:
case <-c.closeChan:
logs.Info("client[%s] readMessage stopped while sending to channel", c.Cid)
return
}
} else {
logs.Error("unsupported stream type : %d", messageType)
}
}
}
}
func (c *WsConnection) PongHandler(data string) error {
if err := c.Conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil {
return err
}
return nil
}
func NewWsConnection(conn *websocket.Conn, manager *Manager) *WsConnection {
cid := fmt.Sprintf("%s-%s-%d", uuid.New().String(), manager.ServerId, atomic.AddUint64(&cidBase, 1))
return &WsConnection{
Conn: conn,
manager: manager,
Cid: cid,
WriteChan: make(chan []byte, 1024),
ReadChan: manager.ClientReadChan,
Session: NewSession(cid, manager),
closeChan: make(chan struct{}),
}
}