C# 使用TouchSocket实现Tcp协议通讯,并且解决分包、粘包的问题
我们知道如果Socket传输数据太频繁并且数据量级比较大,就很容易出现分包(一个包的内容分成了两份)、粘包(前一个包的内容分成了两份,其中一份连着下一个包的内容)的情况。
粘包的处理方式有很多种,常见的三种是:
- 每个包都在头部增加一个当前传输包的int4字节大小作为包头。每次接收到数据先读取的包头,确定这一包的实际长度n,当接收够n+4长度的数据就是一个完整的包,再重复读取下一包的包头。(相对其它方式,编码复杂)
- 使用固定分隔符对每包进行分割。(轮询查询每个分隔符的位置,并且有可能分隔符与实际数据出现相同的情况)
- 发送方和接收方规定固定大小的缓冲区,也就是发送和接收都使用固定大小的 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数据,所以不用额外考虑包大小的问题,组件里面自动会进行拆包传输处理。