C# 网络编程--关于Socket编程TCP协议中封包、拆包问题

在使用 Socket 编程,进行TCP协议网络通信时,经常会遇到"粘包"(也称为"封包、拆包")的问题。粘包是指发送方发送的多个数据包被接收方合并成一个数据包,或者一个数据包被拆分成多个数据包接收。这通常是由于 TCP协议 的滑动窗口机制和 UDP协议 的数据报特性导致的。

1.封包、拆包的原因

  • TCP 滑动窗口机制:
    • TCP 是面向字节流的协议,它会将数据看作一个连续的字节流,而不是一个个独立的数据包。
    • 当发送方发送多个小数据包时,TCP 可能会将这些数据包合并成一个大的数据包发送出去。
    • 接收方接收到的数据可能是多个数据包的组合,从而导致粘包。
  • UDP 数据报特性:
    • UDP 是面向数据报的协议,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
    • 但是,如果发送方在短时间内发送多个数据报,接收方可能会一次性接收到多个数据报,导致重复接收。
    • 同样,如果数据报的大小超过了接收缓冲区的大小,也可能被拆分成多个数据报接收。

2.解决粘包的方法

1. 固定长度的数据包:

• 每个数据包都有固定的长度,接收方按照固定长度读取数据。

• 例如:假设每个数据包长度为 1024 字节。

客户端代码:

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class FixedLengthTcpClient
{
    private static Socket _clientSocket;
    private const int PacketSize = 1024; // 每个数据包的固定长度

    static async Task Main(string[] args)
    {
        string serverIp = "127.0.0.1";
        int port = 12345;
        IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse(serverIp), port);

        _clientSocket = new Socket(remoteEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            await _clientSocket.ConnectAsync(remoteEP);
            Console.WriteLine("已连接到服务器: {0}", _clientSocket.RemoteEndPoint.ToString());

            // 启动一个任务来持续接收数据
            _ = ReceiveDataAsync();

            // 示例:发送一些数据到服务器
            string message = "这是一个测试消息";
            byte[] msg = Encoding.ASCII.GetBytes(message.PadRight(PacketSize)); // 填充到固定长度
            _clientSocket.Send(msg);

            // 保持客户端运行一段时间
            await Task.Delay(60000); // 保持运行60秒

            _clientSocket.Shutdown(SocketShutdown.Both);
            _clientSocket.Close();
        }
        catch (Exception e)
        {
            Console.WriteLine("发生错误: " + e.ToString());
        }
    }
	//持续异步来接收数据
    private static async Task ReceiveDataAsync()
    {
        byte[] buffer = new byte[PacketSize];

        while (true)
        {
            try
            {
                int bytesReceived = await _clientSocket.ReceiveAsync(new ArraySegment<byte>(buffer), SocketFlags.None);

                if (bytesReceived == 0)
                {
                    Console.WriteLine("服务器关闭了连接");
                    break;
                }

                string data = Encoding.ASCII.GetString(buffer, 0, bytesReceived).TrimEnd(); // 去除填充
                Console.WriteLine("收到服务器消息: {0}", data);
            }
            catch (Exception e)
            {
                Console.WriteLine("接收数据时发生错误: " + e.ToString());
                break;
            }
        }
    }
}

服务端代码:

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class FixedLengthTcpServer
{
    static async Task Main(string[] args)
    {
        int port = 12345;
        IPAddress ipAddress = IPAddress.Any;
        IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);

        Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            listener.Bind(localEndPoint);
            listener.Listen(10);//允许最多10个客户端排队等待连接

            Console.WriteLine("等待客户端连接...");

            while (true)
            {
                Socket handler = await listener.AcceptAsync();

                Task.Run(() => HandleClient(handler));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("发生错误: " + e.ToString());
        }
    }

    static void HandleClient(Socket handler)
    {
        try
        {
            byte[] buffer = new byte[1024];
            // 接收固定长度的数据包
            int bytesReceived = handler.Receive(buffer);//同步接收(也可以异步接受,具体分析)
            string data = Encoding.ASCII.GetString(buffer).TrimEnd(); // 去除填充的空格
            Console.WriteLine("收到客户端消息: {0}", data);

            // 发送响应给客户端
            byte[] msg = Encoding.ASCII.GetBytes("消息已收到".PadRight(1024)); // 固定长度为1024字节
            handler.Send(msg);

            // 关闭连接
            handler.Shutdown(SocketShutdown.Both);
            handler.Close();
        }
        catch (Exception e)
        {
            Console.WriteLine("处理客户端时发生错误: " + e.ToString());
        }
    }
}

注意:

  1. 数据填充:
    • 填充数据:确保发送的数据长度符合固定长度要求,不足的部分可以通过填充(如空格、零字节等)补齐。
    • 去除填充:接收端需要去除填充部分以获取实际数据。
  2. 缓冲区大小:
    • 合适大小:选择合适的固定长度,过大可能导致内存浪费,过小可能导致频繁接收。
    • 内存占用方面:缓冲区越大,占用的内存空间就越多。如果在一个系统中有大量的网络连接,每个连接都使用较大的缓冲区,可能会导致系统内存资源的过度占用。例如,一个服务器程序要同时处理数千个客户端连接,若每个连接的接收缓冲区设置为 1MB,那么仅接收缓冲区就会占用数 GB 的内存,这可能使系统内存紧张,甚至导致内存不足的错误。
    • 性能方面:较大的缓冲区可能会增加数据延迟。因为在接收数据时,系统需要等待缓冲区填满或者等待特定的接收条件满足才会进行后续处理。如果缓冲区过大,数据可能会在缓冲区中等待较长时间才能被处理,这对于对实时性要求较高的应用场景(如实时视频流传输、在线游戏等)是不利的。

2. 分隔符:

• 在每个数据包之间添加一个特殊的分隔符。(如:\r\n FTP协议、特殊符号等等,双方协议好的分隔符)

• 接收方读取数据时,根据分隔符来区分不同的数据包。

客户端代码:

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class DelimiterTcpClient
{
    private static Socket _clientSocket;
    private static readonly byte[] _delimiter = Encoding.ASCII.GetBytes("\n"); // 使用换行符作为分隔符
    private static readonly byte[] _buffer = new byte[1024];
    private static byte[] _receivedData = new byte[0];

    static async Task Main(string[] args)
    {
        string serverIp = "127.0.0.1";
        int port = 12345;
        IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse(serverIp), port);

        _clientSocket = new Socket(remoteEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            await _clientSocket.ConnectAsync(remoteEP);
            Console.WriteLine("已连接到服务器: {0}", _clientSocket.RemoteEndPoint.ToString());

            // 启动一个任务来持续接收数据
            _ = ReceiveDataAsync();

            // 示例:发送一些数据到服务器
            string message = "这是一个测试消息\n"; // 每个消息后面加上分隔符
            byte[] msg = Encoding.ASCII.GetBytes(message);
            _clientSocket.Send(msg);

            // 保持客户端运行一段时间
            await Task.Delay(60000); // 保持运行60秒

            _clientSocket.Shutdown(SocketShutdown.Both);
            _clientSocket.Close();
        }
        catch (Exception e)
        {
            Console.WriteLine("发生错误: " + e.ToString());
        }
    }
	//持续异步来接收数据
    private static async Task ReceiveDataAsync()
    {
        while (true)
        {
            try
            {
                int bytesReceived = await _clientSocket.ReceiveAsync(new ArraySegment<byte>(_buffer), SocketFlags.None);

                if (bytesReceived == 0)
                {
                    Console.WriteLine("服务器关闭了连接");
                    break;
                }

                byte[] newData = new byte[_receivedData.Length + bytesReceived];
                Buffer.BlockCopy(_receivedData, 0, newData, 0, _receivedData.Length);
                Buffer.BlockCopy(_buffer, 0, newData, _receivedData.Length, bytesReceived);
                _receivedData = newData;

                int delimiterIndex;
                while ((delimiterIndex = FindDelimiter(_receivedData)) != -1)
                {
                    string data = Encoding.ASCII.GetString(_receivedData, 0, delimiterIndex);
                    Console.WriteLine("收到服务器消息: {0}", data);

                    byte[] remainingData = new byte[_receivedData.Length - delimiterIndex - _delimiter.Length];
                    Buffer.BlockCopy(_receivedData, delimiterIndex + _delimiter.Length, remainingData, 0, remainingData.Length);
                    _receivedData = remainingData;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("接收数据时发生错误: " + e.ToString());
                break;
            }
        }
    }

    private static int FindDelimiter(byte[] data)
    {
        for (int i = 0; i <= data.Length - _delimiter.Length; i++)
        {
            bool match = true;
            for (int j = 0; j < _delimiter.Length; j++)
            {
                if (data[i + j] != _delimiter[j])
                {
                    match = false;
                    break;
                }
            }
            if (match)
            {
                return i;
            }
        }
        return -1;
    }
}

服务端代码:

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class DelimiterTcpServer
{
    static async Task Main(string[] args)
    {
        int port = 12345;
        IPAddress ipAddress = IPAddress.Any;
        IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);

        Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            listener.Bind(localEndPoint);
            listener.Listen(10);//允许最多10个客户端排队等待连接

            Console.WriteLine("等待客户端连接...");

            while (true)
            {
                Socket handler = await listener.AcceptAsync();

                Task.Run(() => HandleClient(handler));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("发生错误: " + e.ToString());
        }
    }

    static void HandleClient(Socket handler)
    {
        try
        {
            byte[] buffer = new byte[1024];
            // 接收数据
            int bytesReceived = handler.Receive(buffer);
            string data = Encoding.ASCII.GetString(buffer);
            string[] messages = data.Split('<'); // 根据分隔符分割消息

            foreach (string message in messages)
            {
                if (!string.IsNullOrEmpty(message))
                {
                    Console.WriteLine("收到客户端消息: {0}", message);

                    // 发送响应给客户端
                    byte[] msg = Encoding.ASCII.GetBytes("消息已收到" + "<EOF>"); // 添加分隔符
                    handler.Send(msg);
                }
            }

            // 关闭连接
            handler.Shutdown(SocketShutdown.Both);
            handler.Close();
        }
        catch (Exception e)
        {
            Console.WriteLine("处理客户端时发生错误: " + e.ToString());
        }
    }
}

注意:

  1. 分隔符选择:
    • 唯一性:选择一个在数据中不会出现的分隔符,避免误判。
    • 转义机制:如果数据中可能包含分隔符,需要实现转义机制。
  2. 数据解析:
    • 查找分隔符:高效地查找分隔符位置,避免不必要的性能开销。
    • 处理分段数据:处理分段接收的情况,确保能够正确拼接完整的数据包。

3. 头部包含长度信息:

• 每个数据包的头部包含数据包的长度信息。

• 接收方先读取头部获取数据包长度,然后根据长度读取完整的数据包。

例如:消息头+消息体

消息头(20字节):(int)消息校验码4字节 + (int)消息体长度4字节 + (long)身份ID 8字节 + (int)加密方式4字节

消息体(消息体长度4字节界定消息体长度):(int)消息1长度4字节 + (string)消息1 + (int)消息2长度4字节 + (string)消息2 + (int)消息3长度4字节 + (string)消息3 + ...

每个消息前面有一个int(4字节)变量记录该消息字节长度

客户端端代码:

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class LengthPrefixTcpClient
{
    private static Socket _clientSocket;
    private static readonly byte[] _buffer = new byte[1024];

    static async Task Main(string[] args)
    {
        string serverIp = "127.0.0.1";
        int port = 12345;
        IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse(serverIp), port);

        _clientSocket = new Socket(remoteEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            await _clientSocket.ConnectAsync(remoteEP);
            Console.WriteLine("已连接到服务器: {0}", _clientSocket.RemoteEndPoint.ToString());

            // 启动一个任务来持续接收数据
            _ = ReceiveDataAsync();

            // 示例:发送一些数据到服务器
            string message = "这是一个测试消息";
            byte[] msg = Encoding.ASCII.GetBytes(message);
            byte[] lengthPrefix = BitConverter.GetBytes(msg.Length); // 获取消息长度的字节数组

            // 发送长度前缀
            _clientSocket.Send(lengthPrefix);
            // 发送消息
            _clientSocket.Send(msg);

            // 保持客户端运行一段时间
            await Task.Delay(60000); // 保持运行60秒

            _clientSocket.Shutdown(SocketShutdown.Both);
            _clientSocket.Close();
        }
        catch (Exception e)
        {
            Console.WriteLine("发生错误: " + e.ToString());
        }
    }
	//持续异步来接收数据
    private static async Task ReceiveDataAsync()
    {
        while (true)
        {
            try
            {
                // 接收长度前缀
                byte[] lengthBuffer = new byte[4];
                int totalBytesReceived = 0;
                while (totalBytesReceived < lengthBuffer.Length)
                {
                    int bytes = await _clientSocket.ReceiveAsync(new ArraySegment<byte>(lengthBuffer, totalBytesReceived, lengthBuffer.Length - totalBytesReceived), SocketFlags.None);
                    if (bytes == 0)
                    {
                        Console.WriteLine("服务器关闭了连接");
                        return;
                    }
                    totalBytesReceived += bytes;
                }

                int messageLength = BitConverter.ToInt32(lengthBuffer, 0);

                // 接收消息
                byte[] buffer = new byte[messageLength];
                totalBytesReceived = 0;
                while (totalBytesReceived < messageLength)
                {
                    int bytes = await _clientSocket.ReceiveAsync(new ArraySegment<byte>(buffer, totalBytesReceived, messageLength - totalBytesReceived), SocketFlags.None);
                    if (bytes == 0)
                    {
                        Console.WriteLine("服务器关闭了连接");
                        return;
                    }
                    totalBytesReceived += bytes;
                }

                string data = Encoding.ASCII.GetString(buffer, 0, messageLength);
                Console.WriteLine("收到服务器消息: {0}", data);
            }
            catch (Exception e)
            {
                Console.WriteLine("接收数据时发生错误: " + e.ToString());
                break;
            }
        }
    }
}

服务端代码:

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

class LengthPrefixTcpServer
{
    static async Task Main(string[] args)
    {
        int port = 12345;
        IPAddress ipAddress = IPAddress.Any;
        IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);

        Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

        try
        {
            listener.Bind(localEndPoint);
            listener.Listen(10);//允许最多10个客户端排队等待连接

            Console.WriteLine("等待客户端连接...");

            while (true)
            {
                Socket handler = await listener.AcceptAsync();//异步等待客户端连接,当有客户端连接时,返回一个新的Socket(handler )用于与客户端通信

                Task.Run(() => HandleClient(handler));
            }
        }
        catch (Exception e)
        {
            Console.WriteLine("发生错误: " + e.ToString());
        }
    }

    static void HandleClient(Socket handler)
    {
        try
        {
            byte[] lengthBuffer = new byte[4];
            // 接收长度前缀
            int bytesReceived = handler.Receive(lengthBuffer);
            int messageLength = BitConverter.ToInt32(lengthBuffer, 0);

            byte[] buffer = new byte[messageLength];
            // 接收消息
            bytesReceived = handler.Receive(buffer);
            string data = Encoding.ASCII.GetString(buffer, 0, bytesReceived);
            Console.WriteLine("收到客户端消息: {0}", data);

            // 发送响应给客户端
            string response = "消息已收到";
            byte[] responseMsg = Encoding.ASCII.GetBytes(response);
            byte[] responseLengthPrefix = BitConverter.GetBytes(responseMsg.Length); // 获取响应长度的字节数组

            // 发送长度前缀
            handler.Send(responseLengthPrefix);
            // 发送响应消息
            handler.Send(responseMsg);

            // 关闭连接
            handler.Shutdown(SocketShutdown.Both);
            handler.Close();
        }
        catch (Exception e)
        {
            Console.WriteLine("处理客户端时发生错误: " + e.ToString());
        }
    }
}

关于自定义头部长度方式,可以看看另一个大神博客,里面讲的更加详细:https://blog.csdn.net/ba_wang_mao/article/details/107759296

注意:

  1. 长度字段大小:
    • 合适大小:选择合适的长度字段大小(如 2 字节、4 字节),确保能够表示最大数据包长度。
    • 字节序:确保发送和接收端使用相同的字节序(如大端序或小端序)。
  2. 数据完整性:
    • 完整接收:确保接收完整的长度字段和数据包内容,避免数据不完整导致解析错误。
    • 错误处理:处理长度字段无效的情况,避免程序崩溃。

3.优缺点

1. 固定长度的数据包:

• 优点:实现简单,不需要额外的解析逻辑。

• 缺点:不适合数据包长度变化较大的情况,可能导致浪费带宽或无法处理大消息。

2. 分隔符:

• 优点:灵活,适用于不同长度的数据包。

• 缺点:需要额外的逻辑来查找分隔符,处理复杂度较高。

3. 头部包含长度信息:

• 优点:灵活且高效,适用于不同长度的数据包,不需要额外的分隔符。

• 缺点:需要额外的字节来存储长度信息,增加了少量的开销。

4.总结

固定长度的数据包 : 适用于数据包长度固定且较小的情况。

分隔符 : 适用于数据包长度不固定但可以确定分隔符的情况。

头部包含长度信息: 适用于数据包长度不固定且没有明显分隔符的情况。

相关推荐
hardWork_yulu2 小时前
Android RTMP直播练习实践
网络·安卓
qq_243050793 小时前
irpas:互联网路由协议攻击套件!全参数详细教程!Kali Linux入门教程!黑客渗透测试!
linux·网络·web安全·网络安全·黑客·渗透测试·系统安全
军训猫猫头4 小时前
52.this.DataContext = new UserViewModel(); C#例子 WPF例子
开发语言·c#·wpf
大丈夫立于天地间5 小时前
机遇、挑战与融合创新之路
网络
青旋.5 小时前
数据链路层——以太网协议
网络·网络协议·tcp/ip
月光技术杂谈6 小时前
OSI&5G&WIFI&自组网协议层次对比
tcp/ip·5g·wifi·osi·自组网·七层模式·协议架构
东锋1.37 小时前
计算机网络中常用的端口号以及对应的应用程序
网络
IpdataCloud7 小时前
如何提升IP地址查询数据服务的安全?
网络·tcp/ip·安全
互联网资讯7 小时前
详解共享WiFi小程序怎么弄!
大数据·运维·网络·人工智能·小程序·生活
可涵不会debug8 小时前
【C++】在线五子棋对战项目网页版
linux·服务器·网络·c++·git