tcp
服務端和客戶端建立連線後會長時間維持這個連線,用於互相傳遞資料,tcp
是以流的方式傳輸資料的,就像一個水管裡的水一樣,從一頭不斷的流向另一頭。
理想情況下,傳送的資料包都是獨立的,
現實要複雜一些,傳送方和接收方都有各自的緩衝區。
傳送緩衝區:應用不斷的把資料傳送到緩衝區,系統不斷的從緩衝區取資料傳送到接收端。
接收緩衝區:系統把接收到的資料放入緩衝區,應用不斷的從緩衝區獲取資料。
當傳送方快速的傳送多個資料包時,每個資料包都小於緩衝區,tcp
會將多次寫入的資料放入緩衝區,一次傳送出去,伺服器在接收到資料流無法區分哪部分資料包獨立的,這樣產生了粘包。
或者接收方因為各種原因沒有從緩衝區裡讀取資料,緩衝區的資料會積壓,等再取出資料時,也是無法區分哪部分資料包獨立的,一樣會產生粘包。
傳送方的資料包大於快取區了,其中有一部分資料會在下一次傳送,接收端一次接收到時的資料不是完整的資料,就會出現半包的情況。
我們可以還原一下粘包和半包,寫一個測試程式碼
服務端
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
裡取出指定大小的資料,然後進行資料解析,如果發現有半包的情況,就再讀取一次,加上上次未解析的資料,再次重新解析。