C# 串口通信不再踩坑:一次发送、分包接收的零丢失实战秘籍

前言

工业控制、物联网设备通信中,是否遇到过这样的场景:向设备发送一个简单的查询指令,却发现返回的数据总是"分批到达"?明明应该收到完整的20字节响应,却只能收到几个零散的数据包?

别急,这不是你的代码有问题!

这是串口通信中最常见的"分包接收"现象。设备可能一次发送10字节,下一次发送剩余的10字节,而我们的程序却不知道什么时候才算接收完成。

今天我们就来彻底解决这个让无数 C# 开发头疼的问题!

为什么会分包接收?

根本原因

串口通信是异步的,数据传输会受到以下因素影响:

硬件缓冲区大小限制

设备处理速度差异

网络延迟(对于串口转以太网设备)

系统调度

这些因素导致原本连续的数据流被操作系统或中间设备拆分成多个小块,逐次送达应用程序。

传统方案的痛点

csharp 复制代码
// ❌ 错误示例:只能收到第一包数据
serialPort.Write(command, 0, command.Length);
Thread.Sleep(100); // 固定等待时间
byte[] buffer = new byte[1024];
int count = serialPort.Read(buffer, 0, 1024); // 可能只读到部分数据

这种写法的问题包括:

固定等待时间不可靠

无法判断数据是否接收完整

容易丢失后续数据包

四种灵活接收策略

为应对不同应用场景,我们设计了以下四种策略:

方案一:数据间隔超时判断(⭐推荐)

适用场景:不知道数据长度,但设备发送完毕后会有明显时间间隔。

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;
}

实际业务中,这个能解决大部分问题。

方案二:结束符判断

适用场景 :数据以特定字符结尾(如 \r\n\0 等)。

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[receivedBuffer.Count - endMarker.Length + i] != endMarker[i])
                    {
                        foundEndMarker = false;
                        break;
                    }
                }
                
                if (foundEndMarker)
                {
                    return receivedBuffer.ToArray();
                }
            }
        }
    }
}

方案三:协议帧结构判断

适用场景:数据有固定帧头和长度字段(如 Modbus 协议)。

csharp 复制代码
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();
                    }
                }
            }
        }
    }
}

方案四:组合策略(⭐⭐推荐)

最灵活的方案,同时使用多种判断条件:

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();
        }
    }
}

核心机制:数据接收事件

所有策略都依赖于统一的数据接收事件处理:

csharp 复制代码
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}");
    }
}

完整代码

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[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();
        }
    }
}

完整代码

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();
            }
        }
    }
}
null

性能优化与实践

关键参数调优

csharp 复制代码
// ✅ 推荐配置
int gapTimeoutMs = 100;    // 100-200ms适合大多数设备
int maxWaitMs = 3000;      // 总超时3秒,避免程序卡死
Thread.Sleep(10);          // 轮询间隔10ms,平衡CPU占用和响应速度

线程安全保障

csharp 复制代码
private readonly object bufferLock = new object();
// 所有缓冲区操作都要加锁
lock (bufferLock)
{
    receivedBuffer.Clear();
    receivedBuffer.AddRange(buffer);
    // ... 其他缓冲区操作
}

常见提醒

1、忘记清空缓冲区:每次查询前必须 receivedBuffer.Clear()

2、超时时间设置不当:间隔超时太短会截断数据,太长会影响响应速度

3、线程安全问题:DataReceived 事件在不同线程中执行,必须加锁保护

4、资源释放:程序结束前记得调用 Close() 方法

适用场景对比

方案 适用场景 优点 缺点
间隔超时 通用场景 简单可靠 需要调试最佳间隔时间
结束符判断 文本协议 精确判断 需要明确的结束符
帧结构判断 二进制协议 最精确 需要了解协议细节
组合策略 复杂场景 最灵活 代码稍复杂

总结

1、选择合适的策略:对于90%的场景,数据间隔超时判断就足够了

2、参数调优很重要:100-200ms 的间隔超时是经验值,需要根据实际设备调整

3、组合策略是王道:当单一策略无法满足需求时,组合策略提供了最大的灵活性

通过本文介绍的四种策略,开发者可以根据具体通信协议灵活选择最适合的接收方式,彻底告别"分包接收"带来的困扰。

关键词

串口通信、分包接收、C#、SerialPort、数据间隔超时、结束符判断、协议帧结构、组合策略、线程安全、工业控制

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

相关推荐
superman超哥43 分钟前
Rust 堆内存与栈内存的所有权管理:精确控制的内存模型
开发语言·后端·rust·编程语言·内存模型·堆内存与栈内存·所有权管理
MyBFuture1 小时前
C#表格与定时器实战技巧
开发语言·windows·c#·visual studio
羊小猪~~1 小时前
数据库学习笔记(十八)--事务
数据库·笔记·后端·sql·学习·mysql
喷火龙8号1 小时前
2025,我的"Vibe Coding"时刻:AI如何重塑我的开发效率与技术成长
人工智能·后端
麦兜*1 小时前
Spring Boot 启动过程全解析:从main方法到Tomcat启动的魔法之旅
java·spring boot·后端·spring·tomcat·firefox
GrowingYi1 小时前
Go语言的特性
开发语言·后端·golang
冬奇Lab1 小时前
Vercel部署全攻略:从GitHub到上线,10分钟让你的前端项目免费拥有自己的域名
前端·后端·node.js
宁在春1 小时前
【2025 年终总结】人好像真的只活那么几个瞬间
后端·程序员·年终总结
程序员根根1 小时前
AOP 全解析:从核心概念到实战落地(Spring Boot 场景)
后端
小蒜学长1 小时前
足球联赛管理系统(代码+数据库+LW)
java·数据库·spring boot·后端