[连载]《C#通讯(串口和网络)框架的设计与实现》- 5.串口和网络统一IO设计

简介: 目       录 第五章           串口和网络统一IO设计... 2 5.1           统一IO接口... 2 5.1.1    串口IO.. 4 5.1.2    网络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通讯操作。继承关系图如下:

 

5.1.1    串口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接口函数重新配置参数。

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管理器

   

     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:504547114@qq.com

QQ:504547114

.NET开发技术联盟:54256083

文档下载:http://pan.baidu.com/s/1pJ7lZWf

官方网址:http://www.bmpj.net

相关文章
|
18天前
|
数据采集 存储 JSON
Python网络爬虫:Scrapy框架的实战应用与技巧分享
【10月更文挑战第27天】本文介绍了Python网络爬虫Scrapy框架的实战应用与技巧。首先讲解了如何创建Scrapy项目、定义爬虫、处理JSON响应、设置User-Agent和代理,以及存储爬取的数据。通过具体示例,帮助读者掌握Scrapy的核心功能和使用方法,提升数据采集效率。
60 6
|
26天前
|
机器学习/深度学习 人工智能
类人神经网络再进一步!DeepMind最新50页论文提出AligNet框架:用层次化视觉概念对齐人类
【10月更文挑战第18天】这篇论文提出了一种名为AligNet的框架,旨在通过将人类知识注入神经网络来解决其与人类认知的不匹配问题。AligNet通过训练教师模型模仿人类判断,并将人类化的结构和知识转移至预训练的视觉模型中,从而提高模型在多种任务上的泛化能力和稳健性。实验结果表明,人类对齐的模型在相似性任务和出分布情况下表现更佳。
57 3
|
11天前
|
存储 安全 网络安全
网络安全法律框架:全球视角下的合规性分析
网络安全法律框架:全球视角下的合规性分析
24 1
|
19天前
|
数据采集 前端开发 中间件
Python网络爬虫:Scrapy框架的实战应用与技巧分享
【10月更文挑战第26天】Python是一种强大的编程语言,在数据抓取和网络爬虫领域应用广泛。Scrapy作为高效灵活的爬虫框架,为开发者提供了强大的工具集。本文通过实战案例,详细解析Scrapy框架的应用与技巧,并附上示例代码。文章介绍了Scrapy的基本概念、创建项目、编写简单爬虫、高级特性和技巧等内容。
45 4
|
19天前
|
网络协议 物联网 API
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第26天】Python 是一门功能强大且易于学习的编程语言,Twisted 框架以其事件驱动和异步IO处理能力,在网络编程领域独树一帜。本文深入探讨 Twisted 的异步IO机制,并通过实战示例展示其强大功能。示例包括创建简单HTTP服务器,展示如何高效处理大量并发连接。
39 1
|
20天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
192 2
|
7天前
|
网络协议 Unix Linux
精选2款C#/.NET开源且功能强大的网络通信框架
精选2款C#/.NET开源且功能强大的网络通信框架
|
7天前
|
网络协议 网络安全 Apache
一个整合性、功能丰富的.NET网络通信框架
一个整合性、功能丰富的.NET网络通信框架
|
18天前
|
网络协议 调度 开发者
Python网络编程:Twisted框架的异步IO处理与实战
【10月更文挑战第27天】本文介绍了Python网络编程中的Twisted框架,重点讲解了其异步IO处理机制。通过反应器模式,Twisted能够在单线程中高效处理多个网络连接。文章提供了两个实战示例:一个简单的Echo服务器和一个HTTP服务器,展示了Twisted的强大功能和灵活性。
29 0
|
6天前
|
存储 SQL 安全
网络安全与信息安全:关于网络安全漏洞、加密技术、安全意识等方面的知识分享
【10月更文挑战第39天】在数字化时代,网络安全和信息安全成为了我们生活中不可或缺的一部分。本文将介绍网络安全漏洞、加密技术和安全意识等方面的内容,帮助读者更好地了解网络安全的重要性,并提供一些实用的技巧和方法来保护自己的信息安全。
19 2