Tcp通信中会出现粘包的情况,Tcp数据不是完全一收一发,而是会将接收到数据存在一个接收缓冲区,等到调用接收的把数据从缓冲区取出来
大部分时候,我们收发消息频率不高,看上去就是发一条,收一条,完整数据;理论上,接收一次,是会出现各种情况,
粘包的几种情况
- 接收不完整一条数据,多次接收
- 接收到2条(或者N条)连在一起的数据
- 接收到1条(或者N条)完整加一条不完整的数据
示例:
比如 modbustcp响应报文,我们查询三次,正常是收到三条下面这样报文
1
00 01 00 00 00 05 FF 01 02 0A 02
2
00 01 00 00 00 05 FF 01 02 0A 02
3
00 01 00 00 00 05 FF 01 02 0A 02
情况1:不完整报文
每条都分开几次收到
1
00 01 00 00 00 05
FF 01 02 0A 02
2
00 01 00 00
00 05 FF 01 02 0A 02
3
00 01 00 00 00 05
FF 01 02
0A 02
情况2:粘连
两条一起收到,第三条单独收到
1+2
00 01 00 00 00 05 FF 01 02 0A 02
00 01 00 00 00 05 FF 01 02 0A 02
3
00 01 00 00 00 05 FF 01 02 0A 02
三条一起收到
1+2+3
00 01 00 00 00 05 FF 01 02 0A 02
00 01 00 00 00 05 FF 01 02 0A 02
00 01 00 00 00 05 FF 01 02 0A 02
情况3:就是前两种结合,不完整粘连
第一条响应完整报文,跟第二条响应报文前半部分一起收到,剩下的和第三条一起收到
00 01 00 00 00 05 FF 01 02 0A 02
00 01 00 00 00 05
FF 01 02 0A 02
00 01 00 00 00 05 FF 01 02 0A 02
实例测试
这边演示 发送三次查询报文,然后再接收,就可以看到效果,三条报文是一起收到的,如果不处理,这里就只会处理一条报文,其他的就忽略了。
报文被拆分的情况主要是正在数据多的情况下,这边比较难演示
代码修改
因为我们这里只会发一次查询报文就立刻接收,所以不用考虑多条粘连的问题,只需要考虑接收不完整的事情,需要循环多次接收,直到等于我们目标长度
这里就会出现多种情况
如果一直收到长度为0就会一直死循环;需要异常退出
一直没收到数据,就会卡死在这里;需要超时处理
收到异常报文,比如modbustcp中正常报文需要11个字节,异常报文只有9个字节,这时收到9个字节只会就不会再收到数据了;需要判断异常报文
修改前
byte[] recvBytes = new byte[9 + len];
var recvLen = client.Receive(recvBytes);
if (recvLen == errLen)
{
Console.WriteLine("返回异常");
continue;
}
修改后
//正常长度
var normalLen = 9 + len;
//接收报文
byte[] recvBytes = new byte[normalLen];
//报文总长
int recevTotalLength = 0;
while (recevTotalLength < normalLen && recevTotalLength != errLen)
{
try
{
int needLength = normalLen - recevTotalLength;
var recvLen = client.Receive(
recvBytes,
recevTotalLength,
needLength,
SocketFlags.None
);
if (recvLen == 0)
{
throw new Exception("未接收到响应数据");
}
recevTotalLength += recvLen;
}
catch (Exception ex)
{
throw new Exception("接收响应数据异常!" + ex.Message);
}
}
if (recvLen == errLen)
{
Console.WriteLine("返回异常");
continue;
}
完整通信代码
/// <summary>
/// 启动主采集线程,循环采集
/// </summary>
public void DoMonitor()
{
Task.Run(() =>
{
//防止重复启动
if (IsMonitored)
return;
IsMonitored = true;
while (true)
{
if (!_tcpClient.Connected)
{
Connect();
}
else
{
try
{
var client = _tcpClient.Client;
foreach (RegisterPoint point in _registers)
{
SetSendBytes(point);
//发送读报文
client.Send(_readbytes);
int len = ReceviceDataLength(point.Length);
var errLen = 9;
var normalLen = 9 + len;
//接收报文
byte[] recvBytes = new byte[normalLen];
//报文总长
int recevTotalLength = 0;
while (recevTotalLength < normalLen && recevTotalLength != errLen)
{
try
{
int needLength = normalLen - recevTotalLength;
var recvLen = client.Receive(
recvBytes,
recevTotalLength,
needLength,
SocketFlags.None
);
if (recvLen == 0)
{
throw new Exception("未接收到响应数据");
}
recevTotalLength += recvLen;
}
catch (Exception ex)
{
throw new Exception("接收响应数据异常!" + ex.Message);
}
}
if (recvLen == errLen)
{
Console.WriteLine("返回异常");
continue;
}
//提取数据部分
byte[] dataBytes = new byte[len];
Array.Copy(recvBytes, 9, dataBytes, 0, len);
//数据处理
DealData(point, dataBytes);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Task.Delay(1000).Wait();
}
});
}
/// <summary>
/// 返回报文数据长度
/// </summary>
/// <param name="pointLength"></param>
/// <returns></returns>
private int ReceviceDataLength(int pointLength)
{
int len;
if (_readbytes[7] <= 2)
{
len = pointLength / 8 + 1;
}
else
{
len = pointLength * 2;
}
return len;
}