#### 5.2 消息的封包與拆包
我們這里就是采用經典的TLV\(Type-Len-Value\)封包格式來解決TCP粘包問題吧。
由于Zinx也是TCP流的形式傳播數據,難免會出現消息1和消息2一同發送,那么zinx就需要有能力區分兩個消息的邊界,所以Zinx此時應該提供一個統一的拆包和封包的方法。在發包之前打包成如上圖這種格式的有head和body的兩部分的包,在收到數據的時候分兩次進行讀取,先讀取固定長度的head部分,得到后續Data的長度,再根據DataLen讀取之后的body。這樣就能夠解決粘包的問題了。
##### A\) 創建拆包封包抽象類
在`zinx/ziface`下,創建`idatapack.go`文件
> zinx/ziface/idatapack.go
```go
package ziface
/*
封包數據和拆包數據
直接面向TCP連接中的數據流,為傳輸數據添加頭部信息,用于處理TCP粘包問題。
*/
type IDataPack interface{
GetHeadLen() uint32 //獲取包頭長度方法
Pack(msg IMessage)([]byte, error) //封包方法
Unpack([]byte)(IMessage, error) //拆包方法
}
```
##### B\) 實現拆包封包類
在`zinx/znet/`下,創建`datapack.go`文件.
> zinx/znet/datapack.go
```go
package znet
import (
"bytes"
"encoding/binary"
"errors"
"zinx/utils"
"zinx/ziface"
)
//封包拆包類實例,暫時不需要成員
type DataPack struct {}
//封包拆包實例初始化方法
func NewDataPack() *DataPack {
return &DataPack{}
}
//獲取包頭長度方法
func(dp *DataPack) GetHeadLen() uint32 {
//Id uint32(4字節) + DataLen uint32(4字節)
return 8
}
//封包方法(壓縮數據)
func(dp *DataPack) Pack(msg ziface.IMessage)([]byte, error) {
//創建一個存放bytes字節的緩沖
dataBuff := bytes.NewBuffer([]byte{})
//寫dataLen
if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetDataLen()); err != nil {
return nil, err
}
//寫msgID
if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetMsgId()); err != nil {
return nil, err
}
//寫data數據
if err := binary.Write(dataBuff, binary.LittleEndian, msg.GetData()); err != nil {
return nil ,err
}
return dataBuff.Bytes(), nil
}
//拆包方法(解壓數據)
func(dp *DataPack) Unpack(binaryData []byte)(ziface.IMessage, error) {
//創建一個從輸入二進制數據的ioReader
dataBuff := bytes.NewReader(binaryData)
//只解壓head的信息,得到dataLen和msgID
msg := &Message{}
//讀dataLen
if err := binary.Read(dataBuff, binary.LittleEndian, &msg.DataLen); err != nil {
return nil, err
}
//讀msgID
if err := binary.Read(dataBuff, binary.LittleEndian, &msg.Id); err != nil {
return nil, err
}
//判斷dataLen的長度是否超出我們允許的最大包長度
if (utils.GlobalObject.MaxPacketSize > 0 && msg.DataLen > utils.GlobalObject.MaxPacketSize) {
return nil, errors.New("Too large msg data recieved")
}
//這里只需要把head的數據拆包出來就可以了,然后再通過head的長度,再從conn讀取一次數據
return msg, nil
}
```
需要注意的是整理的`Unpack`方法,因為我們從上圖可以知道,我們進行拆包的時候是分兩次過程的,第二次是依賴第一次的dataLen結果,所以`Unpack`只能解壓出包頭head的內容,得到msgId 和 dataLen。之后調用者再根據dataLen繼續從io流中讀取body中的數據。
##### C\) 測試拆包封包功能
為了容易理解,我們先不用集成zinx框架來測試,而是單獨寫一個Server和Client來測試一下封包拆包的功能
> Server.go
```go
package main
import (
"fmt"
"io"
"net"
"zinx/znet"
)
//只是負責測試datapack拆包,封包功能
func main() {
//創建socket TCP Server
listener, err := net.Listen("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("server listen err:", err)
return
}
//創建服務器gotoutine,負責從客戶端goroutine讀取粘包的數據,然后進行解析
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("server accept err:", err)
}
//處理客戶端請求
go func(conn net.Conn) {
//創建封包拆包對象dp
dp := znet.NewDataPack()
for {
//1 先讀出流中的head部分
headData := make([]byte, dp.GetHeadLen())
_, err := io.ReadFull(conn, headData) //ReadFull 會把msg填充滿為止
if err != nil {
fmt.Println("read head error")
break
}
//將headData字節流 拆包到msg中
msgHead, err := dp.Unpack(headData)
if err != nil {
fmt.Println("server unpack err:", err)
return
}
if msgHead.GetDataLen() > 0 {
//msg 是有data數據的,需要再次讀取data數據
msg := msgHead.(*znet.Message)
msg.Data = make([]byte, msg.GetDataLen())
//根據dataLen從io中讀取字節流
_, err := io.ReadFull(conn, msg.Data)
if err != nil {
fmt.Println("server unpack data err:", err)
return
}
fmt.Println("==> Recv Msg: ID=", msg.Id, ", len=", msg.DataLen, ", data=", string(msg.Data))
}
}
}(conn)
}
}
```
> Client.go
```go
package main
import (
"fmt"
"net"
"zinx/znet"
)
func main() {
//客戶端goroutine,負責模擬粘包的數據,然后進行發送
conn, err := net.Dial("tcp", "127.0.0.1:7777")
if err != nil {
fmt.Println("client dial err:", err)
return
}
//創建一個封包對象 dp
dp := znet.NewDataPack()
//封裝一個msg1包
msg1 := &znet.Message{
Id: 0,
DataLen: 5,
Data: []byte{'h', 'e', 'l', 'l', 'o'},
}
sendData1, err := dp.Pack(msg1)
if err != nil {
fmt.Println("client pack msg1 err:", err)
return
}
msg2 := &znet.Message{
Id: 1,
DataLen: 7,
Data: []byte{'w', 'o', 'r', 'l', 'd', '!', '!'},
}
sendData2, err := dp.Pack(msg2)
if err != nil {
fmt.Println("client temp msg2 err:", err)
return
}
//將sendData1,和 sendData2 拼接一起,組成粘包
sendData1 = append(sendData1, sendData2...)
//向服務器端寫數據
conn.Write(sendData1)
//客戶端阻塞
select {}
}
```
運行Server.go
```
go run Server.go
```
運行Client.go
```
go run Client.go
```
我們從服務端看到運行結果
```bash
$go run Server.go
==> Recv Msg: ID= 0 , len= 5 , data= hello
==> Recv Msg: ID= 1 , len= 7 , data= world!!
```
我們成功的得到了客戶端發送的兩個包,并且成功的解析出來。
- 一、引言
- 1、寫在前面
- 2、初探Zinx架構
- 二、初識Zinx框架
- 1. Zinx-V0.1-基礎Server
- 2.Zinx-V0.2-簡單的連接封裝與業務綁定
- 三、Zinx框架基礎路由模塊
- 3.1 IRequest 消息請求抽象類
- 3.2 IRouter 路由配置抽象類
- 3.3 Zinx-V0.3-集成簡單路由功能
- 3.4 Zinx-V0.3代碼實現
- 3.5 使用Zinx-V0.3完成應用程序
- 四、Zinx的全局配置
- 4.1 Zinx-V0.4增添全局配置代碼實現
- 4.2 使用Zinx-V0.4完成應用程序
- 五、Zinx的消息封裝
- 5.1 創建消息封裝類型
- 5.2 消息的封包與拆包
- 5.3 Zinx-V0.5代碼實現
- 5.4 使用Zinx-V0.5完成應用程序
- 六、Zinx的多路由模式
- 6.1 創建消息管理模塊
- 6.2 Zinx-V0.6代碼實現
- 6.3 使用Zinx-V0.6完成應用程序
- 七、Zinx的讀寫分離模型
- 7.1 Zinx-V0.7代碼實現
- 7.2 使用Zinx-V0.7完成應用程序
- 八、Zinx的消息隊列及多任務機制
- 8.1 創建消息隊列
- 8.2 創建及啟動Worker工作池
- 8.3 發送消息給消息隊列
- 8.4 Zinx-V0.8代碼實現
- 8.5 使用Zinx-V0.8完成應用程序
- 九、Zinx的鏈接管理
- 9.1 創建鏈接管理模塊
- 9.2 鏈接管理模塊集成到Zinx中
- 9.3 鏈接的帶緩沖的發包方法
- 9.4 注冊鏈接啟動/停止自定義Hook方法功能
- 9.5 使用Zinx-V0.9完成應用程序
- 十、Zinx的連接屬性設置
- 10.1 給鏈接添加鏈接配置接口
- 10.2 鏈接屬性方法實現
- 10.3 鏈接屬性Zinx-V0.10單元測試
- 基于Zinx的應用案例
- 一、應用案例介紹
- 二、服務器應用基礎協議
- 三、MMO多人在線游戲AOI算法
- 3.1 網絡法實現AOI算法
- 3.2 實現AOI格子結構
- 3.3 實現AOI管理模塊
- 3.4 求出九宮格
- 3.5 AOI格子添加刪除操作
- 3.6 AOI模塊單元測試
- 四、數據傳輸協議protocol buffer
- 4.1 簡介
- 4.2 數據交換格式
- 4.3 protobuf環境安裝
- 4.4 protobuf語法
- 4.5 編譯protobuf
- 4.6 利用protobuf生成的類來編碼
- 五、MMO游戲的Proto3協議
- 六、構建項目與用戶上線
- 6.1 構建項目
- 6.2用戶上線流程
- 七、世界聊天系統實現
- 7.1 世界管理模塊
- 7.2 世界聊天系統實現
- 八、上線位置信息同步
- 九、移動位置與AOI廣播(未跨越格子)
- 十、玩家下線
- 十一、移動與AOI廣播(跨越格子)