在工业控制、物联网设备通信中,你是否遇到过这样的场景:向设备发送一个简单的查询指令,却发现返回的数据总是"分批到达"?明明应该收到完整的20字节响应,却只能收到几个零散的数据包?
别急,这不是你的代码有问题!
这是串口通信中最常见的"分包接收"现象。设备可能一次发送10字节,下一次发送剩余的10字节,而我们的程序却不知道什么时候才算接收完成。
今天我们就来彻底解决这个让无数C#开发者头疼的问题!
串口通信是异步的,数据传输会受到以下因素影响:
C#// ❌ 错误示例:只能收到第一包数据
serialPort.Write(command, 0, command.Length);
Thread.Sleep(100); // 固定等待时间
byte[] buffer = new byte[1024];
int count = serialPort.Read(buffer, 0, 1024); // 可能只读到部分数据
这种写法的问题:
基于不同应用场景,我设计了四种接收策略:
适用场景:不知道数据长度,但设备发送完毕后会有明显时间间隔
C#public byte[] SendQueryWithGapTimeout(byte[] command, int gapTimeoutMs = 100, int maxWaitMs = 3000)
{
// 清空缓冲区并开始接收
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
lastReceiveTime = DateTime.Now;
}
// 发送指令
serialPort.Write(command, 0, command.Length);
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
// 🔥 关键逻辑:有数据且间隔超时则认为接收完成
if (receivedBuffer.Count > 0 &&
(DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
{
isWaitingForResponse = false;
return receivedBuffer.ToArray();
}
}
}
return null;
}
实际业务中,这个能解决大部分问题。
适用场景:数据以特定字符结尾(如\r\n、\0等)
C#public byte[] SendQueryWithEndMarker(byte[] command, byte[] endMarker, int maxWaitMs = 3000)
{
// ... 发送逻辑相同 ...
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
lock (bufferLock)
{
if (receivedBuffer.Count >= endMarker.Length)
{
// 🔥 检查缓冲区末尾是否包含结束标记
bool foundEndMarker = true;
for (int i = 0; i < endMarker.Length; i++)
{
if (receivedBuffer[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
return receivedBuffer.ToArray();
}
}
}
}
}
适用场景:数据有固定帧头和长度字段(如Modbus协议)
C#public byte[] SendQueryWithFrameProtocol(byte[] command, byte frameHeader, int lengthFieldOffset, int lengthFieldSize = 1)
{
// ... 发送逻辑 ...
while (/* 超时检查 */)
{
lock (bufferLock)
{
if (receivedBuffer.Count > lengthFieldOffset + lengthFieldSize)
{
// 检查帧头
if (receivedBuffer[0] == frameHeader)
{
// 🔥 从长度字段获取数据长度
int dataLength = lengthFieldSize == 1 ?
receivedBuffer[lengthFieldOffset] :
(receivedBuffer[lengthFieldOffset] << 8) | receivedBuffer[lengthFieldOffset + 1];
int expectedFrameLength = lengthFieldOffset + lengthFieldSize + dataLength;
if (receivedBuffer.Count >= expectedFrameLength)
{
return receivedBuffer.Take(expectedFrameLength).ToArray();
}
}
}
}
}
}
最灵活的方案,同时使用多种判断条件:
C#public byte[] SendQueryWithCombinedStrategy(byte[] command,
int gapTimeoutMs = 100, // 数据间隔超时
byte[] endMarker = null, // 结束标记
int? maxLength = null, // 最大长度限制
int maxWaitMs = 3000) // 总超时时间
{
// ... 发送逻辑 ...
while (/* 总超时检查 */)
{
lock (bufferLock)
{
if (receivedBuffer.Count == 0) continue;
// 🔥 条件1:达到最大长度限制
if (maxLength.HasValue && receivedBuffer.Count >= maxLength.Value)
return receivedBuffer.ToArray();
// 🔥 条件2:发现结束标记
if (endMarker != null && /* 检查结束标记逻辑 */)
return receivedBuffer.ToArray();
// 🔥 条件3:数据间隔超时
if ((DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
return receivedBuffer.ToArray();
}
}
}
C#private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (!isWaitingForResponse) return;
try
{
int bytesToRead = serialPort.BytesToRead;
if (bytesToRead > 0)
{
byte[] buffer = new byte[bytesToRead];
int bytesRead = serialPort.Read(buffer, 0, bytesToRead);
lock (bufferLock)
{
receivedBuffer.AddRange(buffer);
lastReceiveTime = DateTime.Now; // 🔥 更新最后接收时间
Console.WriteLine($"收到数据包 ({bytesRead} 字节): {BitConverter.ToString(buffer, 0, bytesRead)}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"数据接收异常: {ex.Message}");
}
}
C#using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppFlexSerialPort
{
internal class FlexibleSerialPort
{
private SerialPort serialPort;
private List<byte> receivedBuffer;
private readonly object bufferLock = new object();
private Timer timeoutTimer;
private bool isWaitingForResponse = false;
private DateTime lastReceiveTime;
private readonly int dataGapTimeout = 100; // 数据间隔超时时间(ms)
public FlexibleSerialPort()
{
receivedBuffer = new List<byte>();
}
/// <summary>
/// 初始化串口
/// </summary>
public bool InitializePort(string portName = "COM1", int baudRate = 9600,
Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One)
{
try
{
serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits);
serialPort.ReadTimeout = 1000;
serialPort.WriteTimeout = 1000;
serialPort.DataReceived += OnDataReceived;
serialPort.Open();
Console.WriteLine($"串口 {portName} 已成功打开");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"串口初始化失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 方案1: 基于数据间隔超时判断接收完成
/// 适用于:不知道数据长度,但设备发送完后会有明显的时间间隔
/// </summary>
public byte[] SendQueryWithGapTimeout(byte[] command, int gapTimeoutMs = 100, int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打开");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
lastReceiveTime = DateTime.Now;
}
try
{
// 发送查询指令
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已发送查询指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
DateTime lastCheckTime = DateTime.Now;
int lastBufferSize = 0;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
// 如果有数据且数据间隔超过指定时间,认为接收完成
if (receivedBuffer.Count > 0 &&
(DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"基于间隔超时判断接收完成,共收到 {result.Length} 字节");
return result;
}
}
}
// 最大等待时间超时
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"最大等待时间超时,收到 {result.Length} 字节");
return result;
}
}
Console.WriteLine("接收超时,未收到任何数据");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"发送指令失败: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 方案2: 基于结束符判断接收完成
/// 适用于:数据以特定字符或字节序列结尾
/// </summary>
public byte[] SendQueryWithEndMarker(byte[] command, byte[] endMarker, int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打开");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
}
try
{
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已发送查询指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
if (receivedBuffer.Count >= endMarker.Length)
{
// 检查缓冲区末尾是否包含结束标记
bool foundEndMarker = true;
for (int i = 0; i < endMarker.Length; i++)
{
if (receivedBuffer[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"发现结束标记,接收完成,共收到 {result.Length} 字节");
return result;
}
}
}
}
// 超时处理
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"等待结束标记超时,收到 {result.Length} 字节");
return result;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"发送指令失败: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 方案3: 基于协议帧结构判断接收完成
/// 适用于:数据有固定的帧头和长度字段
/// </summary>
public byte[] SendQueryWithFrameProtocol(byte[] command, byte frameHeader, int lengthFieldOffset,
int lengthFieldSize = 1, int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打开");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
}
try
{
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已发送查询指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
if (receivedBuffer.Count > lengthFieldOffset + lengthFieldSize)
{
// 检查帧头
if (receivedBuffer[0] == frameHeader)
{
// 获取数据长度
int dataLength = 0;
if (lengthFieldSize == 1)
{
dataLength = receivedBuffer[lengthFieldOffset];
}
else if (lengthFieldSize == 2)
{
dataLength = (receivedBuffer[lengthFieldOffset] << 8) | receivedBuffer[lengthFieldOffset + 1];
}
int expectedFrameLength = lengthFieldOffset + lengthFieldSize + dataLength;
if (receivedBuffer.Count >= expectedFrameLength)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.Take(expectedFrameLength).ToArray();
Console.WriteLine($"根据帧长度判断接收完成,共收到 {result.Length} 字节");
return result;
}
}
}
}
}
// 超时处理
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"帧协议解析超时,收到 {result.Length} 字节");
return result;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"发送指令失败: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 方案4: 组合策略 - 最灵活的方案
/// 同时使用多种判断条件,任一条件满足就结束接收
/// </summary>
public byte[] SendQueryWithCombinedStrategy(byte[] command,
int gapTimeoutMs = 100,
byte[] endMarker = null,
int? maxLength = null,
int maxWaitMs = 3000)
{
if (serialPort == null || !serialPort.IsOpen)
{
Console.WriteLine("串口未打开");
return null;
}
lock (bufferLock)
{
receivedBuffer.Clear();
isWaitingForResponse = true;
lastReceiveTime = DateTime.Now;
}
try
{
serialPort.Write(command, 0, command.Length);
Console.WriteLine($"已发送查询指令: {BitConverter.ToString(command)}");
DateTime startTime = DateTime.Now;
while ((DateTime.Now - startTime).TotalMilliseconds < maxWaitMs)
{
Thread.Sleep(10);
lock (bufferLock)
{
if (receivedBuffer.Count == 0) continue;
// 条件1: 检查最大长度限制
if (maxLength.HasValue && receivedBuffer.Count >= maxLength.Value)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"达到最大长度限制,接收完成,共收到 {result.Length} 字节");
return result;
}
// 条件2: 检查结束标记
if (endMarker != null && receivedBuffer.Count >= endMarker.Length)
{
bool foundEndMarker = true;
for (int i = 0; i < endMarker.Length; i++)
{
if (receivedBuffer[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"发现结束标记,接收完成,共收到 {result.Length} 字节");
return result;
}
}
// 条件3: 检查数据间隔超时
if ((DateTime.Now - lastReceiveTime).TotalMilliseconds > gapTimeoutMs)
{
isWaitingForResponse = false;
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"数据间隔超时,接收完成,共收到 {result.Length} 字节");
return result;
}
}
}
// 最大等待时间超时
isWaitingForResponse = false;
lock (bufferLock)
{
if (receivedBuffer.Count > 0)
{
byte[] result = receivedBuffer.ToArray();
Console.WriteLine($"最大等待时间超时,收到 {result.Length} 字节");
return result;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"发送指令失败: {ex.Message}");
isWaitingForResponse = false;
return null;
}
}
/// <summary>
/// 数据接收事件处理
/// </summary>
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (!isWaitingForResponse) return;
try
{
int bytesToRead = serialPort.BytesToRead;
if (bytesToRead > 0)
{
byte[] buffer = new byte[bytesToRead];
int bytesRead = serialPort.Read(buffer, 0, bytesToRead);
lock (bufferLock)
{
receivedBuffer.AddRange(buffer);
lastReceiveTime = DateTime.Now; // 更新最后接收时间
Console.WriteLine($"收到数据包 ({bytesRead} 字节): {BitConverter.ToString(buffer, 0, bytesRead)}");
Console.WriteLine($"当前缓冲区总计: {receivedBuffer.Count} 字节");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"数据接收处理异常: {ex.Message}");
}
}
/// <summary>
/// 关闭串口
/// </summary>
public void Close()
{
try
{
isWaitingForResponse = false;
if (serialPort != null && serialPort.IsOpen)
{
serialPort.Close();
Console.WriteLine("串口已关闭");
}
}
catch (Exception ex)
{
Console.WriteLine($"关闭串口异常: {ex.Message}");
}
}
public static string[] GetAvailablePorts()
{
return SerialPort.GetPortNames();
}
}
}
C#namespace AppFlexSerialPort
{
internal class Program
{
static void Main(string[] args)
{
FlexibleSerialPort comm = new FlexibleSerialPort();
try
{
if (comm.InitializePort("COM1", 9600))
{
byte[] queryCommand = { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A };
Console.WriteLine("=== 方案1: 基于数据间隔判断 ===");
byte[] response1 = comm.SendQueryWithGapTimeout(queryCommand, 150, 3000);
Thread.Sleep(1000);
Console.WriteLine("\n=== 方案2: 基于结束符判断 ===");
byte[] endMarker = { 0x0D, 0x0A }; // CR LF
byte[] response2 = comm.SendQueryWithEndMarker(queryCommand, endMarker);
Thread.Sleep(1000);
Console.WriteLine("\n=== 方案3: 基于协议帧结构判断 ===");
byte[] response3 = comm.SendQueryWithFrameProtocol(queryCommand, 0x01, 2, 1);
Thread.Sleep(1000);
Console.WriteLine("\n=== 方案4: 组合策略 ===");
byte[] response4 = comm.SendQueryWithCombinedStrategy(
queryCommand,
gapTimeoutMs: 100, // 数据间隔100ms
endMarker: new byte[] { 0x0A }, // 或者以LF结尾
maxLength: 50, // 或者最多50字节
maxWaitMs: 3000 // 最多等待3秒
);
}
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
catch (Exception ex)
{
Console.WriteLine($"程序异常: {ex.Message}");
}
finally
{
comm.Close();
}
}
}
}


C#// ✅ 推荐配置
int gapTimeoutMs = 100; // 100-200ms适合大多数设备
int maxWaitMs = 3000; // 总超时3秒,避免程序卡死
Thread.Sleep(10); // 轮询间隔10ms,平衡CPU占用和响应速度
C#private readonly object bufferLock = new object();
// 所有缓冲区操作都要加锁
lock (bufferLock)
{
receivedBuffer.Clear();
receivedBuffer.AddRange(buffer);
// ... 其他缓冲区操作
}
receivedBuffer.Clear()Close()方法| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 间隔超时 | 通用场景 | 简单可靠 | 需要调试最佳间隔时间 |
| 结束符判断 | 文本协议 | 精确判断 | 需要明确的结束符 |
| 帧结构判断 | 二进制协议 | 最精确 | 需要了解协议细节 |
| 组合策略 | 复杂场景 | 最灵活 | 代码稍复杂 |
💬 互动时间
你在串口通信中遇到过哪些奇怪的分包现象?欢迎在评论区分享你的经验和踩过的坑!
🔄 觉得这个解决方案有用吗?请转发给更多需要的C#开发同行!
现在就去试试这四种策略,告别分包接收的烦恼吧! 🚀
相关信息
通过网盘分享的文件:AppFlexSerialPort.zip 链接: https://pan.baidu.com/s/1Ya-QoDKcZh8vIhgRdrVMIw?pwd=kz69 提取码: kz69 --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!