轉自:http://www.01happy.com/golang-tcp-socket-adhere/
在用golang開發人工客服系統的時候碰到了粘包問題,那么什么是粘包呢?例如我們和客戶端約定數據交互格式是一個json格式的字符串:
{"Id":1,"Name":"golang","Message":"message"}
當客戶端發送數據給服務端的時候,如果服務端沒有及時接收,客戶端又發送了一條數據上來,這時候服務端才進行接收的話就會收到兩個連續的字符串,形如:
{"Id":1,"Name":"golang","Message":"message"}{"Id":1,"Name":"golang","Message":"message"}
如果接收緩沖區滿了的話,那么也有可能接收到半截的json字符串,醬紫的話還怎么用json解碼呢?真是頭疼。以下用golang模擬了下這個粘包的產生。
備注:下面貼的代碼均可以運行於golang 1.3.1,如果發現有問題可以聯系我
粘包示例:
server.go
//粘包問題演示服務端 package main import ( "fmt" "net" "os" ) func main() { netListen, err := net.Listen("tcp", ":9988") CheckError(err) defer netListen.Close() Log("Waiting for clients") for { conn, err := netListen.Accept() if err != nil { continue } Log(conn.RemoteAddr().String(), " tcp connect success") go handleConnection(conn) } } func handleConnection(conn net.Conn) { buffer := make([]byte, 1024) for { n, err := conn.Read(buffer) if err != nil { Log(conn.RemoteAddr().String(), " connection error: ", err) return } Log(conn.RemoteAddr().String(), "receive data length:", n) Log(conn.RemoteAddr().String(), "receive data:", buffer[:n]) Log(conn.RemoteAddr().String(), "receive data string:", string(buffer[:n])) } } func Log(v ...interface{}) { fmt.Println(v...) } func CheckError(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } }
client.go
//粘包問題演示客戶端 package main import ( "fmt" "net" "os" "time" ) func sender(conn net.Conn) { for i := 0; i < 100; i++ { words := "{\"Id\":1,\"Name\":\"golang\",\"Message\":\"message\"}" conn.Write([]byte(words)) } } func main() { server := "127.0.0.1:9988" tcpAddr, err := net.ResolveTCPAddr("tcp4", server) if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } defer conn.Close() fmt.Println("connect success") go sender(conn) for { time.Sleep(1 * 1e9) } }
運行后查看服務端輸出:
可以看到json格式的字符串都粘到一起了,有種淡淡的憂傷了——頭疼的事情又來了。
粘包產生原因
關於粘包的產生原因網上有很多相關的說明,主要原因就是tcp數據傳遞模式是流模式,在保持長連接的時候可以進行多次的收和發。如果要深入了解可以看看tcp協議方面的內容。這里推薦下鳥哥的私房菜,講的非常通俗易懂。
粘包解決辦法
主要有兩種方法:
1、客戶端發送一次就斷開連接,需要發送數據的時候再次連接,典型如http。下面用golang演示一下這個過程,確實不會出現粘包問題。
//客戶端代碼,演示了發送一次數據就斷開連接的 package main import ( "fmt" "net" "os" "time" ) func main() { server := "127.0.0.1:9988" for i := 0; i < 10000; i++ { tcpAddr, err := net.ResolveTCPAddr("tcp4", server) if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } words := "{\"Id\":1,\"Name\":\"golang\",\"Message\":\"message\"}" conn.Write([]byte(words)) conn.Close() } for { time.Sleep(1 * 1e9) } }
服務端代碼參考上面演示粘包產生過程的服務端代碼
2、包頭+數據的格式,根據包頭信息讀取到需要分析的數據。形式如下圖:
golang粘包問題包頭定義
從數據流中讀取數據的時候,只要根據包頭和數據長度就能取到需要的數據。這個其實就是平時說的協議(protocol),只是這個數據傳輸協議非常簡單,不像tcp、ip等協議有較多的定義。在實際的過程中通常會定義協議類或者協議文件來封裝封包和解包的過程。下面代碼演示了封包和解包的過程:
protocol.go
//通訊協議處理,主要處理封包和解包的過程 package protocol import ( "bytes" "encoding/binary" ) const ( ConstHeader = "www.01happy.com" ConstHeaderLength = 15 ConstSaveDataLength = 4 ) //封包 func Packet(message []byte) []byte { return append(append([]byte(ConstHeader), IntToBytes(len(message))...), message...) } //解包 func Unpack(buffer []byte, readerChannel chan []byte) []byte { length := len(buffer) var i int for i = 0; i < length; i = i + 1 { if length < i+ConstHeaderLength+ConstSaveDataLength { break } if string(buffer[i:i+ConstHeaderLength]) == ConstHeader { messageLength := BytesToInt(buffer[i+ConstHeaderLength : i+ConstHeaderLength+ConstSaveDataLength]) if length < i+ConstHeaderLength+ConstSaveDataLength+messageLength { break } data := buffer[i+ConstHeaderLength+ConstSaveDataLength : i+ConstHeaderLength+ConstSaveDataLength+messageLength] readerChannel <- data i += ConstHeaderLength + ConstSaveDataLength + messageLength - 1 } } if i == length { return make([]byte, 0) } return buffer[i:] } //整形轉換成字節 func IntToBytes(n int) []byte { x := int32(n) bytesBuffer := bytes.NewBuffer([]byte{}) binary.Write(bytesBuffer, binary.BigEndian, x) return bytesBuffer.Bytes() } //字節轉換成整形 func BytesToInt(b []byte) int { bytesBuffer := bytes.NewBuffer(b) var x int32 binary.Read(bytesBuffer, binary.BigEndian, &x) return int(x) }
tips:解包的過程中要注意數組越界的問題;另外包頭要注意唯一性
server.go
//服務端解包過程 package main import ( "./protocol" "fmt" "net" "os" ) func main() { netListen, err := net.Listen("tcp", ":9988") CheckError(err) defer netListen.Close() Log("Waiting for clients") for { conn, err := netListen.Accept() if err != nil { continue } Log(conn.RemoteAddr().String(), " tcp connect success") go handleConnection(conn) } } func handleConnection(conn net.Conn) { //聲明一個臨時緩沖區,用來存儲被截斷的數據 tmpBuffer := make([]byte, 0) //聲明一個管道用於接收解包的數據 readerChannel := make(chan []byte, 16) go reader(readerChannel) buffer := make([]byte, 1024) for { n, err := conn.Read(buffer) if err != nil { Log(conn.RemoteAddr().String(), " connection error: ", err) return } tmpBuffer = protocol.Unpack(append(tmpBuffer, buffer[:n]...), readerChannel) } } func reader(readerChannel chan []byte) { for { select { case data := <-readerChannel: Log(string(data)) } } } func Log(v ...interface{}) { fmt.Println(v...) } func CheckError(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } }
client.go
//客戶端發送封包 package main import ( "./protocol" "fmt" "net" "os" "time" ) func sender(conn net.Conn) { for i := 0; i < 1000; i++ { words := "{\"Id\":1,\"Name\":\"golang\",\"Message\":\"message\"}" conn.Write(protocol.Packet([]byte(words))) } fmt.Println("send over") } func main() { server := "127.0.0.1:9988" tcpAddr, err := net.ResolveTCPAddr("tcp4", server) if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } conn, err := net.DialTCP("tcp", nil, tcpAddr) if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) } defer conn.Close() fmt.Println("connect success") go sender(conn) for { time.Sleep(1 * 1e9) } }
運行這個程序可以看到服務端很好的獲取到期望的json格式數據。完整代碼演示下載:
最后
上面演示的兩種方法適用於不同的場景。第一種方法比較適合被動型的場景,例如打開網頁,用戶有請求才處理交互。第二種方法適合於主動推送的類型,例如即時聊天系統,因為要即時給用戶推送消息,保持長連接是不可避免的,這時候就要用這種方法。