TCP 拆包现象解决方案(一)
比如我的原报文是:02-11-30-30-30-33-7C-33-32-30-37-34-39-39-35-38-33-30-38-31-37-31-7C-30-30-30-30-30-30-30-30-30-30-30-30-30-7C-30-30-30-30-30-30-30-30-30-30-30-30-30-7C-44-3A-5C-73-6F-72-74-69-6E-67-53-79-73-49-6D-61-67-65-5C-32-30-32-35-31-30-31-31-5C-33-32-30-37-34-39-39-35-38-33-30-38-31-37-31-5F-30-30-30-30-33-5F-32-30-32-35-31-30-31-31-30-36-33-35-33-38-31-34-33-5F-4F-72-69-67-69-6E-61-6C-2E-6A-70-67-03。
但是现在我接收到的报文是:
级别:INFO [SingleThreadEventExecutor worker] 时间:2025-10-11 06:35:38,143 信息:收到大华LP软件消息:02-11-30-30-30-33-7C-33-32-30-37-34-39-39-35-38-33-30-38-31-37-31-7C-30-30-30-30-30-30-30-30-30-30-30-30-30-7C-30-30-30-30-30-30-30-30-30-30-30-30-30-7C-44-3A-5C-73-6F-72-74-69-6E-67-53-79-73-49-6D-61-67-65-5C-32-30-32-35-31-30-31-31-5C-33-32-30-37-34-39-39-35-38-33-30-38-31-37-31-5F-30-30-30-30-33-5F-32-30-32-35-31-30-31-31-30-36-33-35-33-38-31-34-33-5F-4F-72-69-67-69-6E-61-6C-2E 位置:DHCameraScanner.DHCameraServicerHandler.ChannelRead(D:\JiPei440\JiPei440\DHCameraScanner\DHCameraScanner.cs:223) 级别:INFO [SingleThreadEventExecutor worker] 时间:2025-10-11 06:35:38,144 信息:收到大华LP软件消息:6A-70-67-03。
第一个报文 从
02
开始,但接收时被拆分成两次第二次接收 的内容
6A-70-67-03
实际上是第一个报文的剩余部分 + 结束标记
这就是典型的TCP拆包现象。
解决方法如下:
1. 报文解析器类
// DhPacketParser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;public class DhPacketParser
{private List<byte> _buffer = new List<byte>();private readonly byte _startMarker = 0x02; // 起始标记private readonly byte _endMarker = 0x03; // 结束标记public void AppendData(byte[] data){_buffer.AddRange(data);Console.WriteLine($"[解析器] 追加数据: {BitConverter.ToString(data)}");Console.WriteLine($"[解析器] 当前缓冲区大小: {_buffer.Count} 字节");}public List<byte[]> GetCompletePackets(){var completePackets = new List<byte[]>();while (true){// 查找起始位置int startIndex = _buffer.IndexOf(_startMarker);if (startIndex == -1) {Console.WriteLine($"[解析器] 未找到起始标记 0x02");break;}Console.WriteLine($"[解析器] 找到起始标记 0x02,位置: {startIndex}");// 查找结束位置int endIndex = _buffer.IndexOf(_endMarker, startIndex + 1);if (endIndex == -1){Console.WriteLine($"[解析器] 未找到结束标记 0x03,等待更多数据...");break;}Console.WriteLine($"[解析器] 找到结束标记 0x03,位置: {endIndex}");// 提取完整报文int packetLength = endIndex - startIndex + 1;byte[] packet = _buffer.GetRange(startIndex, packetLength).ToArray();Console.WriteLine($"[解析器] 提取完整报文,长度: {packetLength}");completePackets.Add(packet);// 移除已处理数据_buffer.RemoveRange(0, endIndex + 1);Console.WriteLine($"[解析器] 移除已处理数据,缓冲区剩余: {_buffer.Count} 字节");}return completePackets;}public byte[] GetBufferSnapshot(){return _buffer.ToArray();}public void Clear(){Console.WriteLine($"[解析器] 清空缓冲区,原大小: {_buffer.Count} 字节");_buffer.Clear();}
}// 扩展方法
public static class ListExtensions
{public static int IndexOf<T>(this List<T> list, T item){for (int i = 0; i < list.Count; i++){if (EqualityComparer<T>.Default.Equals(list[i], item))return i;}return -1;}public static int IndexOf<T>(this List<T> list, T item, int startIndex){for (int i = startIndex; i < list.Count; i++){if (EqualityComparer<T>.Default.Equals(list[i], item))return i;}return -1;}public static List<T> GetRange<T>(this List<T> list, int index, int count){return list.Skip(index).Take(count).ToList();}
}
2. 日志记录器
// PacketLogger.cs
using System;
using System.IO;public static class PacketLogger
{private static readonly string LogFilePath = "tcp_packet_debug.log";static PacketLogger(){// 清空或创建日志文件File.WriteAllText(LogFilePath, $"=== TCP 拆包演示日志 {DateTime.Now:yyyy-MM-dd HH:mm:ss} ===\n");}public static void LogInfo(string message){string logMessage = $"[INFO] {DateTime.Now:HH:mm:ss.fff} {message}";Console.WriteLine(logMessage);File.AppendAllText(LogFilePath, logMessage + "\n");}public static void LogWarning(string message){string logMessage = $"[WARN] {DateTime.Now:HH:mm:ss.fff} {message}";Console.WriteLine(logMessage);File.AppendAllText(LogFilePath, logMessage + "\n");}public static void LogError(string message){string logMessage = $"[ERROR] {DateTime.Now:HH:mm:ss.fff} {message}";Console.WriteLine(logMessage);File.AppendAllText(LogFilePath, logMessage + "\n");}public static void LogDivider(){string divider = new string('=', 60);Console.WriteLine(divider);File.AppendAllText(LogFilePath, divider + "\n");}
}
3.报文处理器
// DhPacketProcessor.cs
using System;
using System.Collections.Generic;
using System.Text;public class DhPacketProcessor
{private readonly DhPacketParser _parser = new DhPacketParser();public void SimulateUnpackingScenario(){PacketLogger.LogDivider();PacketLogger.LogInfo("开始模拟 TCP 拆包场景");PacketLogger.LogDivider();// 模拟你的实际数据(被拆分成两部分)byte[] firstPart = HexStringToByteArray("02-11-30-30-30-33-7C-33-32-30-37-34-39-39-35-38-33-30-38-31-37-31-7C-30-30-30-30-30-30-30-30-30-30-30-30-30-7C-30-30-30-30-30-30-30-30-30-30-30-30-30-7C-44-3A-5C-73-6F-72-74-69-6E-67-53-79-73-49-6D-61-67-65-5C-32-30-32-35-31-30-31-31-5C-33-32-30-37-34-39-39-35-38-33-30-38-31-37-31-5F-30-30-30-30-33-5F-32-30-32-35-31-30-31-31-30-36-33-35-33-38-31-34-33-5F-4F-72-69-67-69-6E-61-6C-2E");byte[] secondPart = HexStringToByteArray("6A-70-67-03");PacketLogger.LogInfo("=== 第一次接收(报文第一部分)===");ProcessReceivedData(firstPart, "第一次接收");PacketLogger.LogInfo("=== 第二次接收(报文第二部分)===");ProcessReceivedData(secondPart, "第二次接收");// 检查最终状态var remainingBuffer = _parser.GetBufferSnapshot();if (remainingBuffer.Length > 0){PacketLogger.LogWarning($"处理后缓冲区仍有数据: {BitConverter.ToString(remainingBuffer)}");}else{PacketLogger.LogInfo("缓冲区已清空,所有报文处理完成");}}public void ProcessReceivedData(byte[] data, string receiveEvent = "接收数据"){PacketLogger.LogInfo($"{receiveEvent} - 原始数据: {BitConverter.ToString(data)}");PacketLogger.LogInfo($"{receiveEvent} - 数据长度: {data.Length} 字节");// 添加到解析器_parser.AppendData(data);// 检查缓冲区状态var bufferSnapshot = _parser.GetBufferSnapshot();PacketLogger.LogInfo($"当前缓冲区内容: {BitConverter.ToString(bufferSnapshot)}");PacketLogger.LogInfo($"当前缓冲区大小: {bufferSnapshot.Length} 字节");// 尝试提取完整报文var completePackets = _parser.GetCompletePackets();PacketLogger.LogInfo($"本次提取到完整报文数量: {completePackets.Count}");// 处理每个完整报文foreach (var packet in completePackets){ProcessCompletePacket(packet);}PacketLogger.LogInfo("");}private void ProcessCompletePacket(byte[] packet){PacketLogger.LogDivider();PacketLogger.LogInfo("成功解析完整报文!");PacketLogger.LogInfo($"完整报文 HEX: {BitConverter.ToString(packet)}");PacketLogger.LogInfo($"报文总长度: {packet.Length} 字节");try{// 解析内容(去掉起始和结束标记)if (packet.Length >= 2){byte[] content = new byte[packet.Length - 2];Array.Copy(packet, 1, content, 0, packet.Length - 2);// 转换为ASCIIstring asciiContent = Encoding.ASCII.GetString(content);PacketLogger.LogInfo($"报文内容(ASCII): {asciiContent}");// 分析报文结构AnalyzePacketStructure(asciiContent);}}catch (Exception ex){PacketLogger.LogError($"解析报文内容时出错: {ex.Message}");}PacketLogger.LogDivider();}private void AnalyzePacketStructure(string content){PacketLogger.LogInfo("报文结构分析:");// 根据你的协议格式分析if (content.Contains("|")){var parts = content.Split('|');PacketLogger.LogInfo($"字段数量: {parts.Length}");for (int i = 0; i < parts.Length; i++){PacketLogger.LogInfo($" 字段[{i}]: {parts[i]}");}// 尝试解析文件路径if (parts.Length >= 5 && parts[4].Contains(".jpg")){PacketLogger.LogInfo($"检测到图片文件路径: {parts[4]}");}}}private static byte[] HexStringToByteArray(string hex){hex = hex.Replace("-", "");int numberChars = hex.Length;byte[] bytes = new byte[numberChars / 2];for (int i = 0; i < numberChars; i += 2){bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);}return bytes;}
}
4. 主程序
// Program.cs
using System;class Program3
{static void Main(string[] args){Console.WriteLine("TCP 拆包现象演示程序");Console.WriteLine("这将模拟你的大华相机报文被拆分成两部分的情况\n");var processor = new DhPacketProcessor();try{// 运行演示processor.SimulateUnpackingScenario();Console.WriteLine("\n演示完成!");Console.WriteLine("详细日志已保存到: tcp_packet_debug.log");}catch (Exception ex) {PacketLogger.LogError($"演示过程中出错: {ex}");}Console.WriteLine("\n按任意键退出...");Console.ReadKey();}
}
📋 在现有项目中集成
如果你要在现有项目中集成,只需要:
复制
DhPacketParser
类到你的项目在接收数据的地方调用:
private DhPacketParser _parser = new DhPacketParser();void OnDataReceived(byte[] data) {_parser.AppendData(data);var packets = _parser.GetCompletePackets();foreach (var packet in packets){// 处理完整报文} }
这样就能清晰地展示 TCP 拆包现象并正确重组报文了!