1. 程式人生 > >詳說tcp粘包和半包

詳說tcp粘包和半包

`tcp`服務端和客戶端建立連線後會長時間維持這個連線,用於互相傳遞資料,`tcp`是以流的方式傳輸資料的,就像一個水管裡的水一樣,從一頭不斷的流向另一頭。 理想情況下,傳送的資料包都是獨立的, ![](https://img2020.cnblogs.com/blog/342595/202006/342595-20200618093729752-684120100.png) 現實要複雜一些,傳送方和接收方都有各自的緩衝區。 傳送緩衝區:應用不斷的把資料傳送到緩衝區,系統不斷的從緩衝區取資料傳送到接收端。 接收緩衝區:系統把接收到的資料放入緩衝區,應用不斷的從緩衝區獲取資料。 當傳送方快速的傳送多個數據包時,每個資料包都小於緩衝區,`tcp`會將多次寫入的資料放入緩衝區,一次傳送出去,伺服器在接收到資料流無法區分哪部分資料包獨立的,這樣產生了粘包。 ![](https://img2020.cnblogs.com/blog/342595/202006/342595-20200618093618829-2033964403.png) 或者接收方因為各種原因沒有從緩衝區裡讀取資料,緩衝區的資料會積壓,等再取出資料時,也是無法區分哪部分資料包獨立的,一樣會產生粘包。 傳送方的資料包大於快取區了,其中有一部分資料會在下一次傳送,接收端一次接收到時的資料不是完整的資料,就會出現半包的情況。 ![](https://img2020.cnblogs.com/blog/342595/202006/342595-20200618094158265-728184357.png) 我們可以還原一下粘包和半包,寫一個測試程式碼 服務端 ``` func main() { l, err := net.Listen("tcp", ":8899") if err != nil { panic(err) } fmt.Println("listen to 8899") for { conn, err := l.Accept() if err != nil { panic(err) } else { go handleConn(conn) } } } func handleConn(conn net.Conn) { defer conn.Close() var buf [1024]byte for { n, err := conn.Read(buf[:]) if err != nil { break } else { fmt.Printf("recv: %s \n", string(buf[0:n])) } } } ``` 客戶端 ``` func main() { data := []byte("~測試資料:一二三四五~") conn, err := net.Dial("tcp", ":8899") if err != nil { panic(err) } for i := 0; i < 2000; i++ { if _, err = conn.Write(data); err != nil { fmt.Printf("write failed , err : %v\n", err) break } } } ``` 檢視一下輸出 ``` recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ ~測試資料:一二三四五~ recv: ~測試資料:一� recv: ��三四五~ ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ ~測試資料:一二三四五~ ~測試資料:一二三四五~ ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ ``` 正常情況下輸出是`recv: ~測試資料:一二三四五~`,發生粘包的時候會輸出多個數據包,當有半包的情況下輸出的是亂碼資料,再下一次會把剩下的半包資料也輸出。 要解決也簡單的就想辦法確定資料的邊界,常見的處理方式: * 固定長度: 比如規定所有的資料包長度為100byte,如果不夠則補充至100長度。優點就是實現很簡單,缺點就是空間有極大的浪費,如果傳遞的訊息中大部分都比較短,這樣就會有很多空間是浪費的,同樣浪費的還有流量。 * 分隔符:用分隔符來確定資料的邊界,這樣做比較簡單也不浪費空間,但資料包內就不能包含相應的分隔符,如果有會造成錯誤的解析。 * 資料頭:通過資料頭部來解析資料包長度,比如用4個位元組來當資料頭,儲存每個實資料包的長度。 個人更推薦資料頭方式來確定資料邊界,在傳送和接收資料時做好規定,每個資料包是不定長的,比如`4位元組的包頭+真實的資料`可以根據自己的業務進行擴充套件,比如上更多的包頭或者包尾,加上資料校驗等。 我修改一下上面的程式碼: 客戶端 ``` data := []byte("~測試資料:一二三四五~") conn, err := net.Dial("tcp", ":8899") if err != nil { panic(err) } for i := 0; i < 2000; i++ { var total int64 = -1 var buf [4]byte bufs := buf[:] binary.BigEndian.PutUint32(bufs, uint32(len(data))) n, err := conn.Write(bufs) total += int64(n) n, err = conn.Write(data) total += int64(n) if err != nil { fmt.Printf("write failed , err : %v\n", err) break } } ``` 服務端 ``` func main() { l, err := net.Listen("tcp", ":8899") if err != nil { panic(err) } fmt.Println("listen to 8899") for { conn, err := l.Accept() if err != nil { panic(err) } else { go handleConn(conn) } } } func handleConn(conn net.Conn) { defer conn.Close() for { var msgSize int32 err := binary.Read(conn, binary.BigEndian, &msgSize) if err != nil { break } buf := make([]byte, msgSize) _, err = io.ReadFull(conn, buf) if err != nil { break } fmt.Printf("recv: %s \n", string(buf)) } } ``` 執行再看一下輸出,沒有粘包或者半包的情況 ``` recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ recv: ~測試資料:一二三四五~ ``` 也可以像第一個例子一樣用一個指定大小的buf `var buf [1024]byte`,每次從`conn`裡取出指定大小的資料,然後進行資料解析,如果發現有半包的情況,就再讀取一次,加上上次未解析的資料,再次重新