C#粘包和分包问题及解决方法

一:通过图解法来描述一下分包和粘包,这样客户更清晰直观的了解: 

C#粘包和分包问题及解决方法_第1张图片

下面对上面的图进行解释: 
1.正常情况:如果Socket Client 发送的数据包,在Socket Server端也是一个一个完整接收的,那个就不会出现粘包和分包情况,数据正常读取。 
2.粘包情况:Socket Client发送的数据包,在客户端发送和服务器接收的情况下都有可能发送,因为客户端发送的数据都是发送的一个缓冲buffer,然后由缓冲buffer最后刷到数据链路层的,那么就有可能把数据包2的一部分数据结合数据包1的全部被一起发送出去了,这样在服务器端就有可能出现这样的情况,导致读取的数据包包含了数据包2的一部分数据,这就产生粘包,当然也有可能把数据包1和数据包2全部读取出来。 
3.分包情况:意思就是把数据包2或者数据包1都有可能被分开一部分发送出去,接着发另外的部分,在服务器端有可能一次读取操作只读到一个完整数据包的一部分。 
4.在数据包发送的情况下,有可能后面的数据包分开成2个或者多个,但是最前面的部分包,黏住在前面的一个完整或者部分包的后面,也就是粘包和分包同时产生了。

二:产生上情况的内部原因有下面几点: 
1.数据发送端发送数据给缓冲buffer太大,导致发送一个完整的数据包被分几次发送给缓存buffer,然而缓冲buffer等到数据满了以后会自动把数据发送的数据链路层去,这样就导致分包了。 
2.TCP协议定义有一个选项叫做最大报文段长度(MSS,Maximum Segment Size),该选项用于在TCP连接建立时,收发双方协商通信时每一个报文段所能承载的最大数据长度。在一定程度上MSS应该能尽可能多地承载用户数据,用于在传输通路上又可能避免分片,但是在复杂的网络环境下确定这个长度值非常困难,那么在这样的情况下在传输过程中产生分包,粘包就很常见了 
3.以太网,IP,TCP,UDP数据包分析大家可以仔细圆研究这篇文章http://www.cnblogs.com/feitian629/archive/2012/11/16/2774065.html 
4.数据帧的有效载荷(payload)比以太网的最大传输单元(MTU)大的时候,进行了IP分片。

三:解决数据分包和粘包的基本策略如下 
1.消息定长,比如定一个100,那么读取端每次读取数据就截取100个长度的数据,然后交给业务成去做解析 
2.在消息的尾部加一些特殊字符,那么在读取数据的时候,只要读到这个特殊字符,就认为已经可以截取一个完整的数据包了,这种情况在一定的业务情况下实用。 
3.读取缓存的数据是不定长的,所以我们把读取到的数据添加到我们自己的一个byte[]数组中,然后根据我们的业务逻辑来找到指定的特殊协议头部,协议长度,协议尾部,然后从我们的byte[]中获取一个完整的数据包,然后再对数据包进行业务解析就可以得到正确结果。

这里我取其中一种方法(方法3)在这里详细介绍一下:

首先将我们要发送的消息重新封装一下

namespace Tcp客户端
{
    class Message
    {

        public  byte[] SendData(byte[] message)
        {

            MemoryStream memoryStream = new MemoryStream();//创建一个内存流

            byte[] BagHead = BitConverter.GetBytes(message.Length + 4);//往字节数组中写入包头(包头自身的长度和消息体的长度)的长度
           
            memoryStream.Write(BagHead,0, BagHead.Length);//将包头写入内存流

            memoryStream.Write(message, 0, message.Length);//将消息体写入内存流

            byte[] HeadAndBody = memoryStream.ToArray();//将内存流中的数据写入字节数组

            memoryStream.Close();//关闭内存
            memoryStream.Dispose();//释放资源




            return HeadAndBody;
        }

当消息发送出去后,接收数据时就要解析数据

namespace TCP服务器
{
    class Messge
    {
        MemoryStream memoryStream = new MemoryStream();//开辟一个内存流
        public void ReceiveData(byte[]receivedata,int receiveLength)
        {

            memoryStream.Write(receivedata,0, receiveLength);//将接受到的数据写入内存流中
            byte[] getData = memoryStream.ToArray();//将内存流中的消息体写入字节数组
            int StartIndex = 0;//设置一个读取数据的起始下标
            while (true)
            {
                if (receiveLength > 0)//如果接受到的消息不为0(不为空)
                {
                    int HeadLength = 0;//包头长度(包头+包体)
                    if (getData.Length - StartIndex < 4)//包头接受不完整
                    {
                        HeadLength = -1;
                    }
                    else
                    {
                        //如果包头接受完整  转换成int类型的数值
                        HeadLength = BitConverter.ToInt32(getData, StartIndex);
                    }
                        //包头接受完整但是消息体不完整              //包头接受不完整
                           //↓↓↓↓↓↓↓↓                            ↓↓↓
                    if (getData.Length - StartIndex < HeadLength || HeadLength == -1)
                    {
                        memoryStream.Close();//关闭内存流
                        memoryStream.Dispose();//释放内存资源
                        memoryStream = new MemoryStream();//创建新的内存流
                        memoryStream.Write(getData, StartIndex, getData.Length - StartIndex);//从新将接受的消息写入内存流
                        break;
                    }
                    else
                    {
                        //如果消息(包头+包体)接受完整了,解析数据
                        string str = Encoding.UTF8.GetString(getData, StartIndex + 4, HeadLength - 4);
                        Console.WriteLine("接收到客户端的消息是-----" + str);

                        StartIndex += HeadLength;//当读取一条完整的数据后,读取数据的起始下标应为当前接受到的消息体的长度(当前数据的尾部或下一条消息的首部)
                    }
                }
            }
          

           
        }

    }
}

 

你可能感兴趣的:(C#粘包和分包问题及解决方法)