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 buf [4]byte
bufs := buf[:]
binary.BigEndian.PutUint32(bufs, uint32(len(data)))
if _, err := conn.Write(bufs); err != nil {
fmt.Printf("write failed , err : %v\n", err)
break
}
if _, err = conn.Write(data); 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
里取出指定大小的數據,然后進行數據解析,如果發現有半包的情況,就再讀取一次,加上上次未解析的數據,再次重新解析。