目 錄
第五章 串口和網絡統一IO設計... 2
5.1 統一IO接口... 2
5.1.1 串口IO.. 4
5.1.2 網絡IO.. 7
5.1.3 擴展應用... 12
5.2 IO管理器... 12
5.2.1 串口I O管理器... 13
5.2.2 網絡IO管理器... 15
5.2.2.1 網絡偵聽... 16
5.2.2.2 連接遠程服務器... 17
5.2.2.3 互斥操作... 18
5.3 小結... 19
作為通訊框架平台軟件,IO是核心部分之一,涉及到與硬件設備、軟件之間的信息數據交互,主要包括兩部分:IO實例與IO管理器。IO實例負責直接對串口和網絡進行操作;IO管理器負責對IO實例進行管理。
受應用環境的影響,IO操作過程中的確出現過一些問題,有些問題的解決也費了好長時間。並不是解決問題有多困難,而是無法確定到底是什麼原因引起的。經過不斷的完善,IO部分才逐漸穩定下來。
框架平台一大特點就是開發一套設備驅動(插件)同時支持串口和網絡兩種通訊方式,而兩種通訊方式的切換只需要改動配制文件。
不同的設備類型和協議、不同的通訊方式,用堆代碼的方式進行開發,根本無法適應不同場景的應用,提高了代碼的維護成本,以及修改代碼可能造成潛在的BUG,是讓人很頭疼的一件事。
在開始設計框架平台的時候,一個核心的思想就是把變的東西要設計靈活,把不變的東西設計穩定。對於設備的協議就是變的東西,對於IO部分就是相對不變的東西,那就需要對串口IO和網絡IO進行整合。不僅在代碼層面要運行穩定;在邏輯層面,不管是串口IO還是網絡IO在框架內部是統一的接口,所有對IO的操作都會通過這個統一的接口來完成。
統一的IO接口代碼如下:
public interface IIOChannel:IDisposable { /// <summary> /// 同步鎖 /// </summary> object SyncLock { get; } /// <summary> /// IO關鍵字,如果是串口通訊為串口號,如:COM1;如果是網絡通訊為IP和端口,例如:127.0.0.1:1234 /// </summary> string Key { get; } /// <summary> /// IO通道,可以是COM,也可以是SOCKET /// </summary> object IO{get;} /// <summary> /// 讀IO; /// </summary> /// <returns></returns> byte[] ReadIO(); /// <summary> /// 寫IO /// </summary> int WriteIO(byte[] data); /// <summary> /// 關閉 /// </summary> void Close(); /// <summary> /// IO類型 /// </summary> CommunicationType IOType { get; } /// <summary> /// 是否被釋放了 /// </summary> bool IsDisposed { get; } }
串口IO和網絡IO都繼承自IIOChannel接口,完成特定的IO通訊操作。繼承關系圖如下:
原來串口IO操作使用是的MS自帶的SerialPort組件,但是這個組件與一些小眾工業串口卡不兼容,操作的時候出現異常"參數不正確"的提示。SerialPort組件本身是對Win32 API的封裝,所以分析應該不是這個組件本身的問題。有網友反饋,如下圖:
但是,從解決問題的成本角度來考慮,從軟件著手解決是成本最低的、效率最高的。基於這方面的考慮,使用MOXA公司的PCOMM.DLL組件進行開發,並沒有出現類似的問題。所以,在代碼重構中使用了PCOMM.DLL組件,並且運行一直很穩定。
針對串口IO操作比較簡單,主要是實現了ReadIO和WriteIO兩個接口,代碼如下:
public class SessionCom : ISessionCom { ...... public byte[] ReadIO() { if (_ReceiveBuffer != null) { int num = InternalRead(_ReceiveBuffer, 0, _ReceiveBuffer.Length); if (num > 0) { byte[] data = new byte[num]; Buffer.BlockCopy(_ReceiveBuffer, 0, data, 0, data.Length); return data; } else { return new byte[] { }; } } else { return new byte[] { }; } } public int WriteIO(byte[] data) { int sendBufferSize = GlobalProperty.GetInstance().ComSendBufferSize; if (data.Length <= sendBufferSize) { return this.InternalWrite(data); } else { int successNum = 0; int num = 0; while (num < data.Length) { int remainLength = data.Length - num; int sendLength = remainLength >= sendBufferSize ? sendBufferSize : remainLength; successNum += InternalWrite(data, num, sendLength); num += sendLength; } return successNum; } } ...... }
針對ReadIO接口函數,可以有多種操作方式,例如:讀固定長度、判斷結尾字符、一直讀到IO緩存為空等。讀固定長度,如果偶爾出現通訊干擾或丟失數據,這種方式會給後續正確讀取數據造成影響;判斷結尾字符,在框架內部的IO實現上又無法做到通用性;一直讀到IO緩存為空,如果接收數據的頻率大於從IO緩存讀取的頻率,那麼會阻塞輪詢調度線程。基於多方面的考慮,現場環境往往比想象的要復雜,在設置讀超時的基礎上,讀一次就返回了。
還要考慮到現場實際的應用環境,例如:USB形式的串口容易松動,造成不穩定;9針串口損壞等情況。所以,有可能因為硬件環境改變引起無法正常對IO進行操作,這時候會通過TryOpen接口函數試著重新打開串口IO;另外,串口參數發生改變時,通過IOSettings接口函數重新配置參數。
網絡IO通訊的本質是對Socket進行操作,框架平台現在支持TCP方式進行通訊;工作模塊支持Server和Client兩種,也就是開發一套設備驅動可以支持Tcp Server和Tcp Client兩種數據交互方式。現在不支持UDP通訊方式,將會在後續進行完善。
發送和接收的代碼實現比較簡單,SessionSocket類中的ReadIO和WriteIO是用同步方式實現的;當並發通訊和自控通訊模式時,接收數據是用異步方式來完成的。當然,也可以使用完全的異步編程方式,使用SocketAsyncEventArgs操作類。SessionSocket操作代碼實現如下:
public class SessionSocket : ISessionSocket { public byte[] ReadIO() { if (!this.IsDisposed) { if (this.AcceptedSocket.Connected) { if (this.AcceptedSocket.Poll(10, SelectMode.SelectRead)) { if (this.AcceptedSocket.Available > this.AcceptedSocket.ReceiveBufferSize) { throw new Exception("接收的數據大於設置的接收緩沖區大小"); } #region int num = this.AcceptedSocket.Receive(this._ReceiveBuffer, 0, this._ReceiveBuffer.Length, SocketFlags.None); if (num <= 0) { throw new SocketException((int)SocketError.HostDown); } else { this._NoneDataCount = 0; byte[] data = new byte[num]; Buffer.BlockCopy(_ReceiveBuffer, 0, data, 0, data.Length); return data; } #endregion } else { this._NoneDataCount++; if (this._NoneDataCount >= 60) { this._NoneDataCount = 0; throw new SocketException((int)SocketError.HostDown); } else { return new byte[] { }; } } } else { throw new SocketException((int)SocketError.HostDown); } } else { return new byte[] { }; } } public int WriteIO(byte[] data) { if (!this.IsDisposed) { if (this.AcceptedSocket.Connected && this.AcceptedSocket.Poll(10, SelectMode.SelectWrite)) { int successNum = 0; int num = 0; while (num < data.Length) { int remainLength = data.Length - num; int sendLength = remainLength >= this.AcceptedSocket.SendBufferSize ? this.AcceptedSocket.SendBufferSize : remainLength; SocketError error; successNum += this.AcceptedSocket.Send(data, num, sendLength, SocketFlags.None, out error); num += sendLength; if (successNum <= 0 || error != SocketError.Success) { throw new SocketException((int)SocketError.HostDown); } } return successNum; } else { throw new SocketException((int)SocketError.HostDown); } } else { return 0; } } }
ReadIO和WriteIO在操作過程中發生Socket失敗後會拋出SocketException異常,框架平台捕捉異常後會對IO實例進行資源銷毀。重新被動偵聽或主動連接獲得Socket實例。
考慮到硬件,由PC機的網卡引起的網絡IO操作異常的可能比較小;但是,要考慮到連接到框架平台的各類終端(客戶端)硬件設備,例如:DTU、無線路由、網絡轉換模塊等;還涉及到通訊鏈路,例如:GPRS、2G/3G/4G等;不同的硬件特性、不同的通訊鏈路,多種原因可能會造成通訊鏈路失效,例如:另外一端的程序不穩定、無法釋放資源等原因導致數據無法正常發送和接收;線路接頭虛接導致鏈路時好時壞導致發送和接收數據不穩定;網絡本身的原因出現Socket“假”連接的現象導致顯示發送數據成功,而另一端卻沒有收到等等。
針對Socket通訊,原來在線程裡定時輪詢IO實例,通過IO實例向另一端發送心跳檢測數據,如果發送失敗,立即釋放IO資源,這種操作方式的缺點是另一端會接收到一些冗余數據信息。重構時改變為另一種方式,對底層進行心跳在線檢測,當進行異步發送和接收數據的時候,如果鏈路出現問題,異步函數會立即返回,並返回結果顯示發送和接收0個數,對此進行判斷而銷毀IO實例資源。在初始化IO實例的時候,增加了對底層心跳檢測功能,代碼如下:
public SessionSocket(Socket socket) { uint dummy = 0; _KeepAliveOptionValues = new byte[Marshal.SizeOf(dummy) * 3]; _KeepAliveOptionOutValues = new byte[_KeepAliveOptionValues.Length]; BitConverter.GetBytes((uint)1).CopyTo(_KeepAliveOptionValues, 0); BitConverter.GetBytes((uint)(2000)).CopyTo(_KeepAliveOptionValues, Marshal.SizeOf(dummy)); BitConverter.GetBytes((uint)(GlobalProperty.GetInstance().HeartPacketInterval)).CopyTo(_KeepAliveOptionValues, Marshal.SizeOf(dummy) * 2); socket.IOControl(IOControlCode.KeepAliveValues, _KeepAliveOptionValues, _KeepAliveOptionOutValues); socket.NoDelay = true; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); ...... }
通過發送、接收拋出異常和底層心跳檢測兩種方式對Socket IO實例有效性進行檢測。對於正常通訊情況下的發送和接收操作很簡單,但是也要通過技術手段防止各種意外情況,從而影響框架平台運行的穩定性。
對於通訊說簡單也簡單,說難也難,因應用場景和環境的原因難易程度不一樣。在網絡世界發展如火如荼的今天,網絡任務調度、分布式消息、大數據處理等無不涉及到多點與多點之間的信息交互,所以在通訊基礎上又發展出來各種協議、各種算法以及數據校驗等。
把IO設計穩定,但是不代表沒有擴展的余地。在《3.設備驅動的設計》的“3.7 IO數據交互設計”中介紹了具體的應用。在調用IRunDevice設備驅動的Send和Receive接口時會把IO實例以參數的形式傳遞進來,在二次開發過程中可以重寫這兩個函數,開發特定的發送和接收業務。
有網友問:串口通訊時,硬件設備一直在向軟件發送數據,軟件分析接收到的數據後進行數據處理,用SuperIO應該怎麼實現?
這種單向通訊方式也是存在的,框架設計前已經考慮到這類情況,具體實現步驟如下:
IO管理器是對串口IO和網絡IO實例進行管理,他們都繼承自IIOChannelManager接口,但是各自的IO管理器的職能又有很大不同,網絡IO管理器更復雜一些。繼承關系結構圖如下:
相對簡單的多,因為串口IO動態改變的幾率比較小,只是創建IO和關閉IO時通過事件反饋到串口監視窗體,主要代碼如下:
public class SessionComManager : IOChannelManager,ISessionComManager<string, IIOChannel> { ...... /// <summary> /// 建立並打開串口IO /// </summary> /// <param name="port"></param> /// <param name="baud"></param> /// <returns></returns> public ISessionCom BuildAndOpenComIO(int port, int baud) { ISessionCom com = new SessionCom(port, baud); com.TryOpen(); if (COMOpen != null) { bool openSuccess = false; if (com.IsOpen) { openSuccess = true; } else { openSuccess = false; } COMOpenArgs args = new COMOpenArgs(port, baud, openSuccess); this.COMOpen(com, args); } return com; } /// <summary> /// 閉關IO /// </summary> /// <param name="key"></param> public override void CloseIO(string key) { ISessionCom com = (ISessionCom)this.GetIO(key); base.CloseIO(key); if (COMClose != null) { bool closeSuccess = false; if (com.IsOpen) { closeSuccess = false; } else { closeSuccess = true; } COMCloseArgs args = new COMCloseArgs(com.Port, com.Baud, closeSuccess); this.COMClose(com, args); } } ...... }
網絡IO管理器相對復雜一些,涉及到Socket的動態連接和斷開,以及根據設備驅動設置的工作模式(Server或Client)切換對連接的處理方式。原來的時候,還負責通過線程定時對所有網絡IO實例進行心跳檢測,現在這部分被底層心跳檢測所替代。
當偵聽並接收到遠程的連接實例後,會做兩件事:
2.判斷當前IO管理器是否存在相同的IP實例對象,如果存在,那麼則銷毀該IP實例對象。因為有可能這個實例對象已失效,至少認為遠程的客戶端認為當前的連接已經失效。所以,既然這樣,我們雙方達成共識,果斷銷毀這樣的IP實例對象,接收新的IP連接實例。
接收連接實例對象的代碼如下:
private void Monitor_SocketHanler(object source, AcceptSocketArgs e) { IRunDevice[] devs = DeviceManager.GetInstance().GetDevices(e.RemoteIP, WorkMode.TcpClient); if (devs.Length > 0) { DeviceMonitorLog.WriteLog(String.Format("有設備設置{0}為Tcp Client模式,此IP不支持遠程主動連接", e.RemoteIP)); SessionSocket.CloseSocket(e.Socket); return; } CheckSameSessionSocket(e.RemoteIP); _ManualEvent.WaitOne(); //如果正在結束SOCKET操作,等待完成後再執行邊接操作 ISessionSocket socket = new SessionSocket(e.Socket); SessionSocketConnect(socket); }
單獨開辟一個線程,獲得所有工作模式為Client的設備驅動,並檢測每一個設備驅動的通訊參數在IO管理器中是否存在相應的IO實例,如果不存在,那麼則主動連接遠程的服務器,連接成功後把連接的IO實例入到IO管理器。
實現的代碼如下:
private void ConnectTarget() { while (true) { if (!_ConnectThreadRun) { break; } IRunDevice[] devList = DeviceManager.GetInstance().GetDevices(WorkMode.TcpClient); for (int i = 0; i < devList.Length; i++) { try { if (!this.ContainIO(devList[i].DeviceParameter.NET.RemoteIP)) { ConnectServer(devList[i].DeviceParameter.NET.RemoteIP, devList[i].DeviceParameter.NET.RemotePort); } } catch (Exception ex) { devList[i].OnDeviceRuningLogHandler(ex.Message); } } System.Threading.Thread.Sleep(2000); } }
當有新的連接,在檢測是否有相同IP實例存在的時候,如果有相同IP實例存在,在銷毀資源未結束之前,不能把新連接的IP實例放到IO管理器。因為相同IP的兩個實例,一個在銷毀資源、一個在創建資源,有可能把新連接的IP實例一起銷毀掉。
防止這種情況的出現,使用ManualResetEvent信號互斥進行狀態控制和改變,示意代碼如下:
public class SessionSocketManager : IOChannelManager, ISessionSocketManager<string, IIOChannel> { /// <summary> /// 初始狀態為終止狀態 /// </summary> private ManualResetEvent _ManualEvent = new ManualResetEvent(true); private void Monitor_SocketHanler(object source, AcceptSocketArgs e) { SessionSocketClose(e.RemoteIP); _ManualEvent.WaitOne(); //如果正在結束SOCKET操作,等待完成後再執行邊接操作 ISessionSocket socket = new SessionSocket(e.Socket); SessionSocketConnect(socket); } private void SessionSocketClose(string key) { this._ManualEvent.Reset(); //為非終止狀態 SessionSocket io = (SessionSocket)GetIO(key); if (io != null) { CloseIO(key); } this._ManualEvent.Set();//為終止狀態 } private void SessionSocketConnect(ISessionSocket socket) { if (!this.ContainIO(socket.Key.ToString())) { this.AddIO(socket.Key.ToString(), (IIOChannel)socket); } } }
IO這塊的設計的思想是一個負責執行一個負責管理,IO實例是具體通道操作,IO管理器負責對IO進行管理,並協調設備和IO之間的關系和工作。
作者:唯笑志在
Email:[email protected]
QQ:504547114
.NET開發技術聯盟:54256083
文檔下載:http://pan.baidu.com/s/1pJ7lZWf
官方網址:http://www.bmpj.net