[ gev ] Go 語言優雅處理 TCP “粘包”

惜朝發表於2019-11-01

https://github.com/Allenxuxu/gev

gev 是一個輕量、快速的基於 Reactor 模式的非阻塞 TCP 網路庫,支援自定義協議,輕鬆快速搭建高效能伺服器。

TCP 為什麼會粘包


TCP 本身就是面向流的協議,就是一串沒有界限的資料。所以本質上來說 TCP 粘包是一個偽命題。

TCP 底層並不關心上層業務資料,會套接字緩衝區的實際情況進行包的劃分,一個完整的業務資料可能會被拆分成多次進行傳送,也可能會將多個小的業務資料封裝成一個大的資料包傳送(Nagle演算法)。

gev 如何優雅處理


gev 通過回撥函式 OnMessage 通知使用者資料到來,回撥函式中會將使用者資料緩衝區(ringbuffer)通過引數傳遞過來。

使用者通過對 ringbuffer 操作,來進行資料解包,獲取到完整使用者資料後再進行業務操作。這樣又一個明顯的缺點,就是會讓業務操作和自定義協議解析程式碼堆在一起。

所以,最近對 gev 進行了一次較大改動,主要是為了能夠以外掛的形式支援各種自定義的資料協議,讓使用者可以便捷處理 TCP 粘包問題,專注於業務邏輯。

https://i.iter01.com/images/7ecc9766f331d6f26987bc538e6e27d1e168f1da6d1776b2fbff3066f460fab8.png


做法如下,定義一個介面 Protocol

```go
// Protocol 自定義協議編解碼介面
type Protocol interface {
UnPacket(c *Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte)
Packet(c *Connection, data []byte) []byte
}
```


使用者只需實現這個介面,並註冊到 server 中,當客戶端資料到來時,gev 會首先呼叫 UnPacket 方法,如果緩衝區中的資料足夠組成一幀,則將資料解包,並返回真正的使用者資料,然後在回撥 OnMessage 函式並將資料通過引數傳遞。

下面,我們實現一個簡單的自定義協議外掛,來啟動一個 Server :

```text
| 資料長度 n | payload |
| 4位元組 | n 位元組 |
```

```go
// protocol.go
package main

import (
"encoding/binary"
"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/ringbuffer"
"github.com/gobwas/pool/pbytes"
)

const exampleHeaderLen = 4

type ExampleProtocol struct{}

func (d *ExampleProtocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (interface{}, []byte) {
if buffer.VirtualLength() > exampleHeaderLen {
buf := pbytes.GetLen(exampleHeaderLen)
defer pbytes.Put(buf)
_, _ = buffer.VirtualRead(buf)
dataLen := binary.BigEndian.Uint32(buf)

if buffer.VirtualLength() >= int(dataLen) {
ret := make([]byte, dataLen)
_, _ = buffer.VirtualRead(ret)

buffer.VirtualFlush()
return nil, ret
} else {
buffer.VirtualRevert()
}
}
return nil, nil
}

func (d *ExampleProtocol) Packet(c *connection.Connection, data []byte) []byte {
dataLen := len(data)
ret := make([]byte, exampleHeaderLen+dataLen)
binary.BigEndian.PutUint32(ret, uint32(dataLen))
copy(ret[4:], data)
return ret
}
```

```go
// server.go
package main

import (
"flag"
"log"
"strconv"

"github.com/Allenxuxu/gev"
"github.com/Allenxuxu/gev/connection"
)

type example struct{}

func (s *example) OnConnect(c *connection.Connection) {
log.Println(" OnConnect : ", c.PeerAddr())
}
func (s *example) OnMessage(c *connection.Connection, ctx interface{}, data []byte) (out []byte) {
log.Println("OnMessage:", data)
out = data
return
}

func (s *example) OnClose(c *connection.Connection) {
log.Println("OnClose")
}

func main() {
handler := new(example)
var port int
var loops int

flag.IntVar(&port, "port", 1833, "server port")
flag.IntVar(&loops, "loops", -1, "num loops")
flag.Parse()

s, err := gev.NewServer(handler,
gev.Address(":"+strconv.Itoa(port)),
gev.NumLoops(loops),
gev.Protocol(&ExampleProtocol{}))
if err != nil {
panic(err)
}

log.Println("server start")
s.Start()
}
```
完整程式碼地址

當回撥 `OnMessage` 函式的時候,會通過引數傳遞已經拆好包的使用者資料。

當我們需要使用其他協議時,僅僅需要實現一個 Protocol 外掛,然後只要 `gev.NewServer` 時指定即可:

```go
gev.NewServer(handler, gev.NumLoops(2), gev.Protocol(&XXXProtocol{}))
```

## 基於 Protocol Plugins 模式為 gev 實現 WebSocket 外掛

得益於 Protocol Plugins 模式的引進,我可以將 WebSocket 的實現做成一個外掛(WebSocket 協議構建在 TCP 之上),獨立於 gev 之外。

```go
package websocket

import (
"log"

"github.com/Allenxuxu/gev/connection"
"github.com/Allenxuxu/gev/plugins/websocket/ws"
"github.com/Allenxuxu/ringbuffer"
)

// Protocol websocket
type Protocol struct {
upgrade *ws.Upgrader
}

// New 建立 websocket Protocol
func New(u *ws.Upgrader) *Protocol {
return &Protocol{upgrade: u}
}

// UnPacket 解析 websocket 協議,返回 header ,payload
func (p *Protocol) UnPacket(c *connection.Connection, buffer *ringbuffer.RingBuffer) (ctx interface{}, out []byte) {
upgraded := c.Context()
if upgraded == nil {
var err error
out, _, err = p.upgrade.Upgrade(buffer)
if err != nil {
log.Println("Websocket Upgrade :", err)
return
}
c.SetContext(true)
} else {
header, err := ws.VirtualReadHeader(buffer)
if err != nil {
log.Println(err)
return
}
if buffer.VirtualLength() >= int(header.Length) {
buffer.VirtualFlush()

payload := make([]byte, int(header.Length))
_, _ = buffer.Read(payload)

if header.Masked {
ws.Cipher(payload, header.Mask, 0)
}

ctx = &header
out = payload
} else {
buffer.VirtualRevert()
}
}
return
}

// Packet 直接返回
func (p *Protocol) Packet(c *connection.Connection, data []byte) []byte {
return data
}
```

具體的實現,可以到倉庫的 [plugins/websocket](https://github.com/Allenxuxu/gev/tree/master/plugins/websocket) 檢視。

## 相關文章

- [開源 gev: Go 實現基於 Reactor 模式的非阻塞 TCP 網路庫](https://note.mogutou.xyz/articles/2019/09/19/1568896693634.html)
- [Go 網路庫併發吞吐量測試](https://note.mogutou.xyz/articles/2019/09/22/1569146969662.html)

## 專案地址

https://github.com/Allenxuxu/gev

相關文章