go游戏后端开发13: websocket读写消息处理

当客户端发送消息时,我们需要接收消息;而当我们需要给客户端发送消息时,我们通过 readMessagewriteMessage 来实现。我们将这些功能分别放入协程中操作,并使用 for 循环持续读取消息。

readMessage 中,我们通过连接读取消息。消息类型为二进制,对应客户端发送的消息。为了持续读取,我们将读取操作放入 for 循环中。如果读取过程中出错,我们可以暂时跳出循环。客户端发送的消息类型为 binary,我们通过定义一个结构体(如 body)来处理消息内容,并设置一个 channel 用于读取消息。如果消息类型不符合预期,我们打印错误信息,提示不支持该消息类型。

如果出现异常,我们可以通过删除相关信息来处理,例如移除客户端连接。我们还需要判断客户端是否仍然存在,如果不存在,则关闭连接并从列表中移除。

writeMessage 中,我们从服务端向客户端发送消息。我们定义一个 writeChannel,并通过字节数组发送消息。如果读取消息失败,我们发送一个关闭消息通知客户端连接已关闭,并记录日志。

我们还可以设置读取消息的限制(如 readLimit),根据实际需求调整参数。在写消息时,我们同样需要处理消息类型和内容,确保消息以二进制形式发送。

readMessagewriteMessage 的实现中,我们需要注意以下几点:

  1. 缓冲区设置:为了避免阻塞,我们为 channel 设置缓冲区,提高效率。

  2. 心跳检测:通过定时发送心跳消息(如 ping)和处理响应(如 pong),检测连接是否正常。

  3. 消息解析:接收到的消息需要根据协议格式进行解析,提取有效信息。

  4. 异常处理:在读取或写入过程中,如果出现异常,需要及时处理并记录日志。

在实际开发中,我们还需要通过命令行参数动态设置服务器 ID,而不是写死在代码中。我们可以使用 Cobra 框架来处理命令行参数,方便启动和配置服务。

完成这些功能后,我们可以通过启动服务并连接客户端进行测试。测试过程中,我们需要关注以下几点:

  1. 握手消息:客户端连接后会发送握手消息,我们需要正确处理并返回响应。

  2. 心跳消息:客户端会定期发送心跳消息,我们需要响应以保持连接。

  3. 消息类型处理:根据协议类型,正确解析和处理不同类型的消息。

通过这些步骤,我们可以实现一个基本的 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{}),
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值