在使用golang做socket服务时,我想大多数人都会碰见粘包的问题。 以前用python做socket服务时就想写一篇关于tcp粘包的问题,后来因为单纯的tcp服务器开发功能实在烦杂,索性直接用http tornado进行通信了。
下面的资料有些是来自我个人的印象笔记,相关的参考引用链接早就找不到了。
该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。
什么是半包 ?
接受方没有接受到完整的包,只接受了一部分。 由于发送方看到内容太大切分数据包进行发送,这样切包能提高传输效率,如果一个包太大,接受方并不能一次接受完。(在长连接和短连接中都会出现)。
注: 半包、粘包都可以用后面的方法解决.
什么是分包?
既然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 接收方没能及时地接收缓冲区的包,造成多个包接收
解决办法:
为了避免粘包现象,可采取以下几种措施。
1. 对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
缺点: 第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。
2. 对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
缺点: 第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。
最后解决tcp粘包的方法:
客户端会定义一个标示,比如数据的前4位是数据的长度,后面才是数据。那么客户端只需发送 ( 数据长度+数据 ) 的格式数据就可以了,接收方根据包头信息里的数据长度读取buffer.
下面直接说golang socket下解决粘包的实例代码.
客户端:
#xiaorui.cc //客户端发送封包 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) } }
一些经典的问答:
在流传输中出现,UDP不会出现粘包,因为它有消息边界.
如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题.
END.