C#实现串口通信的四种灵活策略和避坑指南
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>前言</li><li>为什么会分包接收</li><ul class="second_class_ul"><li>根本原因</li><li>传统方案的痛点</li></ul><li>四种灵活接收策略</li><ul class="second_class_ul"><li>方案一:数据间隔超时判断(⭐推荐)</li><li>方案二:结束符判断</li><li>方案三:协议帧结构判断</li><li>方案四:组合策略(⭐⭐推荐)</li></ul><li>核心机制:数据接收事件</li><ul class="second_class_ul"></ul><li>完整代码</li><ul class="second_class_ul"></ul><li>性能优化与实践</li><ul class="second_class_ul"><li>关键参数调优</li><li>线程安全保障</li><li>常见提醒</li></ul><li>适用场景对比</li><ul class="second_class_ul"></ul><li>总结</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>前言</h2><p>工业控制、物联网设备通信中,是否遇到过这样的场景:向设备发送一个简单的查询指令,却发现返回的数据总是"分批到达"?明明应该收到完整的20字节响应,却只能收到几个零散的数据包?</p>
<p><strong>别急,这不是你的代码有问题!</strong></p>
<p>这是串口通信中最常见的"分包接收"现象。设备可能一次发送10字节,下一次发送剩余的10字节,而我们的程序却不知道什么时候才算接收完成。</p>
<p>今天我们就来彻底解决这个让无数 C# 开发头疼的问题!</p>
<p class="maodian"></p><h2>为什么会分包接收</h2>
<p class="maodian"></p><h3>根本原因</h3>
<p>串口通信是<strong>异步</strong>的,数据传输会受到以下因素影响:</p>
<p><strong>硬件缓冲区大小限制</strong></p>
<p><strong>设备处理速度差异</strong></p>
<p><strong>网络延迟</strong>(对于串口转以太网设备)</p>
<p><strong>系统调度</strong></p>
<p>这些因素导致原本连续的数据流被 操作系统或中间设备拆分成多个小块,逐次送达应用程序。</p>
<p class="maodian"></p><h3>传统方案的痛点</h3>
<div class="jb51code"><pre class="brush:csharp;">// ❌ 错误示例:只能收到第一包数据
serialPort.Write(command, 0, command.Length);
Thread.Sleep(100); // 固定等待时间
byte[] buffer = new byte;
int count = serialPort.Read(buffer, 0, 1024); // 可能只读到部分数据
</pre></div>
<p>这种写法的问题包括:</p>
<p>固定等待时间不可靠</p>
<p>无法判断数据是否接收完整</p>
<p>容易丢失后续数据包</p>
<p class="maodian"></p><h2>四种灵活接收策略</h2>
<p>为应对不同应用场景,我们设计了以下四种策略:</p>
<p class="maodian"></p><h3>方案一:数据间隔超时判断(⭐推荐)</h3>
<p><strong>适用场景</strong>:不知道数据长度,但设备发送完毕后会有明显时间间隔。</p>
<div class="jb51code"><pre class="brush:csharp;">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;
}
</pre></div>
<p>实际业务中,这个能解决大部分问题。</p>
<p class="maodian"></p><h3>方案二:结束符判断</h3>
<p><strong>适用场景</strong>:数据以特定字符结尾(如 <code>\r\n</code>、<code>\0</code> 等)。</p>
<div class="jb51code"><pre class="brush:csharp;">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 != endMarker)
{
foundEndMarker = false;
break;
}
}
if (foundEndMarker)
{
return receivedBuffer.ToArray();
}
}
}
}
}
</pre></div>
<p class="maodian"></p><h3>方案三:协议帧结构判断</h3>
<p><strong>适用场景</strong>:数据有固定帧头和长度字段(如 Modbus 协议)。</p>
<div class="jb51code"><pre class="brush:csharp;">public byte[] SendQueryWithFrameProtocol(byte[] command, byte frameHeader, int lengthFieldOffset, int lengthFieldSize = 1)
{
// ... 发送逻辑 ...
while (/* 超时检查 */)
{
lock (bufferLock)
{
if (receivedBuffer.Count > lengthFieldOffset + lengthFieldSize)
{
// 检查帧头
if (receivedBuffer == frameHeader)
{
// 🔥 从长度字段获取数据长度
int dataLength = lengthFieldSize == 1 ?
receivedBuffer :
(receivedBuffer << 8) | receivedBuffer;
int expectedFrameLength = lengthFieldOffset + lengthFieldSize + dataLength;
if (receivedBuffer.Count >= expectedFrameLength)
{
return receivedBuffer.Take(expectedFrameLength).ToArray();
}
}
}
}
}
}
</pre></div>
<p class="maodian"></p><h3>方案四:组合策略(⭐⭐推荐)</h3>
<p><strong>最灵活的方案</strong>,同时使用多种判断条件:</p>
<div class="jb51code"><pre class="brush:csharp;">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();
}
}
}
</pre></div>
<p class="maodian"></p><h2>核心机制:数据接收事件</h2>
<p>所有策略都依赖于统一的数据接收事件处理:</p>
<div class="jb51code"><pre class="brush:csharp;">private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (!isWaitingForResponse) return;
try
{
int bytesToRead = serialPort.BytesToRead;
if (bytesToRead > 0)
{
byte[] buffer = new byte;
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}");
}
}
</pre></div>
<p class="maodian"></p><h2>完整代码</h2>
<div class="jb51code"><pre class="brush:csharp;">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 != endMarker)
{
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 == frameHeader)
{
// 获取数据长度
int dataLength = 0;
if (lengthFieldSize == 1)
{
dataLength = receivedBuffer;
}
else if (lengthFieldSize == 2)
{
dataLength = (receivedBuffer << 8) | receivedBuffer;
}
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 != endMarker)
{
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;
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();
}
}
}
</pre></div>
<div class="jb51code"><pre class="brush:csharp;">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();
}
}
}
}
</pre></div>
<p>结果如下: </p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026010609280655.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026010609280645.png" /></p>
<p class="maodian"></p><h2>性能优化与实践</h2>
<p class="maodian"></p><h3>关键参数调优</h3>
<div class="jb51code"><pre class="brush:csharp;">// ✅ 推荐配置
int gapTimeoutMs = 100; // 100-200ms适合大多数设备
int maxWaitMs = 3000; // 总超时3秒,避免程序卡死
Thread.Sleep(10); // 轮询间隔10ms,平衡CPU占用和响应速度
</pre></div>
<p class="maodian"></p><h3>线程安全保障</h3>
<div class="jb51code"><pre class="brush:csharp;">private readonly object bufferLock = new object();
// 所有缓冲区操作都要加锁
lock (bufferLock)
{
receivedBuffer.Clear();
receivedBuffer.AddRange(buffer);
// ... 其他缓冲区操作
}
</pre></div>
<p class="maodian"></p><h3>常见提醒</h3>
<p>1、忘记清空缓冲区:每次查询前必须 <code>receivedBuffer.Clear()</code></p>
<p>2、超时时间设置不当:间隔超时太短会截断数据,太长会影响响应速度</p>
<p>3、线程安全问题:<code>DataReceived</code> 事件在不同线程中执行,必须加锁保护</p>
<p>4、资源释放:程序结束前记得调用 <code>Close()</code> 方法</p>
<p class="maodian"></p><h2>适用场景对比</h2>
<table><thead><tr><th>方案</th><th>适用场景</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>间隔超时</td><td>通用场景</td><td>简单可靠</td><td>需要调试最佳间隔时间</td></tr><tr><td>结束符判断</td><td>文本协议</td><td>精确判断</td><td>需要明确的结束符</td></tr><tr><td>帧结构判断</td><td>二进制协议</td><td>最精确</td><td>需要了解协议细节</td></tr><tr><td>组合策略</td><td>复杂场景</td><td>最灵活</td><td>代码稍复杂</td></tr></tbody></table>
<p class="maodian"></p><h2>总结</h2>
<p>1、选择合适的策略:对于90%的场景,<strong>数据间隔超时判断</strong>就足够了</p>
<p>2、参数调优很重要:100-200ms 的间隔超时是经验值,需要根据实际设备调整</p>
<p>3、组合策略是王道:当单一策略无法满足需求时,组合策略提供了最大的灵活性</p>
<p>通过本文介绍的四种策略,开发者可以根据具体通信协议灵活选择最适合的接收方式,彻底告别"分包接收"带来的困扰。</p>
<p>到此这篇关于C#实现串口通信的四种灵活策略和避坑指南的文章就介绍到这了,更多相关C#串口通信内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>使用c#进行串口通信的实现示例</li><li>C#串口通信总是丢数据的原因及解决方案</li><li>C#实现串口通信详解</li><li>C#实现简单串口通信的示例详解</li><li>C#实现串口通信的示例详解</li><li>c# 模拟串口通信 SerialPort的实现示例</li><li>C#操作串口通信协议Modbus的常用方法介绍</li><li>C#实现简单串口通信</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]