TCP接收Teltonika FMB920 GPS追踪器数据时重复接收问题排查
我用示例代码接收Teltonika FMB920 GPS追踪器的数据,按照Teltonika编解码器文档的TCP交互规则:
- 追踪器先发送IMEI,服务器回复1字节的0x01(接受)或0x00(拒绝)
- 之后追踪器发送GPS数据包,服务器必须回复已接收的数据包数量,否则追踪器会重复发送同一数据包直到收到回复
目前代码能正常接收并响应IMEI,但始终重复收到同一个GPS数据包。奇怪的是,在this.ResponseReverse(avlData.NumberOfData1, stream);这行打断点,取消断点继续运行后,追踪器就能正常发送新数据包。尝试用Thread.Sleep()模拟断点效果,但没有作用。
相关核心代码:
private void ClientHandler(object client) { var tcpClient = (TcpClient)client; //tcpClient.NoDelay = true; var stream = tcpClient.GetStream(); var imei = this.ReadImei(stream); var commArgs = this.OnDeviceCnnected(imei); if (commArgs.CommunicationAccepted) { this.Accept(stream); this.OnStatusChanged(imei, AvlTcpStatus.Accepted); } else { this.Reject(stream); this.OnStatusChanged(imei, AvlTcpStatus.Rejected); } while (true) { try { var memoryStream = this.ReadData(stream); if (memoryStream.Length == 0) { this.OnStatusChanged(imei, AvlTcpStatus.Terminated); break; } var packet = PacketParser.Parse<AvlTcpPacket>(memoryStream); if (memoryStream.Position != memoryStream.Length) throw new InvalidDataException("unfinished avl packet"); var avlData = packet.GetAvlDataArray(); this.OnDataFetched(imei, avlData); //Thread.Sleep(1000); this.ResponseReverse(avlData.NumberOfData1, stream); //Thread.Sleep(1000); } catch (Exception exception) { this.OnErrorOccured(imei, exception); } } }
更新:补充相关方法代码
private void Accept(NetworkStream stream) { // 1 as byte array response will determine if it would accept data from this module. this.Response(1, stream); } private void Reject(NetworkStream stream) { // 0 as byte array response will determine if it would reject data from this module. this.Response(0, stream); } private void Response(int count, NetworkStream stream) { var answer = BitConverter.GetBytes(count).ToArray(); stream.Write(answer, 0, answer.Length); } private void ResponseReverse(int count, NetworkStream stream) { var answer = BitConverter.GetBytes(count).Reverse().ToArray(); stream.Write(answer, 0, answer.Length); } private string ReadImei(NetworkStream stream) { var buffer = new byte[128]; //IMEI buffer var length = stream.Read(buffer, 0, buffer.Length); if (buffer[0] != 0 || buffer[1] != 0x0f) throw new InvalidDataException("Wrong IMEI"); var imei = Encoding.ASCII.GetString(buffer, 2, length - 2); return imei; } private MemoryStream ReadData(NetworkStream stream) { var buffer = new byte[4096]; var memoryStream = new MemoryStream(); while (true) { var length = stream.Read(buffer, 0, buffer.Length); if (length == 0) break; memoryStream.Write(buffer, 0, length); if (!stream.DataAvailable) break; } memoryStream.Position = 0; return memoryStream; }
核心原因
断点暂停时,系统会自动刷新TCP缓冲区,把回复数据发送给追踪器;但正常运行时,NetworkStream.Write()只是把数据写入本地缓冲区,TCP的Nagle算法会等待更多数据再一起发送,导致追踪器迟迟收不到回复,因此重复发包。另外,还要确认回复的字节序和长度是否严格符合Teltonika的要求。
具体修复步骤
强制刷新TCP缓冲区
在所有发送回复的方法中,写完数据后立即调用stream.Flush(),确保数据立刻发送:private void Response(int count, NetworkStream stream) { var answer = BitConverter.GetBytes(count).ToArray(); stream.Write(answer, 0, answer.Length); stream.Flush(); // 新增:强制刷新缓冲区 } private void ResponseReverse(int count, NetworkStream stream) { var answer = BitConverter.GetBytes(count).Reverse().ToArray(); stream.Write(answer, 0, answer.Length); stream.Flush(); // 新增:强制刷新缓冲区 }确保回复的字节序和长度正确
Teltonika要求回复是4字节大端序(Big-Endian)整数,表示已接收的AVL记录数。可以用更可靠的方式生成大端序数据,避免系统字节序影响:// 替换原有的ResponseReverse方法,显式生成4字节大端序数据 private void SendAvlResponse(int recordCount, NetworkStream stream) { byte[] response = new byte[4]; response[0] = (byte)(recordCount >> 24); response[1] = (byte)(recordCount >> 16); response[2] = (byte)(recordCount >> 8); response[3] = (byte)recordCount; stream.Write(response, 0, 4); stream.Flush(); }然后把
ClientHandler中的this.ResponseReverse(avlData.NumberOfData1, stream);替换为:this.SendAvlResponse(avlData.NumberOfData1, stream);可选:禁用Nagle算法
取消注释tcpClient.NoDelay = true;,禁用Nagle算法,减少小数据包的发送延迟:var tcpClient = (TcpClient)client; tcpClient.NoDelay = true; // 启用NoDelay,避免Nagle算法延迟
内容的提问来源于stack exchange,提问作者Pouya

