go語言處理TCP粘包
TCP粘包是指發送方發送的若干數據包到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接著前一包數據的尾。
粘包可能由發送方造成,也可能由接收方造成。
粘包的原因:
發送端原因: 由於TCP協議本身的機制(面向連接的可靠的協議-三次握手機制)客戶端與服務器會維持一個連接,數據在連接不斷開的情況下,可以持續不斷地將多個數據包發往服務器,但是如果發送的網絡數據包太小,那麼他本身會啟用Nagle算法對較小的數據包進行合併,然後再發送(超時或者包大小足夠)。 那麼這樣的話,服務器在接收到消息(數據流)的時候就無法區分哪些數據包是客戶端自己分開發送的,這樣產生了粘包。
接收端原因: 服務器在接收到數據後,放到緩衝區中,如果消息沒有被及時從緩存區取走,下次在取數據的時候可能就會出現一次取出多個數據包的情況,造成粘包現象。
TCP粘包一般有3種解決方案:
(1)發送固定長度的消息
(2)把消息的尺寸與消息一塊發送
(3)使用特殊標記來區分消息間隔
這裡採用第二種解決方案,下面來看下go語言實現的TCP粘包現象和封包、拆包。
tcpserver.go
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [2048]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("reader.Read error :", err)
break
}
recvStr := string(buf[:n])
fmt.Printf("received data:%s\n\n", recvStr)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Listen error : ", err)
return
}
defer listen.Close()
fmt.Println("server start ... ")
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept error :", err)
continue
}
go process(conn)
}
}
tcpclient.go
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Dial error : ", err)
return
}
defer conn.Close()
fmt.Println("client start ... ")
for i := 0; i < 30; i++ {
msg := `hello hacker`
conn.Write([]byte(msg))
}
fmt.Println("send data over... ")
}
分別運行服務端和客戶端,結果如下:
server start ...
received data:hello hackerhello hackerhello hackerhello hackerhello hacker
received data:hello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerhello hackerello hackerhello hackerhello hackerhello hacker
received data:hello hackerhello hackerhello hackerhello hacker
received data:hello hacker
received data:hello hackerhello hackerhello hackerhello hackerhello hacker
可以看到發生了粘包現象,客戶端發送了30次,服務器端接收到5次。
我們採用第二種方案把消息的長度和消息體一起發送。
tcpclient2.go
package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
)
func Encode(message string) ([]byte, error) {
var length = uint16(len(message))
var nb = new(bytes.Buffer)
// 寫入消息頭
err := binary.Write(nb, binary.BigEndian, length)
if err != nil {
return nil, err
}
// 寫入消息體
nb.Write([]byte(message))
return nb.Bytes(), nil
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Dial error : ", err)
return
}
defer conn.Close()
for i := 0; i < 30; i++ {
msg := `hello hacker`
data, err := Encode(msg)
if err != nil {
fmt.Println("Encode msg error : ", err)
return
}
conn.Write(data)
}
}
客戶端封包先計算消息體的長度,作為消息的頭部,之後寫入消息體,一起發送。
tcpserver2.go
package main
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"io"
"net"
)
func Decode(reader *bufio.Reader) (string, error) {
// 讀取消息的長度
lengthByte, _ := reader.Peek(2) // 讀取前2個字節,看看包頭
fmt.Println("get byte",lengthByte)
lengthBuff := bytes.NewBuffer(lengthByte)
var length int16
err := binary.Read(lengthBuff, binary.BigEndian, &length)
if err != nil {
return "", err
}
fmt.Println("get length ",length)
if int16(reader.Buffered()) < length+2 {
return "", err
}
realData := make([]byte, int(2+length))
_, err = reader.Read(realData)
if err != nil {
return "", err
}
fmt.Println("get data",realData)
return string(realData[2:]), nil
}
func process2(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg, err := Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("Decode error : ", err)
return
}
fmt.Println("received data :", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("net.Listen error :", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept error :", err)
continue
}
go process2(conn)
}
}
服務器端先讀取消息的長度,之後在讀取消息體中的內容
分別運行服務端和客戶端,可以看到服務器端接收數據正常。