C#网络编程学习(5)---Tcp连接中出现的粘包、拆包问题 1、疑问 2、什么是粘包和拆包 3、粘包拆包的表现形式 4、粘包拆包的发生原因 5、粘包拆包解决方法 6、构造包头包尾方法

本文参考于CSDN博客wxy941011

我们使用第四个博客中的项目。
修改客户端为:连接成功后循环向服务器发送从1-100的数字。看看服务器会不会正常的接收100次数据。

C#网络编程学习(5)---Tcp连接中出现的粘包、拆包问题
1、疑问
2、什么是粘包和拆包
3、粘包拆包的表现形式
4、粘包拆包的发生原因
5、粘包拆包解决方法
6、构造包头包尾方法
可是我们发现服务器只接收了两次数据,为什么和期望的不一样呢,这就触发了粘包问题。

2、什么是粘包和拆包

当客户端不断向服务器发送数据包时,服务器就可能出现两个数据包粘在一起的情况。

而和Tcp同为传输层的Udp则不会发生粘包和拆包问题。因为Udp是基于报文发送的,从Udp帧结构可以看出,Udp首部采用了16bit来显示Udp数据报文长度,因此在应用层可以很好的把不同数据报文分开,避免了粘包和拆包问题。

TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段。所以只有Tcp会发生粘包、拆包现象

3、粘包拆包的表现形式

客户端向服务器连续发送两个数据包,packet1、packet2,服务器收到有三种情况。

  1. 正常收到了两个数据包
    C#网络编程学习(5)---Tcp连接中出现的粘包、拆包问题
1、疑问
2、什么是粘包和拆包
3、粘包拆包的表现形式
4、粘包拆包的发生原因
5、粘包拆包解决方法
6、构造包头包尾方法

  2. 只收到一个数据包,由于Tcp不会出现丢包现象,所以这一个数据包包含了两个数据包的信息,称为粘包。
    C#网络编程学习(5)---Tcp连接中出现的粘包、拆包问题
1、疑问
2、什么是粘包和拆包
3、粘包拆包的表现形式
4、粘包拆包的发生原因
5、粘包拆包解决方法
6、构造包头包尾方法

  3. 接收到两个错误的数据报,发生了粘包+拆包
    C#网络编程学习(5)---Tcp连接中出现的粘包、拆包问题
1、疑问
2、什么是粘包和拆包
3、粘包拆包的表现形式
4、粘包拆包的发生原因
5、粘包拆包解决方法
6、构造包头包尾方法

4、粘包拆包的发生原因

  1. 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  2. 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
  3. 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  4. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
    等等。

5、粘包拆包解决方法

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
等等。

6、构造包头包尾方法

class EncodeTool
{
    /// <summary>
    /// 构造包  包头+包尾
    /// </summary>
    public static byte[] EncodePacket(byte[] data)
    {
        //内存流用完需要close释放,using自动释放。
        using (MemoryStream ms = new MemoryStream())
        {
            //bw用于向ms流中写入内容
            using (BinaryWriter bw = new BinaryWriter(ms))
            {
                //写入包头(数据的长度),把字节数组写入流
                bw.Write(data.Length);
                //写入包尾(数据)
                bw.Write(data);

                //拿到写入的数据
                byte[] packet = new byte[ms.Length];
                Buffer.BlockCopy(ms.GetBuffer(), 0, packet, 0, (int) ms.Length); //把内存流中的数据复制到packet中
                return packet;
            }
        }
    }

    /// <summary>
    /// 解析包,从缓冲区里取出一个完成的包
    /// </summary>
    public static byte[] DecodePacket(ref List<byte> cache)
    {
        if (cache.Count < 4)
        {
            return null;
        }

        //这种构造实例根据byte类型的字节数组进行初始化
        //并且实例的容量大小固定为字节数组的长度
        using (MemoryStream ms = new MemoryStream(cache.ToArray()))
        {
            //从流中读取数据
            using (BinaryReader br = new BinaryReader(ms))
            {
                int length = br.ReadInt32(); //读取前四个字节(包头-数据的长度)
                //读取的长度和缓冲区剩余的长度进行比较
                int remainLength = (int) ms.Length - (int) ms.Position; //流中剩余的长度
                if (length > remainLength)
                {
                    return null;
                }

                byte[] data = br.ReadBytes(length);
                //更新数据缓存
                cache.Clear();
                cache.AddRange(br.ReadBytes(remainLength)); //把剩余的再填进去???
                return data;
            }
        }
    }

}