详说tcp粘包和半包
2020/6/18 17:27:36
本文主要是介绍详说tcp粘包和半包,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
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 total int64 = -1 var buf [4]byte bufs := buf[:] binary.BigEndian.PutUint32(bufs, uint32(len(data))) n, err := conn.Write(bufs) total += int64(n) n, err = conn.Write(data) total += int64(n) if 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
里取出指定大小的数据,然后进行数据解析,如果发现有半包的情况,就再读取一次,加上上次未解析的数据,再次重新解析。
这篇关于详说tcp粘包和半包的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-30用PydanticAI和Gemini 2.0构建Airflow的AI助手
- 2024-12-30阿里云ECS教程:新手入门必读
- 2024-12-30使用vxe-table的插槽时页面卡顿怎么优化?-icode9专业技术文章分享
- 2024-12-30在 Kotlin 中使用 Coil 怎么实现高斯模糊效果?-icode9专业技术文章分享
- 2024-12-30有哪些常见的方法和工具查看和分析域名访问量?-icode9专业技术文章分享
- 2024-12-30aar 文件和jar 文件的区别是什么?-icode9专业技术文章分享
- 2024-12-30Gradle引用依赖 annotationProcessor 和implementation 的区别是什么?-icode9专业技术文章分享
- 2024-12-30packaging 类型 jar.sha256 和 jar的区别是什么?-icode9专业技术文章分享
- 2024-12-30.aar 是什么文件?-icode9专业技术文章分享
- 2024-12-30Build > Build Bundle(s) / APK(s) 的作用是什么?-icode9专业技术文章分享