在客户端传输的时候,如果我们想要进行消息的连发,或者说一次性发送多个消息包,就必要要解决Tcp粘包的问题。关于Tcp粘包在这里不作过多的讲解,另外本文主要是呈现解决思路,而并非实际的工业生产方案。
在这里为了解决上述粘包的问题,我们需要将原来的数据再进行一层包装,其基本的逻辑概念如下:
当然这个具体设计细节需要跟随自己的业务进行改变,在这里我还设计了一个id,主要是方便我后面的一些相关业务。主要解决粘包的还是前面的datalen,读取时会首先读取len,根据这个len来读取后面具体长度的data,进而解决Tcp粘包的问题。
创建一个项目
3.1 创建封装消息的接口及其实现类
package isticky
type IMessage interface {
GetMsgID() uint32
GetMsgLen() uint32
GetData() []byte
}
package sticky
type Message struct {
Id uint32 // 消息ID
DataLen uint32 // 消息的长度
Data []byte // 消息的内容
}
func (m *Message) GetMsgID() uint32 {
return m.Id
}
func (m *Message) GetMsgLen() uint32 {
return m.DataLen
}
func (m *Message) GetData() []byte {
return m.Data
}
3.2 实现打包、拆包的接口及其实现类⭐️
package isticky
type IDataPack interface {
GetHeadLen() uint32
Pack(IMessage) ([]byte, error)
UnPack([]byte) (IMessage, error)
}
package sticky
import (
"bytes"
"encoding/binary"
"fmt"
"v1/isticky"
)
type DataPack struct {
}
func (d *DataPack) GetHeadLen() uint32 {
// 根据自身设计:len 4字节 + id 4字节
return 8
}
// Pack 打包的实现
func (d *DataPack) Pack(message isticky.IMessage) ([]byte, error) {
// 创建一个buffer
buffer := bytes.NewBuffer([]byte{})
// 将DataLen写入
if err := binary.Write(buffer, binary.LittleEndian, message.GetMsgLen()); err != nil {
fmt.Println("Failed to pack in write DataLen:", err)
return nil, err
}
if err := binary.Write(buffer, binary.LittleEndian, message.GetMsgID()); err != nil {
fmt.Println("Failed to pack in write MsgId:", err)
return nil, err
}
if err := binary.Write(buffer, binary.LittleEndian, message.GetData()); err != nil {
fmt.Println("Failed to pack in write Data:", err)
return nil, err
}
return buffer.Bytes(), nil
}
// UnPack 拆包的实现
func (d *DataPack) UnPack(binaryData []byte) (isticky.IMessage, error) {
// 创建一个二进制的io.Reader
dataBuffer := bytes.NewReader(binaryData)
// 解压Head信息
msg := &Message{}
// 读DataLen
if err := binary.Read(dataBuffer, binary.LittleEndian, &msg.DataLen); err != nil {
fmt.Println("Failed to unpack in read DataLen:", err)
return nil, err
}
// 读Id
if err := binary.Read(dataBuffer, binary.LittleEndian, &msg.Id); err != nil {
fmt.Println("Failed to unpack in read id/:", err)
return nil, err
}
// 注意到这里消息还没有解析
return msg, nil
}
3.3 测试封包拆包的Server和Client
StickyBuns/sticky/datapack_test.go
package sticky
import (
"fmt"
"io"
"net"
"testing"
)
func TestDataPack(t *testing.T) {
/*
模拟服务器
*/
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("Failed to listen at 127.0.0.1:8080:", err)
return
}
go func() {
for {
// 进行Accept
conn, err := listener.Accept()
if err != nil {
fmt.Println("Failed to accept:", err)
continue
}
go func(conn net.Conn) {
dp := DataPack{}
for {
// 第一次从conn中读,把包的head(DataLen+Id)读取出来
headData := make([]byte, dp.GetHeadLen())
if _, err := io.ReadFull(conn, headData); err != nil {
fmt.Println("Failed to unpack head:", err)
return
}
// 第二次从conn中读取,之前head已经没有在conn中了,剩下的就是Data和其余消息体
// headData里面只有头部,将其放入了拆包函数中,解析出DataLen和ID的具体取值
// 尤其是DataLen他是关键的,因为后面需要该值来确定数据的长度
msgHead, err := dp.UnPack(headData)
if err != nil {
fmt.Println("Failed to unpack:", err)
return
}
msgData := make([]byte, msgHead.GetMsgLen())
if _, err := io.ReadFull(conn, msgData); err != nil {
fmt.Println("Failed to unpack data:", err)
}
fmt.Printf("---->DataLen=%d\tDataId=%d\tData=%s\n", msgHead.GetMsgLen(), msgHead.GetMsgID(), msgData)
}
}(conn)
}
}()
/*
模拟客户端
*/
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
fmt.Println("Failed dial to 127.0.0.1:8080:", err)
return
}
// 创建一个打包对象
dp := DataPack{}
// 新建一个消息体1
msg1 := &Message{
Id: 1,
DataLen: 5,
Data: []byte("hello"),
}
sendMsg1, err := dp.Pack(msg1)
if err != nil {
fmt.Println("Failed to pack msg1:", err)
return
}
msg2 := &Message{
Id: 1,
DataLen: 5,
Data: []byte("nihao"),
}
sendMsg2, err := dp.Pack(msg2)
// 将两个消息一起发送
sendMsg1 = append(sendMsg1, sendMsg2...)
_, err = conn.Write(sendMsg1)
if err != nil {
return
}
// 主程序阻塞
select {}
}
输出结果:
=== RUN TestDataPack
---->DataLen=5 DataId=1 Data=hello
---->DataLen=5 DataId=1 Data=nihao
即实现了上述的功能。如果你的业务非常的复杂,那么在这里就需要按照自身的业务对消息进行封装,然后分别根据自己对消息的封装然后分别设计自身的封包和拆包的方法。