這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
編程過程中遇到了粘包問題,看到這篇很詳盡的就mark下來了,雖然看代碼很簡單,也沒能解決我的粘包問題,但是對於自己瞭解粘包還是有用的麼,是吧。
在使用golang做socket服務時,我想大多數人都會碰見粘包的問題。 以前用python做socket服務時就想寫一篇關於tcp粘包的問題,後來因為單純的tcp伺服器開發功能實在煩雜,索性直接用http tornado進行通訊了。
下面的資料有些是來自我個人的印象筆記,相關的參考引用連結早就找不到了。
該文章寫的有些亂,歡迎來噴 ! 另外文章後續不斷更新中,請到原文地址查看更新。
http://xiaorui.cc/?p=2888
什麼是半包 ?
接受方沒有接受到完整的包,只接受了一部分。 由於發送方看到內容太大切分資料包進行發送,這樣切包能提高傳輸效率,如果一個包太大,接受方並不能一次接受完。(在長串連和短串連中都會出現)。
注: 半包、粘包都可以用後面的方法解決.
什麼是分包?
既然tcp的包產生了粘包,那麼需要分開處理吧。 對,這就是分包 ! 分包的前提是用戶端和服務端都提前定義一組結構,可以讓你準確拆分粘包的結構。
什麼時候需要考慮粘包的問題?
1: 類似 http的請求就不用考慮粘包的問題,因為服務端收到報文後, 就將緩衝區資料接收, 然後關閉串連,這樣粘包問題不用考慮到,因為大家都知道是發送一段字元。
2:如果發送資料無結構,如檔案傳輸,這樣發送方只管發送,接收方只管接收儲存就ok,也不用考慮粘包
3:如果雙方建立串連,需要在串連後一段時間內發送不同結構資料,如串連後,有好幾種結構: 1)”save it” 2)”delete it “ 這時候很不巧,發送方連續發送這個兩個包出去,接收方一次接收可能會是”saveit delete it” 這樣接收方就傻了,到底是要幹嘛? 不知道,因為協議沒有規定這麼詭異的字串,所以要處理把它分包,怎麼分也需要雙方組織一個比較好的包結構,所以一般可能會在頭加一個資料長度之類的包,以確保接收。 接著我們用虛擬碼來實現下tcp粘包的情境.
粘包問題就是TCP在傳輸資料時, 為了提高傳輸速度和效率, 把發送緩衝區中的資料拼為一個資料包發送到目的地 比如:
發送方:send(s, “abce”);send(s, “decfg”);
接收方:recv(s, buf); //buf = “abcedecfg”;
再廢話下,用一段話來描述什麼是tcp粘包:
出現粘包現象的原因既可能由發送方造成,也可能由接收方造成。
1 發送端需要等緩衝區滿才發送出去,造成粘包
2 接收方沒能及時地接收緩衝區的包,造成多個包接收
解決辦法:
為了避免粘包現象,可採取以下幾種措施。
- 對於發送方引起的粘包現象,使用者可通過編程設定來避免,TCP提供了強制資料立即傳送的操作指令push,TCP軟體收到該操作指令後,就立即將本段資料發送出去,而不必等待發送緩衝區滿;
缺點: 第一種編程設定方法雖然可以避免發送方引起的粘包,但它關閉了最佳化演算法,降低了網路發送效率,影響應用程式的效能,一般不建議使用。
- 對於接收方引起的粘包,則可通過最佳化程式設計、精簡接收進程工作量、提高接收進程優先順序等措施,使其及時接收資料,從而盡量避免出現粘包現象;
缺點: 第二種方法只能減少出現粘包的可能性,但並不能完全避免粘包,當發送頻率較高時,或由於網路突發可能使某個時間段資料包到達接收方較快,接收方還是有可能來不及接收,從而導致粘包。
最後解決tcp粘包的方法:
用戶端會定義一個標示,比如資料的前4位是資料的長度,後面才是資料。那麼用戶端只需發送 ( 資料長度+資料 ) 的格式資料就可以了,接收方根據包頭資訊裡的資料長度讀取buffer.
下面直接說golang socket下解決粘包的執行個體代碼.
用戶端:
//用戶端發送封包package main import ( "fmt" "math/rand" "net" "os" "strconv" "strings" "time") func main() { server := "127.0.0.1:5000" 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() for i := 0; i < 50; i++ { //msg := strconv.Itoa(i) msg := RandString(i) msgLen := fmt.Sprintf("%03s", strconv.Itoa(len(msg))) //fmt.Println(msg, msgLen) words := "aaaa" + msgLen + msg //words := append([]byte("aaaa"), []byte(msgLen), []byte(msg)) fmt.Println(len(words), words) conn.Write([]byte(words)) }} /***產生隨機字元**/func RandString(length int) string { rand.Seed(time.Now().UnixNano()) rs := make([]string, length) for start := 0; start < length; start++ { t := rand.Intn(3) if t == 0 { rs = append(rs, strconv.Itoa(rand.Intn(10))) } else if t == 1 { rs = append(rs, string(rand.Intn(26)+65)) } else { rs = append(rs, string(rand.Intn(26)+97)) } } return strings.Join(rs, "")}
服務端:
package main import ( "fmt" "io" "net" "os" "strconv") func main() { netListen, err := net.Listen("tcp", ":5000") CheckError(err) defer netListen.Close() for { conn, err := netListen.Accept() if err != nil { continue } go handleConnection(conn) }} func handleConnection(conn net.Conn) { allbuf := make([]byte, 0) buffer := make([]byte, 1024) for { readLen, err := conn.Read(buffer) //fmt.Println("readLen: ", readLen, len(allbuf)) if err == io.EOF { break } if err != nil { fmt.Println("read error") return } if len(allbuf) != 0 { allbuf = append(allbuf, buffer...) } else { allbuf = buffer[:] } var readP int = 0 for { //fmt.Println("allbuf content:", string(allbuf)) //buffer長度小於7 if readLen-readP < 7 { allbuf = buffer[readP:] break } msgLen, _ := strconv.Atoi(string(allbuf[readP+4 : readP+7])) logLen := 7 + msgLen //fmt.Println(readP, readP+logLen) //buffer剩餘長度>將處理的資料長度 if len(allbuf[readP:]) >= logLen { //fmt.Println(string(allbuf[4:7])) fmt.Println(string(allbuf[readP : readP+logLen])) readP += logLen //fmt.Println(readP, readLen) if readP == readLen { allbuf = nil break } } else { allbuf = buffer[readP:] break } } }} func CheckError(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) os.Exit(1) }}
代碼測試可以直接用