net.Conn 的 Write 和 Read 方法都是阻塞式执行的,所以要 为每个 TCP 连接创建两个协程,分别用来接收和发送数据.在接收协程中计算生成的数据,要发送给对端时,常规做法是生成一个 []byte 切片 ------ buf := make( []byte,length),然后把计算生成的数据拷贝到 buf 中,再投递到类型是 chan []byte 的 channel 中.发送协程只做一件事,从 channel 中取出数据并发送给对端
在上面所述的接收协程中,buf := make( []byte,length) 这行代码可以用内存池进行优化,内存池的实现可以选用达达的开源代码: https://github.com/funny/slab
在登录服务器 login 和网关服务器 gate 中,会建立数千个 TCP 连接数/协程数,在这两种服务器上选用的内存池类型是基于 golang 临时对象池的 slab.SyncPool;而大厅服务器 lobby 和路由服务器 route 的 TCP 连接数/协程数(几个至几十个不等)几乎是固定的,在这两种服务器上选用的内存池类型是基于 channel 的 slab.ChanPool;发送协程中把 buf 从 channel 中取出并发送完后,需要把 buf 回收进内存池
为了处理TCP的粘包情况,定义TCP数据逻辑包(以下简称逻辑包)格式为 包头 + 数据体;包头是两个 int32 字段,共计8字节,第一个 int32 字段表示协议号,第二个 int32 字段表示随后的数据体;比如客户端发送长度为 3 的帐号字符串 "abc" 给登录服务器login,则逻辑包包头的协议号字段可以约定填 1,数据长度填 3,逻辑包的数据体就是字符串 "abc"
逻辑包是自取的名字,在不同的团队可能有不同的称呼,是指在应用层提交的一段 TCP 数据,能够完整的表示上层业务逻辑意义,并非底层(网卡或者 TCP/IP 层)上的数据包.在不同的团队定义的格式也可能不同,但目的都是为了处理粘包
golang 中处理粘包可以使用 io.ReadFull 和 bufio.Reader,具体用法:
// c 表示刚创建的 net.Conn r := bufio.NewReaderSize( c,1024 ) recvBuf := make( []byte,1024 ) io.ReadFull( r,recvBuf[ :8 ] ) // 先读取包头,收到 8 个字节才继续往下执行,否则一直阻塞 dataBodyLen := binary.BigEndian.Uint32( recvBuf[4:]) // 读取包头中的数据体长度 io.ReadFull( read,recvBuf[ 8: 8 + dataBodyLen ] ) // 执行完这一句后,recvBuf 中保存的就是一个完整的逻辑包了,完整逻辑包的长度是 8 + dataBodyLen