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粘包和半包的更多相关文章

  1. 关于TCP封包、粘包、半包

    关于Tcp封包 很多朋友已经对此作了不少研究,也花费不少心血编写了实现代码和blog文档.当然也充斥着一些各式的评论,自己看了一下,总结一些心得. 首先我们学习一下这些朋友的心得,他们是: http: ...

  2. socket编程 TCP 粘包和半包 的问题及解决办法

    一般在socket处理大数据量传输的时候会产生粘包和半包问题,有的时候tcp为了提高效率会缓冲N个包后再一起发出去,这个与缓存和网络有关系. 粘包 为x.5个包 半包 为0.5个包 由于网络原因 一次 ...

  3. TCP的粘包、半包和Netty的处理

    参考文献:极客时间傅健老师的<Netty源码剖析与实战>Talk is cheap.show me the code! 什么是粘包和半包 在客户端发送数据时,实际是把数据写入到了TCP发送 ...

  4. Netty - 粘包和半包(上)

    在网络传输中,粘包和半包应该是最常出现的问题,作为 Java 中最常使用的 NIO 网络框架 Netty,它又是如何解决的呢?今天就让我们来看看. 定义 TCP 传输中,客户端发送数据,实际是把数据写 ...

  5. C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)

    介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿. 一般来说,教你C#下Socket编程的老师,很少会教你如何解 ...

  6. Netty - 粘包和半包(下)

    上一篇介绍了粘包和半包及其通用的解决方案,今天重点来看一下 Netty 是如何实现封装成帧(Framing)方案的. 解码核心流程 之前介绍过三种解码器FixedLengthFrameDecoder. ...

  7. TCP的组包、半包、粘包与分包

    一.概念 1)组包.简单的说就是tcp协议把过大的数据包分成了几个小的包传输,接收方要把同一组的数据包重新组合成一个完整的数据包. 2)半包.指接受方没有接受到一个完整的包,只接受了部分,这种情况主要 ...

  8. c# socket 解决粘包,半包

    处理原理: 半包:即一条消息底层分几次发送,先有个头包读取整条消息的长度,当不满足长度时,将消息临时缓存起来,直到满足长度再解码 粘包:两条完整/不完整消息粘在一起,一般是解码完上一条消息,然后再判断 ...

  9. TCP粘包和半包的处理方法

    先把处理的方法的代码放这里: 解析数据帧的代码: bool CSocket::findData(byte* buff, int& len) { for (int i = 0; i <= ...

随机推荐

  1. 如何为Form表单的多个提交按钮指定不同的Action地址?

    这是我很久以前看到的一个技巧,但我忘记在哪里了,当时遇到这样的需求,做了笔记,现在整理成文章分享出来,因为我感觉这个小技巧还是挺有用的,这种应用场景也算比较常见,比如一个表单有"保存&quo ...

  2. 搭建本地的yum仓库-较简单

    1.创建目录安装软件程序 1.在/root路径下创建123.sh文件,把此文件复制到123.sh里,  sh 123.sh2.首选安装nginx,作为web展示 3.强力清除老版本残留rpm -e n ...

  3. [SD心灵鸡汤]002.每月一则 - 2015.06

    1.用最多的梦面对未来 2.自己要先看得起自己,别人才会看得起你 3.一个今天胜过两个明天 4.要铭记在心:每天都是一年中最美好的日子 5.乐观者在灾祸中看到机会:悲观者在机会中看到灾祸 6.有勇气并 ...

  4. @Spring Boot程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码

    前言 这篇其实是对一年前的一篇文章的补坑. @Java Web 程序员,我们一起给程序开个后门吧:让你在保留现场,服务不重启的情况下,执行我们的调试代码 当时,就是在spring mvc应用里定义一个 ...

  5. 应用4:利用Filter限制用户浏览权限

    1. 使用 Filter 完成一个简单的权限模型: 1). 需求: ①. 管理权限 > 查看某人的权限 > 修改某人的权限 ②. 对访问进行权限控制: 有权限则可以访问, 否则提示: 没有 ...

  6. 04 . Python入门之条件语句

    一. Python条件语句 Python条件语句是通过一条或多条语句执行结果(True或False)来决定执行的代码块. 可以通过下图简单了解语句的执行过程 Python程序语言指定任何非0和非空(n ...

  7. 【Linux】文件权限,ssh免密登录

    1.文件/文件夹权限 例子: -rw-r--r--. 1 root root 12288 Aug 21 09:50 aliases.db drwxr-xr-x. 2 root root 4096 Au ...

  8. Java实现 LeetCode 732 我的日程安排表 III(暴力 || 二叉树)

    732. 我的日程安排表 III 实现一个 MyCalendar 类来存放你的日程安排,你可以一直添加新的日程安排. MyCalendar 有一个 book(int start, int end)方法 ...

  9. Java实现 LeetCode 341 扁平化嵌套列表迭代器

    341. 扁平化嵌套列表迭代器 给你一个嵌套的整型列表.请你设计一个迭代器,使其能够遍历这个整型列表中的所有整数. 列表中的每一项或者为一个整数,或者是另一个列表.其中列表的元素也可能是整数或是其他列 ...

  10. Java实现蓝桥杯VIP算法训练 小生物的逃逸

    试题 算法训练 小生物的逃逸 资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 空间中有n个球,这些球不相交也不相切.有m个可以视为质点的小生物,可能在某些球内,也可能在所有球之外,但 ...