C# 优雅的解决TCP Socket粘包、分包问题
一、概述
TCP是流性质数据的发送与接收,就像是一条河流,我们不能依靠它本身来区分哪部分水来自哪条山沟。但是往往,数据的解析就必须清楚的知道哪里是解析界限,不然发送的数据则像臭婆娘的烂线裤,难以把握。
二、探索问题
2.1 源码位置
2.3 说明文档
三、解决思路
实际上,解决粘包、分包问题的思路有很多,例如:
- Http:使用“\r\n”分割header,然后读取contentLength的Body。
- WebSocket:采用特殊数据帧,用可变头表示载荷数据。
- 等等。
而当我们自己搭建TCP服务器或客户端以后,如何优雅的解决这个问题呢?
常规思路其实也就三种:
- 固定包头:以前几个固定字节作为包头,然后标识后续字节数量。
- 固定长度:不管发送多少数据,客户端和服务器以固定长度接收。
- 终止因子分割:约定数据以某种特定组合结束,可以理解为汉语的“句号”。
四、实现方式
要实现上述的算法,我们需要引入TouchSocket
的Nuget包。然后服务器和客户端选择对应的适配器即可。
例如:
【服务器】
使用固定包头数据处理适配器(FixedHeaderPackageAdapter)
,然后设置包头由int标识,也就是4+n的数据格式。
TcpService service = new TcpService();
service.Connecting = (client, e) =>
{
client.SetDataHandlingAdapter(new FixedHeaderPackageAdapter() { FixedHeaderType= FixedHeaderType.Int });
};
service.Received = (client, byteBlock, requestInfo) =>
{
//从客户端收到信息
string mes = byteBlock.ToString();
Console.WriteLine($"已从{client.IP}:{client.Port}接收到信息:{mes}");//Name即IP+Port
client.Send(byteBlock);
};
var config = new RRQMConfig();
config.SetListenIPHosts(new IPHost[] { new IPHost("127.0.0.1:7789"), new IPHost(7790) });//同时监听两个地址
service.Setup(config);
service.Start();
【客户端】
使用同样的适配器。
TcpClient tcpClient = new TcpClient();
tcpClient.Connecting = (client, e) =>
{
client.SetDataHandlingAdapter(new FixedHeaderPackageAdapter() { FixedHeaderType= FixedHeaderType.Int });
};
tcpClient.Received = (client, byteBlock, obj) =>
{
//从服务器收到信息
string mes = Encoding.UTF8.GetString(byteBlock.Buffer, 0, byteBlock.Len);
Console.WriteLine($"已从服务器接收到信息:{mes}");
};
tcpClient.Setup("127.0.0.1:7789");
tcpClient.Connect();
此时,服务器和客户端就已经拥有了处理粘包
、分包
的能力了。
除此之外,可选的适配器还有:
正常数据处理适配器(NormalDataHandlingAdapter)
固定包头数据处理适配器(FixedHeaderPackageAdapter)
固定长度数据处理适配器(FixedSizePackageAdapter)
终止因子分割数据处理适配器(TerminatorPackageAdapter)
五、普通Socket如何实现
上述方法可以很简单的处理粘包
、分包
,但是有时候可能大家不太想用TouchSocket组件,只想用原生Socket
实现。那怎么实现呢?
实际上也比较简单。但是依然要引用TouchSocket库,然后独立使用
适配器即可。
[Fact]
public void AdapterCallbackShouldBeOk()
{
FixedHeaderPackageAdapter adapter = new FixedHeaderPackageAdapter();
bool sendCallBack = false;
bool receivedCallBack = false;
byte[] sentData=null;
adapter.SendCallBack = (buffer,offset,length,async) =>
{
//此处会回调发送的最终调用。例如:此处使用固定包头,则发送的数据为4+n的封装。
sentData = new byte[length];
Array.Copy(buffer,offset,sentData,0,length);
if (length==4+4)
{
sendCallBack = true;
}
};
adapter.ReceivedCalloTouc= (byteBlock,requestInfo) =>
{
//此处会回调接收的最终触发,例如:此处使用的固定包头,会解析4+n的数据为n。
if (byteBlock.Len==4)
{
receivedCallBack = true;
}
};
byte[] data = Encoding.UTF8.GetBytes("RRQM");
adapter.SendInput(data,0,data.Length,false);//模拟输入,会在SendCallBack中输出最终要发送的数据。
using (ByteBlock block=new ByteBlock())
{
block.Write(sentData);
block.Pos = 0;
adapter.ReceivedInput(block);//模拟输出,会在ReceivedCallBack中输出最终收到的实际数据。
}
Assert.True(sendCallBack);
Assert.True(receivedCallBack);
}