自定義協議/解決tcp粘包問題(golang版本)
Tcp/Udp介紹
Tcp是位元組流協議, 資料傳輸像流水一樣沒有邊界, 那麼對等方在一次資料讀取後,無法分辨讀取是一個訊息還是多個,
或者是不足一個, 那麼對等方拿到"殘缺"訊息就不知道如何處理.
Udp是基於訊息的傳輸服務,每個訊息就是一個報文,是有邊界的,對等方每次接收都是一個完整的訊息.
這樣就需要我們在應用層,
自己來區分.
粘包是如何出現的?
- 使用者程序write訊息, 但核心快取區不足以容乃這個完整的訊息, 一個訊息分多次傳送出去, 接收的時候就可能一個訊息分多次接收
- Tcp的報文段有大小限制(MSS)
- IP層最大傳輸單元(MTU), 會對包進行分片,
- 其他, Tcp流量控制, 擁塞控制
一般有三種常見的方式
1. 定長訊息
傳送端和接收端約定訊息長度, 缺點: 訊息很短時, 效率很低, 浪費頻寬
2. 特殊標誌作為結束標誌
ftp協議就是這種方式, 缺點: 訊息內容不能含有這種特殊標誌, 會提前終止訊息。 redis是如何解決類似的問題的呢, redis自定義
了動態字串, 裡面提到是二進位制安全的, 意思就是字串裡面可以含有空字元(assic碼為0), 原因就是它記錄了這個字串的長度,
其實也就是下面說的第三種方式
3. 定長的包頭 + 變長的包體, 包頭中寫入包體的長度, 本文主要介紹這種方式:
每次都要儘可能的去讀資料, 讀到之後分析:
先取包頭, 在包頭裡分析出包體的長度, 如果包頭都不夠, 要繼續讀資料拼接在已有的資料後面, 繼續分析包體的長度, 拿到包體的長度就從包頭結束的問題擷取包體, 依次遞迴, 直到對等方關閉
程式碼
// 讀取訊息, 可匯出的方法 func (buffer *Buffer) Read(msg chan string) (error) { for { buffer.grow()// 移動資料 _, err := buffer.readFromReader() // 讀資料拼接到定額快取後面 if err != nil { fmt.Println(err) return err } // 檢查定額快取裡面的資料有幾個訊息(可能不到1個,可能連一個訊息頭都不夠,可能有幾個完整訊息+一個訊息的部分) isBreak := buffer.checkMsg(msg) // 只要讀到有完整的訊息, isBreak就為true, 跳出去處理 if (isBreak) { return nil } } }
// grow 將有用的位元組前移, 不可匯出 func (b *Buffer) grow() { if b.start == 0 { return } copy(b.buf, b.buf[b.start:b.end]) b.end -= b.start b.start = 0 }
// 檢查應用層快取區是否包含完整的訊息, 不可匯出 func (buffer *Buffer) checkMsg(msg chan string) (bool) { var isBreak bool HEADER_LENG := HEAD_SIZE + len(buffer.header) headBuf, err1 := buffer.seek(HEADER_LENG) if err1 != nil { // 一個訊息頭都不夠, 跳出去繼續讀吧 return false } if (string(headBuf[:len(buffer.header)]) == buffer.header) { // 判斷訊息頭正確性 } else { } contentSize := int(binary.BigEndian.Uint16(headBuf[len(buffer.header):])) if (buffer.len() >= contentSize-HEADER_LENG) { // 一個訊息體也是夠的 contentBuf := buffer.read(HEADER_LENG, contentSize) // 把訊息讀出來,把start往後移 msg <- string(contentBuf) // 遞迴,看剩下的還夠一個訊息不 isBreak = true buffer.checkMsg(msg) } else { // 一個訊息體不夠的, 跳出去繼續讀吧 isBreak = false } return isBreak }
演示
go get github.com/weiwenwang/DiyProtocol cd $GOPATH/github.com/weiwenwang/DiyProtocol/example/server/ go run server.go
cd $GOPATH/github.com/weiwenwang/DiyProtocol/example/client/ go run client.go
詳情
原始碼請移步: ofollow,noindex" target="_blank">github , 本人附上一個demo, 程式碼註釋詳細.