对比SerialCommunication和微软的SerialPort,向SerialPort看齐
SerialCommunication是我综合网上看到的代码稍作修改而成的串口通信类,而SerialPort则是C#的System类库的IO目录Ports子目录下的串口通信类。SerialCommunication只有区区的二百多行,而SerialPort则有几千行。下面我将介绍SerialPort相对于SerialCommunication好在哪里。
首先是一些基本字段的对比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /// <summary> /// 波特率 /// </summary> public int BaudRate; /// <summary> /// 数据位 /// </summary> public byte ByteSize = 8; /// <summary> /// 奇偶校验,0-4=no,odd,even,mark,space /// </summary> public byte Parity = 1; /// <summary> /// 串口号 /// </summary> public int PortNum; /// <summary> /// 读超时 /// </summary> public UInt32 ReadTimeout = 1000; /// <summary> /// 停止位,0,1,2 = 1, 1.5, 2 /// </summary> public byte StopBits; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | public SerialPort() { this .baudRate = 0x2580; this .dataBits = 8; this .stopBits = System.IO.Ports.StopBits.One; this .portName = "COM1" ; this .encoding = System.Text.Encoding.ASCII; this .decoder = System.Text.Encoding.ASCII.GetDecoder(); this .maxByteCountForSingleChar = System.Text.Encoding.ASCII.GetMaxByteCount(1); this .readTimeout = -1; this .writeTimeout = -1; this .receivedBytesThreshold = 1; this .parityReplace = 0x3f; this .newLine = "\n" ; this .readBufferSize = 0x1000; this .writeBufferSize = 0x800; this .inBuffer = new byte [0x400]; this .oneChar = new char [1]; } public SerialPort(IContainer container) { this .baudRate = 0x2580; this .dataBits = 8; this .stopBits = System.IO.Ports.StopBits.One; this .portName = "COM1" ; this .encoding = System.Text.Encoding.ASCII; this .decoder = System.Text.Encoding.ASCII.GetDecoder(); this .maxByteCountForSingleChar = System.Text.Encoding.ASCII.GetMaxByteCount(1); this .readTimeout = -1; this .writeTimeout = -1; this .receivedBytesThreshold = 1; this .parityReplace = 0x3f; this .newLine = "\n" ; this .readBufferSize = 0x1000; this .writeBufferSize = 0x800; this .inBuffer = new byte [0x400]; this .oneChar = new char [1]; container.Add( this ); } [TargetedPatchingOptOut( "Performance critical to inline this type of method across NGen image boundaries" )] public SerialPort( string portName) : this (portName, 0x2580, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One) { } [TargetedPatchingOptOut( "Performance critical to inline this type of method across NGen image boundaries" )] public SerialPort( string portName, int baudRate) : this (portName, baudRate, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One) { } [TargetedPatchingOptOut( "Performance critical to inline this type of method across NGen image boundaries" )] public SerialPort( string portName, int baudRate, System.IO.Ports.Parity parity) : this (portName, baudRate, parity, 8, System.IO.Ports.StopBits.One) { } [TargetedPatchingOptOut( "Performance critical to inline this type of method across NGen image boundaries" )] public SerialPort( string portName, int baudRate, System.IO.Ports.Parity parity, int dataBits) : this (portName, baudRate, parity, dataBits, System.IO.Ports.StopBits.One) { } public SerialPort( string portName, int baudRate, System.IO.Ports.Parity parity, int dataBits, System.IO.Ports.StopBits stopBits) { this .baudRate = 0x2580; this .dataBits = 8; this .stopBits = System.IO.Ports.StopBits.One; this .portName = "COM1" ; this .encoding = System.Text.Encoding.ASCII; this .decoder = System.Text.Encoding.ASCII.GetDecoder(); this .maxByteCountForSingleChar = System.Text.Encoding.ASCII.GetMaxByteCount(1); this .readTimeout = -1; this .writeTimeout = -1; this .receivedBytesThreshold = 1; this .parityReplace = 0x3f; this .newLine = "\n" ; this .readBufferSize = 0x1000; this .writeBufferSize = 0x800; this .inBuffer = new byte [0x400]; this .oneChar = new char [1]; this .PortName = portName; this .BaudRate = baudRate; this .Parity = parity; this .DataBits = dataBits; this .StopBits = stopBits; } |
可以看到我有一个明显的不足,就是用了public的字段,这貌似是违反了面向对象的理念的,按照面向对象的理念,外部类应该是不能直接访问操作其他类的内部字段的,不过有些人就认为写private字段,public属性太麻烦,过度设计,能不用属性就不用属性。这里就不讨论用属性的优点了。除此之外还可以看到SerialPort有个明显的优点,使用了大量的重载方法,那些重载方法之间又用了继承,最终执行了参数最多的那个重载方法,该方法里面又将那些参数赋值给了对应的属性,各属性赋值时候又可以进行各种判断,这就是C#的经典套路,也是比较好的套路,只是这样写代码会长很多。
然后看串口打开方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public void Open() { // OPEN THE COMM PORT. _hComm = CreateFile( "COM" + PortNum, GenericRead | GenericWrite, 0, 0, OpenExisting, 0, 0); // IF THE PORT CANNOT BE OPENED, BAIL OUT. if (_hComm == InvalidHandleValue) { throw ( new Exception( "Port Open Failure" )); } CommTimeouts ctoCommPort = new CommTimeouts { ReadTotalTimeoutConstant = ReadTimeout }; if (!SetCommTimeouts(_hComm, ref ctoCommPort)) { throw ( new Exception( "Bad Timeout Settings" )); } Dcb dcb = new Dcb(); GetCommState(_hComm, ref dcb); dcb.BaudRate = BaudRate; dcb.Parity = Parity; dcb.ByteSize = ByteSize; dcb.StopBits = StopBits; if (!SetCommState(_hComm, ref dcb)) { throw ( new Exception( "Bad Com Settings" )); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | public void Open() { if ( this .IsOpen) { throw new InvalidOperationException(SR.GetString( "Port_already_open" )); } new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Demand(); this .internalSerialStream = new SerialStream( this .portName, this .baudRate, this .parity, this .dataBits, this .stopBits, this .readTimeout, this .writeTimeout, this .handshake, this .dtrEnable, this .rtsEnable, this .discardNull, this .parityReplace); this .internalSerialStream.SetBufferSizes( this .readBufferSize, this .writeBufferSize); this .internalSerialStream.ErrorReceived += new SerialErrorReceivedEventHandler( this .CatchErrorEvents); this .internalSerialStream.PinChanged += new SerialPinChangedEventHandler( this .CatchPinChangedEvents); this .internalSerialStream.DataReceived += new SerialDataReceivedEventHandler( this .CatchReceivedEvents); } internal SerialStream( string portName, int baudRate, System.IO.Ports.Parity parity, int dataBits, System.IO.Ports.StopBits stopBits, int readTimeout, int writeTimeout, System.IO.Ports.Handshake handshake, bool dtrEnable, bool rtsEnable, bool discardNull, byte parityReplace) { int dwFlagsAndAttributes = 0x40000000; if (Environment.OSVersion.Platform == PlatformID.Win32Windows) { dwFlagsAndAttributes = 0x80; this .isAsync = false ; } if ((portName == null ) || !portName.StartsWith( "COM" , StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException(SR.GetString( "Arg_InvalidSerialPort" ), "portName" ); } SafeFileHandle hFile = Microsoft.Win32.UnsafeNativeMethods.CreateFile( @"\\.\" + portName, -1073741824, 0, IntPtr.Zero, 3, dwFlagsAndAttributes, IntPtr.Zero); if (hFile.IsInvalid) { InternalResources.WinIOError(portName); } try { int fileType = Microsoft.Win32.UnsafeNativeMethods.GetFileType(hFile); if ((fileType != 2) && (fileType != 0)) { throw new ArgumentException(SR.GetString( "Arg_InvalidSerialPort" ), "portName" ); } this ._handle = hFile; this .portName = portName; this .handshake = handshake; this .parityReplace = parityReplace; this .tempBuf = new byte [1]; this .commProp = new Microsoft.Win32.UnsafeNativeMethods.COMMPROP(); int lpModemStat = 0; if (!Microsoft.Win32.UnsafeNativeMethods.GetCommProperties( this ._handle, ref this .commProp) || !Microsoft.Win32.UnsafeNativeMethods.GetCommModemStatus( this ._handle, ref lpModemStat)) { int errorCode = Marshal.GetLastWin32Error(); switch (errorCode) { case 0x57: case 6: throw new ArgumentException(SR.GetString( "Arg_InvalidSerialPortExtended" ), "portName" ); } InternalResources.WinIOError(errorCode, string .Empty); } if (( this .commProp.dwMaxBaud != 0) && (baudRate > this .commProp.dwMaxBaud)) { throw new ArgumentOutOfRangeException( "baudRate" , SR.GetString( "Max_Baud" , new object [] { this .commProp.dwMaxBaud })); } this .comStat = new Microsoft.Win32.UnsafeNativeMethods.COMSTAT(); this .dcb = new Microsoft.Win32.UnsafeNativeMethods.DCB(); this .InitializeDCB(baudRate, parity, dataBits, stopBits, discardNull); this .DtrEnable = dtrEnable; this .rtsEnable = this .GetDcbFlag(12) == 1; if ((handshake != System.IO.Ports.Handshake.RequestToSend) && (handshake != System.IO.Ports.Handshake.RequestToSendXOnXOff)) { this .RtsEnable = rtsEnable; } if (readTimeout == 0) { this .commTimeouts.ReadTotalTimeoutConstant = 0; this .commTimeouts.ReadTotalTimeoutMultiplier = 0; this .commTimeouts.ReadIntervalTimeout = -1; } else if (readTimeout == -1) { this .commTimeouts.ReadTotalTimeoutConstant = -2; this .commTimeouts.ReadTotalTimeoutMultiplier = -1; this .commTimeouts.ReadIntervalTimeout = -1; } else { this .commTimeouts.ReadTotalTimeoutConstant = readTimeout; this .commTimeouts.ReadTotalTimeoutMultiplier = -1; this .commTimeouts.ReadIntervalTimeout = -1; } this .commTimeouts.WriteTotalTimeoutMultiplier = 0; this .commTimeouts.WriteTotalTimeoutConstant = (writeTimeout == -1) ? 0 : writeTimeout; if (!Microsoft.Win32.UnsafeNativeMethods.SetCommTimeouts( this ._handle, ref this .commTimeouts)) { InternalResources.WinIOError(); } if ( this .isAsync && !ThreadPool.BindHandle( this ._handle)) { throw new IOException(SR.GetString( "IO_BindHandleFailed" )); } Microsoft.Win32.UnsafeNativeMethods.SetCommMask( this ._handle, 0x1fb); this .eventRunner = new EventLoopRunner( this ); new Thread( new ThreadStart( this .eventRunner.WaitForCommEvent)) { IsBackground = true }.Start(); } catch { hFile.Close(); this ._handle = null ; throw ; } } |
打开方法SerialPort比我的复杂很多,我那个只是打开并设置了一下串口,SerialPort除此之外还开启了ErrorReceived、PinChanged和DataReceived事件。两个接收事件和一个貌似是串口端口改变事件吧。前两个不写是因为我觉得有些人串口通信只需要写不需要接收,那么那两个事件也就不需要了吧,如果需要接收的话调用我那个Read方法就可以接收消息了,不过需要调用者自己开线程监听。
不说这个,就说代码方面的问题吧,我那里有个明显的不太好的地方就是,抛出的异常没有进行分类,异常是有格式的,也可以自定义异常,我那里没有对异常进行分类,全都是抛出Exception就完了,这样的话调用者就不能方便的根据异常的类型进行不同的处理了。除此之外抛出的异常也不够详细,SerialPort的Open方法一开始就有一个Port_already_open端口已打开异常,我那边就没有分的这么细了,只要端口打开失败就返回Port Open Failure,异常分的不够细也是不好的。
除此之外就是SerialPort用了大量的枚举,如StopBits、Handshake、Parity、SerialData、SerialError和SerialPinChange,用枚举有个很大的好处就是调用者可以清楚的根据枚举来判断每个枚举值的意义,这样子调用者可以在不查文档的情况下就顺利的写完了代码,这可能会节省编码人员很多的时间,与用枚举损失的些须效率,这是完全值得的,我没有用枚举只是贪图方便省事,这是不够好的,应该注意一下。
接着看串口关闭方法
1 2 3 4 5 6 7 | public void Close() { if (_hComm != InvalidHandleValue) { CloseHandle(_hComm); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public void Close() { base .Dispose(); } public void Dispose() { this .Dispose( true ); GC.SuppressFinalize( this ); } protected virtual void Dispose( bool disposing) { if (disposing) { lock ( this ) { if (( this .site != null ) && ( this .site.Container != null )) { this .site.Container.Remove( this ); } if ( this .events != null ) { EventHandler handler = (EventHandler) this .events[EventDisposed]; if (handler != null ) { handler( this , EventArgs.Empty); } } } } } |
关闭方法就完全不同了,我那关闭方法只是把串口关闭,SerialPort则貌似是把整个类Dispose了,这两种做法萝卜青菜各有所爱吧。不过我那个类貌似没有继承自IDisposable貌似不太好,比较大的类应该要提供Dispose才好的吧。
最后看看读写方法吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public byte [] Read( int numBytes) { if (_hComm == InvalidHandleValue) { throw ( new Exception( "Comm Port Not Open" )); } int bytesRead = 0; byte [] bufBytes = new byte [numBytes]; ReadFile(_hComm, bufBytes, numBytes, ref bytesRead, ref _ovlCommPort); byte [] outBytes = new byte [bytesRead]; Array.Copy(bufBytes, outBytes, bytesRead); return outBytes; } public void Write( byte [] writeBytes) { if (_hComm == InvalidHandleValue) { throw ( new Exception( "Comm Port Not Open" )); } WriteFile(_hComm, writeBytes, writeBytes.Length, ref _bytesCount, ref _ovlCommPort); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | public int Read( byte [] buffer, int offset, int count) { if (! this .IsOpen) { throw new InvalidOperationException(SR.GetString( "Port_not_open" )); } if (buffer == null ) { throw new ArgumentNullException( "buffer" , SR.GetString( "ArgumentNull_Buffer" )); } if (offset < 0) { throw new ArgumentOutOfRangeException( "offset" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if (count < 0) { throw new ArgumentOutOfRangeException( "count" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if ((buffer.Length - offset) < count) { throw new ArgumentException(SR.GetString( "Argument_InvalidOffLen" )); } int num = 0; if ( this .CachedBytesToRead >= 1) { num = Math.Min( this .CachedBytesToRead, count); Buffer.BlockCopy( this .inBuffer, this .readPos, buffer, offset, num); this .readPos += num; if (num == count) { if ( this .readPos == this .readLen) { this .readPos = this .readLen = 0; } return count; } if ( this .BytesToRead == 0) { return num; } } this .readLen = this .readPos = 0; int num2 = count - num; num += this .internalSerialStream.Read(buffer, offset + num, num2); this .decoder.Reset(); return num; } public int Read( char [] buffer, int offset, int count) { if (! this .IsOpen) { throw new InvalidOperationException(SR.GetString( "Port_not_open" )); } if (buffer == null ) { throw new ArgumentNullException( "buffer" , SR.GetString( "ArgumentNull_Buffer" )); } if (offset < 0) { throw new ArgumentOutOfRangeException( "offset" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if (count < 0) { throw new ArgumentOutOfRangeException( "count" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if ((buffer.Length - offset) < count) { throw new ArgumentException(SR.GetString( "Argument_InvalidOffLen" )); } return this .InternalRead(buffer, offset, count, this .readTimeout, false ); } public void Write( string text) { if (! this .IsOpen) { throw new InvalidOperationException(SR.GetString( "Port_not_open" )); } if (text == null ) { throw new ArgumentNullException( "text" ); } if (text.Length != 0) { byte [] bytes = this .encoding.GetBytes(text); this .internalSerialStream.Write(bytes, 0, bytes.Length, this .writeTimeout); } } public void Write( byte [] buffer, int offset, int count) { if (! this .IsOpen) { throw new InvalidOperationException(SR.GetString( "Port_not_open" )); } if (buffer == null ) { throw new ArgumentNullException( "buffer" , SR.GetString( "ArgumentNull_Buffer" )); } if (offset < 0) { throw new ArgumentOutOfRangeException( "offset" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if (count < 0) { throw new ArgumentOutOfRangeException( "count" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if ((buffer.Length - offset) < count) { throw new ArgumentException(SR.GetString( "Argument_InvalidOffLen" )); } if (buffer.Length != 0) { this .internalSerialStream.Write(buffer, offset, count, this .writeTimeout); } } public void Write( char [] buffer, int offset, int count) { if (! this .IsOpen) { throw new InvalidOperationException(SR.GetString( "Port_not_open" )); } if (buffer == null ) { throw new ArgumentNullException( "buffer" ); } if (offset < 0) { throw new ArgumentOutOfRangeException( "offset" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if (count < 0) { throw new ArgumentOutOfRangeException( "count" , SR.GetString( "ArgumentOutOfRange_NeedNonNegNumRequired" )); } if ((buffer.Length - offset) < count) { throw new ArgumentException(SR.GetString( "Argument_InvalidOffLen" )); } if (buffer.Length != 0) { byte [] buffer2 = this .Encoding.GetBytes(buffer, offset, count); this .Write(buffer2, 0, buffer2.Length); } } public void WriteLine( string text) { this .Write(text + this .NewLine); } |
对比可以得出我那个读写方法没有SerialPort那么多,我那里只提供了byte[]的读写,这是相对来说比较不够用的,不过这个不是问题。问题是我那里读写时的异常判断太少,只判断了串口关闭的情况,这样有些异常就不能在收发之前被判断出来了。
总结来说就是自己写的代码面向对象得还不够,代码不够规范,考虑的不够全面等,希望以后可以学着向“SerialPort”看齐!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端