首页 > 其他分享 >如何处理Tcp粘包半包问题

如何处理Tcp粘包半包问题

时间:2024-10-31 14:46:11浏览次数:3  
标签:buffer 数据 Tcp 粘包 TCP new 半包 byte data

专注于.NET技术开发的博主,关注我个人微信公众号查看更多:承哥技术交流小作坊。

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 来判断有多少字节可以读取,那么直接在缓冲区中读取相应的字节数。读取完成后,再将他们合并起来,给到业务层使用。

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

关注我:承哥技术交流小作坊

标签:buffer,数据,Tcp,粘包,TCP,new,半包,byte,data
From: https://blog.csdn.net/qq_22933729/article/details/143230655

相关文章

  • TCP和UDP
    TCP(传输控制协议)连接导向:在数据传输之前,TCP需要建立连接(如三次握手),确保双方可以通信。可靠性:TCP提供数据传输的可靠性,确保数据包按顺序到达,且没有丢失。丢失的数据包会被重传。流量控制和拥塞控制:TCP具有流量控制机制,防止发送方过快发送数据,导致接收方处理不过来。同时,它也会根......
  • python3 tcp_client
    tcp_client.py#-*-coding:utf-8-*-#tcp客户端,使用单例模式实现#create:2023-06-26importsocketimporttimeimporttracebackclassTCPConnection:__instance=None#存储单例对象的类属性def__new__(cls,host,port):"""实现......
  • JavaEE初阶---网络原理之TCP篇(二)
    文章目录1.断开连接--四次挥手1.1TCP状态1.2四次挥手的过程1.3time_wait等待1.4三次四次的总结2.前段时间总结3.滑动窗口---传输效率机制3.1原理分析3.2丢包的处理3.3快速重传4.流量控制---接收方安全机制4.1流量控制思路4.2剩余空间大小4.3探测包的机制5.拥塞控制--......
  • 【JavaEE初阶】深入理解TCP协议特性之延时应答,捎带应答,面向字节流以及异常处理
     前言......
  • 【重磅新品】芯驿电子 ALINX 推出全新 IP 核产品线,覆盖 TCP/UDP/NVMe AXI IP 核
    在创新加速的浪潮中,为更好地响应客户群需求,芯驿电子ALINX推出全新IP核产品线,致力于为高性能数据传输和复杂计算需求提供高带宽、低延迟的解决方案。发布的第一批IP核包括10GBe/40GBeUDP协议栈IP核、10GbETCP/IP协议栈IP核和NVMeAXIIP核。 ALINX发布的10Gb......
  • TCP攻击实验
       作业题目本实验的学习目标是让学生获得有关漏洞以及针对这些漏洞的攻击的第一手经验。聪明人从错误中学习。在安全教育中,我们研究导致软件漏洞的错误。研究过去的错误不仅有助于学生理解为什么系统容易受到攻击,为什么“看似良性”的错误会变成灾难,以及为什么需要许多安......
  • 【软考】计算机网络 - 网络协议TCP/IP协议的七层网络协议模型和四层网络协议模型,网络
    一、TCP/IP协议的七层网络协议详解计算机网络构建于七层模型之上图注:中继器:信号会随着距离的增加而逐渐衰减,而中继器则接受一端的信息再将其原封不动的发给另一端,起到延长传输距离的作用,而集线器就是多端口的中继器;网桥是用于连接两个同类型网络的设备,交换机则是多端......
  • TCP与UDP的区别和应用场景
    TCP和UDP的区别包括:1.连接方式不同;2.传输可靠性不同;3.数据顺序性不同;4.速度和延迟不同;5.头部大小不同;6.应用场景不同。TCP是一种面向连接、可靠的传输协议,主要用于需要数据完整性和顺序的应用,如Web浏览和电子邮件。而UDP是一种无连接、速度更快但可能丢失数据的协议,常用于流媒体......
  • Python TCP通讯教程
    文章目录一、TCP协议简介二、PythonTCP通讯基础三、TCP服务器端的实现四、TCP客户端的实现五、TCP通讯的扩展功能六、示例代码七、注意事项一、TCP协议简介TCP(TransmissionControlProtocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。T......
  • 鸿蒙网络编程系列36-固定包头可变包体解决TCP粘包问题
    1.TCP数据传输粘包简介在本系列的第6篇文章《鸿蒙网络编程系列6-TCP数据粘包表现及原因分析》中,我们演示了TCP数据粘包的表现,如图所示:随后解释了粘包背后的可能原因,并给出了解决TCP传输粘包问题的两种思路,第一种是指定数据包结束标志,在本系列第35篇《鸿蒙网络编程系列35......