TCP粘包,拆包现象及解决方案(C++/C#)(一)

        在以前的一个项目中,使用五台主机进行tcp通讯,一台为服务机,其余为客户机。当其中一台客户机数据的发生变化,要经过服务机通知其它客户机必须作出相应的状态变化,也就是数据不能丢弃或丢失。因为当时未考虑到数据TCP粘包,拆包的问题,在数据包格式不正确时直接执行了return,致使了程序未达到预期的要求,花了好久时间才找到问题的所在,因此写下这篇博客,来加深一下印象,须要tcp实时完整通讯是避免再次入坑。web

        发生粘包,拆包的缘由:

发生TCP粘包或拆包有不少缘由,现列出常见的几点,可能不全面,欢迎补充,c#

一、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。网络

二、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。async

三、要发送的数据小于TCP发送缓冲区的大小,TCP将屡次写入缓冲区的数据一次发送出去,将会发生粘包。tcp

四、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。学习

等等测试

        表现形式:

如今假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据能够分为三种,现列举以下:spa

第一种状况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种状况不在本文的讨论范围内。normal.net

第二种状况,接收端只收到一个数据包,因为TCP是不会出现丢包的,因此这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种状况因为接收端不知道这两个数据包的界限,因此对于接收端来讲很难处理。onecode

第三种状况,这种状况有两种表现形式,以下图。接收端收到了两个数据包,可是这两个数据包要么是不完整的,要么就是多出来一块,这种状况即发生了拆包和粘包。这两种状况若是不加特殊处理,对于接收端一样是很差处理的。half_oneone_half

解决方式:

缘由和现象是查阅资料或摘抄自别人的博客,比较容易理解,咱们主要作的是怎么解决这种问题:
        既然发生了数据拆包和粘包,咱们就要从数据的结构入手,如今知道的是确定不能再tcp包中只存放咱们所需的数据,由于这样发生了拆包和粘包,没有办法从新整理出咱们所须要的数据
        方式一:给数据添加首部,首部包含了数据的长度
        方式二:定长发送,不足的位补0
        方式三:添加数据尾部和首部
        。。。。。
方式一实现方式(C#):

客户端:

       static TcpClient tcpClient;
        static BinaryReader br;
        static BinaryWriter bw;
        static bool isConnect = false;
        static void Main(string[] args)
        {
            tcpClient = new TcpClient();
            reConnect();
           
            Console.ReadLine();
        }

        private static bool ConnectServer(string ip, int port)
        {
            try
            {
                tcpClient.Connect(IPAddress.Parse(ip), port);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return false;
            }

            try
            {
                NetworkStream networkStream = tcpClient.GetStream();
                //将网络流做为二进制读写对象
                
                br = new BinaryReader(networkStream);
                bw = new BinaryWriter(networkStream);
                Thread threadReceive = new Thread(new ThreadStart(ThreadMethod));
                threadReceive.IsBackground = true;
                threadReceive.Start();
             
                isConnect = true;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                isConnect = false;
                reConnect();
                return false;
            }

            return true;
        }

        private static void ThreadMethod()
        {
            byte[] nHeadLen = new byte[4]; //包含长度的数据首部
            bool isFirstReadHead = true;
            bool isFirstReadData = true;
            try
            {
                while (isConnect)
                {
                    if (isFirstReadHead)
                    {
                        nHeadLen = br.ReadBytes(4); //从字节流中读取四个字节,若是字节数小于四个,则继续读取
                    }

                    if (nHeadLen.Length < 4)
                    {
                        nHeadLen = nHeadLen.Concat(br.ReadBytes(4 - nHeadLen.Length)).ToArray();
                    }
                    if (nHeadLen.Length == 4) //读到一个整形的数据后,读指定长度的内容
                    {
                        isFirstReadHead = false;
                        int dataLen = BitConverter.ToInt32(nHeadLen, 0);
                        byte[] data = new byte[dataLen];


                        data = br.ReadBytes(dataLen);
                        if (data.Length < dataLen)  //若是读到数据没有到指定长度,继续读取
                        {
                            data = nHeadLen.Concat(br.ReadBytes(dataLen - nHeadLen.Length)).ToArray();
                        }
                        else
                        {
                            Console.WriteLine(System.Text.Encoding.Default.GetString(data));
                            Array.Clear(nHeadLen, 0, nHeadLen.Length);
                            isFirstReadHead = true;
                        }
                    }
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.ToString());
                onClose();
                tcpClient = new TcpClient();  //若是服务端断开,上一个链接关闭,必须从新建立对象
                isConnect = false;
                reConnect();
            }
         
        }

        private static async Task reConnect()
        {
            await Task.Run(()=>
                {
                    while(!isConnect)
                    {
                        Thread.Sleep(2000);
                        ConnectServer("127.0.0.1", 12334);
                        Console.WriteLine("Reconnecting.......");
                    }
                    Console.WriteLine("Connect Success********");
                }
                );
        }

        private static void onClose()
        {
            if (isConnect)
            {
                br.Close();
                bw.Close();
                tcpClient.Close();
            }
        }

string ms1 = "first_data";
string ms2 = "second_data";
string ms3 = "third_data_test";

客户端分别须要的ms1,ms2,ms3

在方法一的模式下,服务端须要发送的数据是 ms1.length (byte)+ ms1(byte),ms2.length (byte)+ ms2(byte),ms2.length (byte)+ ms3(byte)

模拟粘包发送,将三个数据包一块儿发送:ms1.length (byte)+ ms1(byte)+ ms2.length (byte)+ ms3(byte)+ ms2.length (byte)+ ms3(byte)

主要代码:

                byte[] bt = BitConverter.GetBytes(ms1.Length);
                    bt = bt.Concat(System.Text.Encoding.Default.GetBytes(ms1)).ToArray();

                    bt = bt.Concat(BitConverter.GetBytes(ms2.Length)).ToArray();
                    bt = bt.Concat(System.Text.Encoding.Default.GetBytes(ms2)).ToArray();

                    bt = bt.Concat(BitConverter.GetBytes(ms3.Length)).ToArray();
                    bt = bt.Concat(System.Text.Encoding.Default.GetBytes(ms3)).ToArray();

                    bw.Write(bt);
客户端能够正常输出三个所须要的数据。

而后使用For循环模拟拆包发送,使用for循环按字节发送每个数据:


客户端仍然能够输出所需的数据。

测试方式不太严谨,解决方式也是根据本身的理解去写的,可能存在不太合理的地方,欢迎指正:

c#源码:点击打开连接

欢迎加群一块儿深刻学习WPF/.Net/C#:Hello WPF