我们知道如果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数据,所以不用额外考虑包大小的问题,组件里面自动会进行拆包传输处理。