TCP是网络传输层中非常重要的传输协议,广泛应用于Http、WebSocket、FTP、Telnet、SMTP、POP3与DNS等应用协议。了解TCP的基本原理对我们分析网络问题有着举足轻重的作用。 此次我们先来了解下:如何解决TCP的粘包,半包的问题。

先来简单的了解下TCP协议的基本原理

TCP是以流动的方式传输数据,传输的最小单位为一个报文段(segment)。TCP Header中有个Options标识位,常见的标识为mss(Maximum Segment Size最大消息长度)指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,即:

MSS = MTU - header

TCP是数据安全的,它可以确保数据在传输的过程中不会出现数据丢失,这是因为TCP在传输过程中,会对传输的数据做校验,以确保数据接收的完整性。另外也因为TCP是分段传输,需要确保数据接收的顺序正确。也正是因为有这种机制,才导致TCP的传输效率相比UDP协议会更低,于是TCP为提高性能,发送端会将需要发送的数据发送到缓冲区,等待缓冲区满了之后,再将缓冲中的数据发送到接收方。同理,接收方也有缓冲区这样的机制,来接收数据。其中数据缓冲区的大小,默认65536字节。

那什么是粘包和半包?

粘包:指的是发送端在多次发送数据的过程中,数据包在同一个数据流中传输给了接收端,此现象就称之为粘包。

半包:发送端发送的数据大于发送缓冲区,接收端一次接收的数据不是完整的数据,此现象称之为半包

粘包、拆包发生原因

原因很多种,有补充请在评论区留言

拆包:

  1. 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  2. 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

粘包:

  1. 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  2. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

粘包常规的解决方法

  1. 传输字符串类数据,可使用特殊字符作为分隔符。字节数组也可以使用特定的字节码来作为分隔符。如:\0
  2. 使用固定字节长度作为传输协议
  3. 添加数据头,通过数据头部来解析数据包长度

其中第一种和第二种方法都比较简单,也有一定的局限性,不推荐采用。就第三种实现方法,以C# 代码作为简单的案例分享。

准备工作,数据长度与字节数组的转换

        /// <summary>
        /// 将int类型以数组方式添加到目标数组
        /// </summary>
        /// <param name="data"></param>
        /// <param name="offset"></param>
        /// <param name="value"></param>
        public static void IntoBytes(byte[] data, int offset, int value)
        {
            data[offset++] = (byte)(value);
            data[offset++] = (byte)(value >> 8);
            data[offset++] = (byte)(value >> 16);
            data[offset] = (byte)(value >> 24);
        }

        /// <summary>
        /// bytes数据长度转成int类型
        /// </summary>
        /// <param name="data"></param>
        /// <param name="offset"></param>
        /// <returns></returns>
        public static int ToInt32(byte[] data, int offset)
        {
            return (int)(data[offset++] | data[offset++] << 8 | data[offset++] << 16 | data[offset] << 24);
        }

定义消息头部

        /// <summary>
        /// Demo消息头定义
        /// </summary>
        public struct SocketHead
        {
            //起始位,表示字节的开始
            public byte StartFlag;
            //校验位,检验数据是否正确
            public byte CheckNum;
            //协议位,表示需要执行什么功能
            public byte Cmd;
            //消息体数据长度
            public int Length;
        }

构建数据包头

        public static byte[] BuildData(byte cmd, byte[] data)
        {
            byte[] buffer = new byte[7 + data.Length];

            byte startFlag = 0xF;
            //起始位
            buffer[0] = startFlag;
            //指令位
            buffer[1] = cmd;
            //校验位
            buffer[2] = (byte)(cmd + startFlag);

            IntoBytes(buffer, 3, data.Length);

            Array.Copy(data, 0, buffer, 7, data.Length);

            return buffer;
        }

解析数据包头

        public static bool ParseHead(byte[] data, out SocketHead socketHead)
        {
            if (data.Length >= 7)
            {
                socketHead = new SocketHead
                {
                    StartFlag = data[0],
                    Cmd = data[1],
                    CheckNum = data[2]
                };
                //验证数据是否正确
                if (socketHead.CheckNum == socketHead.StartFlag + socketHead.Cmd)
                {
                    socketHead.Length = ToInt32(data, 3);
                    return true;
                }
                return false;
            }
            socketHead = new SocketHead();
            return false;
        }

发送数据

        //示例代码,非完整代码
        public bool SendData(byte[] data)
        {
            //将头部增加到数据中
            var bufferData = SocketWrap.BuildData(0x9, data);
            //发送缓存数据
            var len = _tcpClient.Send(bufferData);
            if(len==bufferData.Length)
            {
                return true;
            }
            return false;
        }        

接收数据

        //示例代码,非完整代码
        public void ReviceData()
        {
            try
            {
                while (_tcpClient.Connected)
                {
                    var socketClient = _tcpClient.Client;
                    if (socketClient == null)
                    {
                        break;
                    }

                    byte[] head = new byte[7];
                    var receiveCount = socketClient.Receive(head);
                    if (receiveCount != 0)
                    {
                        if (SocketWrap.ParseHead(head, out SocketWrap.SocketHead socketHead))
                        {
                            var len = socketHead.Length;

                            byte[] data = new byte[len];

                            var count = 1;
                            if (len > 1024)
                            {
                                var ys = len % 1024;
                                var c = len / 1024;
                                if (ys > 0)
                                {
                                    count = c + 1;
                                }
                                else
                                {
                                    count = c;
                                }
                            }

                            if (count == 1)
                            {

                                byte[] buffer = new byte[len];
                                _tcpClient.Client.Receive(buffer);

                                buffer.CopyTo(data, 0);

                                //todo:可以增加数据接收完成事件
                            }
                            else
                            {
                                byte[] buffer = new byte[1024];
                                for (int i = 0; i < count - 1; i++)
                                {
                                    _tcpClient.Client.Receive(buffer);

                                    buffer.CopyTo(data, i * 1024);
                                }

                                var d = len - (count - 1) * 1024;

                                _tcpClient.Client.Receive(buffer);

                                Array.Copy(buffer, 0, data, (count - 1) * 1024, d);

                                //todo:可以增加数据接收完成事件
                            }
                        }
                    }
                    else
                    {
                        //数据接收返回为0,表示连接已经断开了,需要做重连
                        throw new SocketException(5);
                    }
                }
            }
            catch (SocketException ex)
            {
                //todo:
            }
            catch
            {
                //todo:
            }
        }

以上接收代码看似没毛病,但是在实际运行过程中,我们可能会出现发送的数据和接收的数据不一致!!!

那么为什么会出现这样的情况呢?

我们找来 SocketTool 测试工具进行了一轮测试,使用SocketTool来接收相应的消息;结果发现,SocketTool能够很好的接收完成相关数据。 那基本可以排除发送端的问题,那主要在接收端查找原因。 当我们把 _tcpClient.Client.Receive(buffer) 调用返回的数据全部打印出来查看时,我们发现这样的情况:

接收的数据并没有按buffer大小全部接收完成!

于是我们将以上代码重新做个修改,修改如下:

        //示例代码,非完整代码
        public void ReviceData()
        {
            try
            {
                while (_tcpClient.Connected)
                {
                    var socketClient = _tcpClient.Client;
                    if (socketClient == null)
                    {
                        break;
                    }

                    byte[] head = new byte[7];
                    var receiveCount = socketClient.Receive(head);
                    if (receiveCount != 0)
                    {
                        if (SocketWrap.ParseHead(head, out SocketWrap.SocketHead socketHead))
                        {
                            var len = socketHead.Length;
                            List<byte> data = new List<byte>();
                            int totalCount = 0;
                            int tmpSize = 0;
                            while (totalCount < len)
                            {
                                tmpSize = _tcpClient.Available;
                                if(tmpSize==0)
                                {
                                    Thread.Sleep(10);
                                    continue;
                                }

                                //保证接收到的数据是协议内的数据
                                if (totalCount + tmpSize >= len)
                                {
                                    var buffer = new byte[len - totalCount];
                                    _tcpClient.Client.Receive(buffer);
                                    data.AddRange(buffer);
                                    totalCount = len;
                                }
                                else
                                {
                                    var receiveBuffer = new byte[tmpSize];
                                    _tcpClient.Client.Receive(receiveBuffer);
                                    data.AddRange(receiveBuffer);
                                    totalCount += tmpSize;
                                }
                            }

                            DataReceived?.Invoke(this, new SocketDataArgs(data.ToArray()));                            
                        }
                    }
                    else
                    {
                        //数据接收返回为0,表示连接已经断开了,需要做重连
                        throw new SocketException(5);
                    }
                }
            }
            catch (SocketException ex)
            {
                //todo:
            }
            catch
            {
                //todo:
            }
        }

通过 _tcpClient.Available 来判断有多少字节可以读取,那么直接在缓冲区中读取相应的字节数。读取完成后,再将他们合并起来,给到业务层使用。

以上就是解决粘包,半包的基本方法,欢迎留言评论!


本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/C-Tcp%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E7%B2%98%E5%8C%85%E5%8D%8A%E5%8C%85%E9%97%AE%E9%A2%98.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系