[ gev ] Go 語言優雅處理 TCP “粘包”
https://github.com/Allenxuxu/gev
gev 是一個輕量、快速的基於 Reactor 模式的非阻塞 TCP 網路庫,支援自定義協議,輕鬆快速搭建高效能伺服器。
TCP 為什麼會粘包
TCP 本身就是面向流的協議,就是一串沒有界限的資料。所以本質上來說 TCP 粘包是一個偽命題。
TCP 底層並不關心上層業務資料,會套接字緩衝區的實際情況進行包的劃分,一個完整的業務資料可能會被拆分成多次進行傳送,也可能會將多個小的業務資料封裝成一個大的資料包傳送(Nagle演算法)。
gev 如何優雅處理
gev 通過回撥函式 OnMessage 通知使用者資料到來,回撥函式中會將使用者資料緩衝區(ringbuffer)通過引數傳遞過來。
使用者通過對 ringbuffer 操作,來進行資料解包,獲取到完整使用者資料後再進行業務操作。這樣又一個明顯的缺點,就是會讓業務操作和自定義協議解析程式碼堆在一起。
所以,最近對 gev 進行了一次較大改動,主要是為了能夠以外掛的形式支援各種自定義的資料協議,讓使用者可以便捷處理 TCP 粘包問題,專注於業務邏輯。
做法如下,定義一個介面 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
相關文章
- go語言處理TCP拆包/粘包GoTCP
- C# 優雅的處理TCP資料(心跳,超時,粘包斷包,SSL加密 ,資料處理等)C#TCP加密
- Go TCP 粘包問題GoTCP
- TCP 粘包拆包TCP
- Netty Protobuf處理粘包分析Netty
- Go 語言異常處理Go
- TCP粘包拆包問題TCP
- [譯]Go如何優雅的處理異常Go
- Go 語言處理 yaml 檔案GoYAML
- TCP的粘包拆包技術TCP
- Go 語言操作 MySQL 之 預處理GoMySql
- Go語言基礎-錯誤處理Go
- Go語言錯誤處理機制Go
- 詳說tcp粘包和半包TCP
- Go語言實現TCP通訊GoTCP
- TCP 粘包 - 拆包問題及解決方案TCP
- Go語言處理—Day11—反射機制Go反射
- hanlp自然語言處理包的基本使用--pythonHanLP自然語言處理Python
- GO語言————6.8 閉包Go
- TCP協議粘包問題詳解TCP協議
- 漢語言處理包HanLP1.6.4釋出,優化新詞發現HanLP優化
- Go語言的 序列處理 和 並行處理 有什麼區別 ?Go並行
- 如何編譯執行HanLP自然語言處理包編譯HanLP自然語言處理
- Go 每日一庫之 go-carbon,優雅的 golang 日期時間處理庫Golang
- 如何優雅處理前端異常?前端
- 如何優雅的處理異常
- Go 語言閉包詳解Go
- Go 語言 context 包實踐GoContext
- [06 Go語言基礎-包]Go
- 如何優雅地處理前端異常?前端
- GO語言————4.7 strings和strconv 包Go
- Go 語言操作 MySQL 之 SQLX 包GoMySql
- go語言有哪些優勢Go
- 計算機網路 - TCP粘包、拆包以及解決方案計算機網路TCP
- 從錯誤處理看 Rust 的語言和 Go 語言的設計RustGo
- 在2018年如何優雅的開發一個typescript語言的npm包?TypeScriptNPM
- 如何在 Go 中優雅的處理和返回錯誤(1)——函式內部的錯誤處理Go函式
- 如何在 Swift 中優雅的處理閉包導致的迴圈引用Swift