天天看點

[連載]《C#通訊(序列槽和網絡)架構的設計與實作》- 5.序列槽和網絡統一IO設計第五章     序列槽和網絡統一IO設計

目       錄

第五章           序列槽和網絡統一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操作過程中的确出現過一些問題,有些問題的解決也費了好長時間。并不是解決問題有多困難,而是無法确定到底是什麼原因引起的。經過不斷的完善,IO部分才逐漸穩定下來。

5.1    統一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通訊操作。繼承關系圖如下:

[連載]《C#通訊(序列槽和網絡)架構的設計與實作》- 5.序列槽和網絡統一IO設計第五章     序列槽和網絡統一IO設計

5.1.1    序列槽IO

     原來序列槽IO操作使用是的MS自帶的SerialPort元件,但是這個元件與一些小衆工業序列槽卡不相容,操作的時候出現異常"參數不正确"的提示。SerialPort元件本身是對Win32 API的封裝,是以分析應該不是這個元件本身的問題。有網友回報,如下圖:

[連載]《C#通訊(序列槽和網絡)架構的設計與實作》- 5.序列槽和網絡統一IO設計第五章     序列槽和網絡統一IO設計

     但是,從解決問題的成本角度來考慮,從軟體着手解決是成本最低的、效率最高的。基于這方面的考慮,使用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接口函數重新配置參數。

5.1.2    網絡IO

      網絡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執行個體有效性進行檢測。對于正常通訊情況下的發送和接收操作很簡單,但是也要通過技術手段防止各種意外情況,進而影響架構平台運作的穩定性。

對于通訊說簡單也簡單,說難也難,因應用場景和環境的原因難易程度不一樣。在網絡世界發展如火如荼的今天,網絡任務排程、分布式消息、大資料處理等無不涉及到多點與多點之間的資訊互動,是以在通訊基礎上又發展出來各種協定、各種算法以及資料校驗等。

5.1.3    擴充應用

     把IO設計穩定,但是不代表沒有擴充的餘地。在《3.裝置驅動的設計》的“3.7 IO資料互動設計”中介紹了具體的應用。在調用IRunDevice裝置驅動的Send和Receive接口時會把IO執行個體以參數的形式傳遞進來,在二次開發過程中可以重寫這兩個函數,開發特定的發送和接收業務。

    有網友問:序列槽通訊時,硬體裝置一直在向軟體發送資料,軟體分析接收到的資料後進行資料處理,用SuperIO應該怎麼實作?

    這種單向通訊方式也是存在的,架構設計前已經考慮到這類情況,具體實作步驟如下:

  1. 重寫IRunDevice裝置驅動中的Send接口函數,直接return傳回,不進行發送資料。
  2. 重寫IRunDevice裝置驅動中的Receive接口函數,把接收上來的資料入到緩存裡。
  3. 啟動IRunDevice裝置驅動中的IsStartTimer 的定時器,在DeviceTimer中定時分析緩存裡的資料并處理資料。
  4. 查到可用的資料,調用RunIODevice(byte[])驅動函數,其他的代碼不需要改動。

5.2    IO管理器

[連載]《C#通訊(序列槽和網絡)架構的設計與實作》- 5.序列槽和網絡統一IO設計第五章     序列槽和網絡統一IO設計

     IO管理器是對序列槽IO和網絡IO執行個體進行管理,他們都繼承自IIOChannelManager接口,但是各自的IO管理器的職能又有很大不同,網絡IO管理器更複雜一些。繼承關系結構圖如下:

5.2.1    序列槽I O管理器

     相對簡單的多,因為序列槽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);
              }
       }
       ......
}
      

5.2.2    網絡IO管理器

     網絡IO管理器相對複雜一些,涉及到Socket的動态連接配接和斷開,以及根據裝置驅動設定的工作模式(Server或Client)切換對連接配接的處理方式。原來的時候,還負責通過線程定時對所有網絡IO執行個體進行心跳檢測,現在這部分被底層心跳檢測所替代。

5.2.2.1     網絡偵聽

       當偵聽并接收到遠端的連接配接執行個體後,會做兩件事:

  1. 判斷該連接配接執行個體的IP在裝置管理器中的裝置驅動是否設定為Client工作模式,如果是的話,那麼則銷毀該資源執行個體,并退出目前事務。裝置驅動設定的IP參數和用戶端的IP參數一緻,但是兩端的工作模式又都為Client模式。也就是說在一個網絡記憶體在兩個相同的IP和相同的Client工作模式,又要讓他們之間進行通訊,這不符合C/S通訊的基本原理。是以,果斷拒絕這樣的連接配接并銷毀資源。

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

5.2.2.2     連接配接遠端伺服器

     單獨開辟一個線程,獲得所有工作模式為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);
       }
}
      

5.2.2.3     互斥操作

    當有新的連接配接,在檢測是否有相同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);
              }
       }
}
      

5.3    小結

     IO這塊的設計的思想是一個負責執行一個負責管理,IO執行個體是具體通道操作,IO管理器負責對IO進行管理,并協調裝置和IO之間的關系和工作。

作者:唯笑志在

Email:[email protected]

QQ:504547114

.NET開發技術聯盟:54256083

文檔下載下傳:

http://pan.baidu.com/s/1pJ7lZWf

官方網址:

http://www.bmpj.net