C# 使用TouchSocket实现Tcp协议通讯,并且解决分包、粘包的问题

我们知道如果Socket传输数据太频繁并且数据量级比较大,就很容易出现分包(一个包的内容分成了两份)、粘包(前一个包的内容分成了两份,其中一份连着下一个包的内容)的情况。

粘包的处理方式有很多种,常见的三种是:

  1. 每个包都在头部增加一个当前传输包的int4字节大小作为包头。每次接收到数据先读取的包头,确定这一包的实际长度n,当接收够n+4长度的数据就是一个完整的包,再重复读取下一包的包头。(相对其它方式,编码复杂
  2. 使用固定分隔符对每包进行分割。(轮询查询每个分隔符的位置,并且有可能分隔符与实际数据出现相同的情况
  3. 发送方和接收方规定固定大小的缓冲区,也就是发送和接收都使用固定大小的 byte[] 数组长度,当字符长度不够时使用空字符弥补。(字符长度不够增加空字符使得传输数据增长

推荐使用第一种方式。但是第一种方式编码相对复杂,所以下面结合第三方组件简单快速的实现第一种方式。

下面演示使用TouchSocket组件进行Socket传输。

客户端:

           TouchSocket.Sockets.TcpClient tcpClient = new TouchSocket.Sockets.TcpClient();

            tcpClient.Connecting = (client, e) =>
            {
                // 这里是指增加Int大小的包头
                client.SetDataHandlingAdapter(new FixedHeaderPackageAdapter() { FixedHeaderType = FixedHeaderType.Int });
            };

            tcpClient.Received = (client, byteBlock, obj) =>
            {
                // 前4字节为包头,包含了包大小信息
                var data = new byte[byteBlock.Buffer.Length - 4];
                Array.Copy(byteBlock.Buffer, 4, data, 0, data.Length);

                //从服务器收到信息
                string mes = Encoding.UTF8.GetString(data, 0, data.Length);
                var text = $"已从服务器接收到信息:{mes}\r\n";
                Console.WriteLine(text);
                Application.Current.Dispatcher.Invoke(() =>
                {
                    textBlock.Text += text;
                });
            };
            tcpClient.Setup("127.0.0.1:21345");
            tcpClient.Connect();

            var str = "物服务器服务器服务器发起非亲非故";
            var bytes = Encoding.UTF8.GetBytes(str);
            var addLen = AddLen(bytes);
            tcpClient.Send(addLen);
            tcpClient.Close();
        /// <summary>
        /// 增加包头
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        internal byte[] AddLen(byte[] bytes)
        {
            int dataLength = bytes.Length;

            // 不要=先将int转字符串,再通过Encoding.UTF8.GetBytes转byte[],那样子byte[]的长度大小不固定,而BitConverter.GetBytes(int)固定为4个字节,BitConverter.GetBytes(long)固定为8个字节
            var dataLengthBytes = BitConverter.GetBytes(dataLength);

            byte[] sendBuffer = new byte[dataLengthBytes.Length + bytes.Length]; // 为数据包申请缓存,包括4个字节的分隔符和实际数据

            Buffer.BlockCopy(dataLengthBytes, 0, sendBuffer, 0, dataLengthBytes.Length); // 将长度信息写入数据包缓存的前面4个字节
            Buffer.BlockCopy(bytes, 0, sendBuffer, dataLengthBytes.Length, bytes.Length); // 将实际数据写入数据包缓存的后面

            return sendBuffer;
        }

服务端:

           TcpService service = new TcpService();
            service.Connecting = (client, ex) =>
            {
                client.SetDataHandlingAdapter(new FixedHeaderPackageAdapter() { FixedHeaderType = FixedHeaderType.Int });
            };

            service.Connected += (client, args) =>
            {
                _socketClient = client;
            };

            service.Received = (client, byteBlock, requestInfo) =>
            {
                var array = byteBlock.ToArray();

                // 前4字节为包头,包含了包大小信息
                var data = new byte[byteBlock.Buffer.Length - 4];
                Array.Copy(byteBlock.Buffer, 4, data, 0, data.Length);

                var text = $"已从{client.IP}:{client.Port}接收到信息:{Encoding.UTF8.GetString(data)}\r\n";
                Console.WriteLine(text);//Name即IP+Port
                Application.Current.Dispatcher.Invoke(() =>
                {
                    textBlock.Text += text;
                });

                var sendBuffer = AddLen(data);

                for (int i = 0; i < 100; i++)
                {
                    _socketClient.Send(sendBuffer); // 发送整个数据包
                    Thread.Sleep(1000);
                }
            };

            var config = new TouchSocketConfig();
            config.SetListenIPHosts(new IPHost[] { new IPHost("127.0.0.1:21345"), new IPHost(7790) });//同时监听两个地址
            service.Setup(config);
            service.Start();

其中增加Int包头的设置必须在Connecting回调中。

注意包的大小为int类型,不要先把int类型转成字符串再转byte[],那样子会导致包头不是固定的4字节(因为int类型本身只占4字节),所以应该使用BitConverter.GetBytes(dataLength)进行转换。

由于增加了包头,所以接收到的数据都必须先把包头前的4字节去掉。

【FixedHeaderPackageAdapter包模式】

该适配器主要解决TCP粘分包问题,数据格式采用简单而高效的“包头+数据体”的模式,其中包头支持:

  • Byte模式(1+n),一次性最大接收255字节的数据。
  • Ushort模式(2+n),一次最大接收65535字节。(默认)
  • Int模式(4+n),一次最大接收2G数据。

由于Int模式一次最大接收2G数据,所以不用额外考虑包大小的问题,组件里面自动会进行拆包传输处理。

 

posted @ 2023-08-09 11:44  log9527  阅读(4111)  评论(0编辑  收藏  举报