对CustomSerialPort类库的改进
CustomSerialPort 项目地址:flyfire.CustomSerialPort。Github主页上对其介绍为:一个增强的自定义串口类,实现协议无关的数据帧完整接收功能,支持跨平台使用。
经过查看其源码,发现其核心思想是在SerialPortStream类库的基础上,将128ms(默认)时间内接收的串口数据当作一个完整的数据包,通过事件机制将数据分发出去。
该类库是对SerialPortStream类库的再封装,实现了跨平台的串口开发,因此具有一定普适性,但是其源码存在一定问题,下面进行概述。
Gitee项目地址:SerialPortStreamHelper。
注意:Gitee平台中的代码是最新的,如有修改,本文将不再提示。
1、CustomSerialPort存在的问题
(1) CustomSerialPort只在串口接收最新的数据128ms之后才将之前累积的缓存发送出去,因此如果在128ms之内有大于一个包数据到来,那么就会产生粘包问题。
(2) CustomSerialPort在处理数据的时候不是线程安全的,存在接收数据线程和数据处理线程共用数据的情况。
(3) CustomSerialPort会在一个数据处理周期(128ms)内创建一个线程,导致线程被频繁创建,造成资源浪费。
2、可能的解决方法
(1) CustomSerialPort假定每一个数据接收周期(默认128ms)之内收到的数据都是完整数据包(可能有多个),因此不必处理粘包问题。
(2) 如果在一个接收数据时间周期之内发送多个命令,可能在这个周期内会收到多个完整包,因此需要对收到的多个包进行处理,以保证数据的完整性。
(3) 将数据处理封装在一个基于信号量的线程处理代码逻辑中,可以解决串口数据线程安全的问题。同时,这样处理也能避免频繁创建线程,节约系统资源。
3、对CustomSerialPort源码的重构:SerialPortStreamHelper
(1) 创建对象时只传入是否启用超时机制以及超时参数。
(2) SerialPortStream的参数只暴露读取属性。
(3) 封装Open、Close、Dispose、Write等方法,将串口参数在Open方法中体现。
(4) 创建串口数据帮助类SerialPortDataHelper及数据处理接口ISerialPortDataObserver。在SerialPortDataHelper中创建线程,基于信号量处理完整的串口数据包。
4、具体实现
ISerialPortDataObserver.cs
namespace Xhubobo.IO.Ports { interface ISerialPortDataObserver { void ReceiveData(byte[] data); } }
SerialPortDataHelper.cs
using System; using System.Collections.Generic; using System.Threading; namespace Xhubobo.IO.Ports { internal class SerialPortDataHelper { #region 线程 private Thread _threadWorker; private bool _threadWorking; private readonly object _threadWorkingLockHelper = new object(); private bool IsThreadWorking { get { bool ret; lock (_threadWorkingLockHelper) { ret = _threadWorking; } return ret; } set { lock (_threadWorkingLockHelper) { _threadWorking = value; } } } #endregion #region 队列 private readonly Queue<byte[]> _messageQueue; private readonly Semaphore _messageSemaphore; #endregion private readonly byte[] _buffer; private int _offset; private int _lastMessageTick; private readonly int _timeout; private readonly ISerialPortDataObserver _dataObserver; public SerialPortDataHelper( ISerialPortDataObserver dataObserver, int timeout = 128, int bufferSize = 4096) { _dataObserver = dataObserver; _timeout = timeout; _buffer = new byte[bufferSize]; _messageQueue = new Queue<byte[]>(); _messageSemaphore = new Semaphore(0, byte.MaxValue); } public void Start() { IsThreadWorking = true; _threadWorker = new Thread(DoWork) { IsBackground = true }; _threadWorker.Start(); } public void Stop() { IsThreadWorking = false; AddMessage(null); if (_threadWorker != null) { _threadWorker.Join(); _threadWorker = null; } ClearMessage(); } #region 队列操作 public void AddMessage(byte[] message) { if (IsThreadWorking) { lock (_messageQueue) { _messageQueue.Enqueue(message); } _messageSemaphore.Release(); } } private byte[] PickMessage() { byte[] message = null; lock (_messageQueue) { if (_messageQueue.Count > 0) { message = _messageQueue.Peek(); _messageQueue.Dequeue(); } } return message; } private void ClearMessage() { lock (_messageQueue) { _messageQueue.Clear(); } } #endregion /// <summary> /// 线程执行方法 /// </summary> private void DoWork() { while (IsThreadWorking) { if (_messageSemaphore.WaitOne(1)) { var message = PickMessage(); HandleMessage(message); } DispatchData(); } } #region HandleMessage private void HandleMessage(byte[] message) { _lastMessageTick = Environment.TickCount; if (_offset + message.Length > _buffer.Length) { ResetBuffer(); return; } Buffer.BlockCopy(message, 0, _buffer, _offset, message.Length); _offset += message.Length; } private void DispatchData() { if (_offset > 0 && Environment.TickCount - _lastMessageTick > _timeout) { var buffer = new byte[_offset]; Buffer.BlockCopy(_buffer, 0, buffer, 0, _offset); _dataObserver?.ReceiveData(buffer); ResetBuffer(); } } private void ResetBuffer() { _offset = 0; } #endregion } }
SerialPortStreamHelper.cs
using RJCP.IO.Ports; using System; using System.Linq; namespace Xhubobo.IO.Ports { public class SerialPortStreamHelper : ISerialPortDataObserver { public event Action<string, byte[]> DataReceived = (portName, data) => { }; public string LastErrorMessage { get; private set; } #region SerialPortStream Attributes public string PortName => _serialPortStream.PortName; public int BaudRate => _serialPortStream.BaudRate; public Parity Parity => _serialPortStream.Parity; public int DataBits => _serialPortStream.DataBits; public StopBits StopBits => _serialPortStream.StopBits; public bool IsOpen => _serialPortStream.IsOpen; public bool DtrEnable { set { _serialPortStream.DtrEnable = value; } get { return _serialPortStream.DtrEnable; } } public bool RtsEnable { set { _serialPortStream.RtsEnable = value; } get { return _serialPortStream.RtsEnable; } } #endregion /// <summary> /// 是否使用接收超时机制 /// 默认为true /// 接收到数据后计时,计时期间收到数据,累加数据,重新开始计时。超时后返回接收到的数据。 /// </summary> private readonly bool _enableTimeout; private readonly SerialPortStream _serialPortStream; private readonly SerialPortDataHelper _dataHelper; public SerialPortStreamHelper(bool enableTimeout = true, int timeout = 128, int bufferSize = 4096) { _enableTimeout = enableTimeout; _serialPortStream = new SerialPortStream() { DtrEnable = true, RtsEnable = true }; _serialPortStream.DataReceived += OnDataReceived; _dataHelper = new SerialPortDataHelper(this, timeout, bufferSize); } public static string[] GetPortNames() => SerialPortStream.GetPortNames(); public static string BytesToHexStr(byte[] bytes) { return string.Join(" ", bytes.Select(t => t.ToString("X2"))); } public bool Open(string portName, int baudRate = 115200, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One) { _serialPortStream.PortName = portName; _serialPortStream.BaudRate = baudRate; _serialPortStream.Parity = parity; _serialPortStream.DataBits = dataBits; _serialPortStream.StopBits = stopBits; try { _serialPortStream.Open(); _dataHelper.Start(); return true; } catch (Exception e) { LastErrorMessage = e.Message; return false; } } public void Close() { if (_serialPortStream.IsOpen) { _dataHelper.Stop(); _serialPortStream.Close(); } } public void Dispose() { _dataHelper.Stop(); _serialPortStream.Dispose(); } public void ReceiveData(byte[] data) { DataReceived?.Invoke(PortName, data); } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { if (_enableTimeout) { while (_serialPortStream.BytesToRead > 0) { var bytesToRead = _serialPortStream.BytesToRead; var buffer = new byte[bytesToRead]; var bytesRead = _serialPortStream.Read(buffer, 0, bytesToRead); //此处可能存在数组越界问题 if (bytesRead != bytesToRead) { throw new Exception("Serial port receives exception!"); } _dataHelper.AddMessage(buffer); } } else { var bytesToRead = _serialPortStream.BytesToRead; if (bytesToRead == 0) { return; } var buffer = new byte[bytesToRead]; var offset = 0; while (offset < bytesToRead) { //读取数据到缓冲区 offset += _serialPortStream.Read(buffer, offset, bytesToRead - offset); } DataReceived?.Invoke(PortName, buffer); } } #region Write public void Write(byte[] buffer) { if (IsOpen) { _serialPortStream.Write(buffer, 0, buffer.Length); } } public void Write(byte[] buffer, int offset, int count) { if (IsOpen) { _serialPortStream.Write(buffer, offset, count); } } public void Write(string text) { if (IsOpen) { _serialPortStream.Write(text); } } public void WriteLine(string text) { if (IsOpen) { _serialPortStream.WriteLine(text); } } #endregion } }