using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Net; using System.Threading; using MECF.Framework.RT.Core; using MECF.Framework.RT.Core.IoProviders.Siemens; using MECF.Framework.RT.Core.IoProviders.Siemens.IMessage; using MECF.Framework.RT.Core.IoProviders.Siemens.Transfer; using MECF.Framework.RT.Core.ThreadLock; using MECF.Framework.RT.Core.IoProviders.Siemens.Net.StateOne; namespace MECF.Framework.RT.Core.IoProviders.Siemens.Net.NetworkBase { /// /// 支持长连接,短连接两个模式的通用客户端基类 -> /// Universal client base class that supports long connections and short connections to two modes /// /// /// 无,请使用继承类实例化,然后进行数据交互。 /// public class NetworkDoubleBase : NetworkBase where TNetMessage : INetMessage, new() where TTransform : IByteTransform, new() { #region Constructor /// /// 默认的无参构造函数 -> Default no-parameter constructor /// public NetworkDoubleBase( ) { ByteTransform = new TTransform( ); // 实例化变换类的对象 InteractiveLock = new SimpleHybirdLock( ); // 实例化数据访问锁 connectionId = SoftBasic.GetUniqueStringByGuidAndRandom( ); // 设备的唯一的编号 } #endregion #region Private Member private TTransform byteTransform; // 数据变换的接口 private string ipAddress = "127.0.0.1"; // 连接的IP地址 private int port = 10000; // 端口号 private int connectTimeOut = 10000; // 连接超时时间设置 private int receiveTimeOut = 10000; // 数据接收的超时时间 private bool isPersistentConn = false; // 是否处于长连接的状态 private SimpleHybirdLock InteractiveLock; // 一次正常的交互的互斥锁 private bool IsSocketError = false; // 指示长连接的套接字是否处于错误的状态 private bool isUseSpecifiedSocket = false; // 指示是否使用指定的网络套接字访问数据 private string connectionId = string.Empty; // 当前连接 #endregion #region Public Member /// /// 当前客户端的数据变换机制,当你需要从字节数据转换类型数据的时候需要。-> /// The current client's data transformation mechanism is required when you need to convert type data from byte data. /// /// /// 主要是用来转换数据类型的,下面仅仅演示了2个方法,其他的类型转换,类似处理。 /// /// public TTransform ByteTransform { get { return byteTransform; } set { byteTransform = value; } } /// /// 获取或设置连接的超时时间,单位是毫秒 -> Gets or sets the timeout for the connection, in milliseconds /// /// /// 设置1秒的超时的示例 /// /// /// /// 不适用于异形模式的连接。 /// public int ConnectTimeOut { get{return connectTimeOut;} set { if (value >= 0) connectTimeOut = value; } } /// /// 获取或设置接收服务器反馈的时间,如果为负数,则不接收反馈 -> /// Gets or sets the time to receive server feedback, and if it is a negative number, does not receive feedback /// /// /// 设置1秒的接收超时的示例 /// /// /// /// 超时的通常原因是服务器端没有配置好,导致访问失败,为了不卡死软件,所以有了这个超时的属性。 /// public int ReceiveTimeOut { get { return receiveTimeOut; } set { receiveTimeOut = value; } } /// /// 获取或是设置服务器的IP地址 /// /// /// 最好实在初始化的时候进行指定,当使用短连接的时候,支持动态更改,切换;当使用长连接后,无法动态更改 /// /// /// 以下举例modbus-tcp的短连接及动态更改ip地址的示例 /// /// public virtual string IpAddress { get { return ipAddress; } set { if (!string.IsNullOrEmpty( value )) { if (!IPAddress.TryParse( value, out IPAddress address )) { throw new Exception( StringResources.Language.IpAddresError ); } ipAddress = value; } else { ipAddress = "127.0.0.1"; } } } /// /// 获取或设置服务器的端口号 /// /// /// 最好实在初始化的时候进行指定,当使用短连接的时候,支持动态更改,切换;当使用长连接后,无法动态更改 /// /// /// 动态更改请参照IpAddress属性的更改。 /// public virtual int Port { get { return port; } set { port = value; } } /// /// 当前连接的唯一ID号,默认为长度20的guid码加随机数组成,方便列表管理,也可以自己指定 /// /// /// Current Connection ID, conclude guid and random data, also, you can spcified /// public string ConnectionId { get { return connectionId; } set { connectionId = value; } } /// /// 当前的异形连接对象,如果设置了异形连接的话 /// /// /// 具体的使用方法请参照Demo项目中的异形modbus实现。 /// public AlienSession AlienSession { get; set; } #endregion #region Public Method /// /// 在读取数据之前可以调用本方法将客户端设置为长连接模式,相当于跳过了ConnectServer的结果验证,对异形客户端无效 /// /// /// 以下的方式演示了另一种长连接的机制 /// /// public void SetPersistentConnection( ) { isPersistentConn = true; } #endregion #region Connect Close /// /// 切换短连接模式到长连接模式,后面的每次请求都共享一个通道 /// /// 返回连接结果,如果失败的话(也即IsSuccess为False),包含失败信息 /// /// 简单的连接示例,调用该方法后,连接设备,创建一个长连接的对象,后续的读写操作均公用一个连接对象。 /// /// 如果想知道是否连接成功,请参照下面的代码。 /// /// public OperateResult ConnectServer( ) { isPersistentConn = true; OperateResult result = new OperateResult( ); // 重新连接之前,先将旧的数据进行清空 CoreSocket?.Close( ); OperateResult rSocket = CreateSocketAndInitialication( ); if (!rSocket.IsSuccess) { IsSocketError = true; rSocket.Content = null; result.Message = rSocket.Message; } else { CoreSocket = rSocket.Content; result.IsSuccess = true; } return result; } /// /// 使用指定的套接字创建异形客户端 /// /// 异形客户端对象,查看类型创建的客户端 /// 通常都为成功 /// /// 简单的创建示例。 /// /// 如果想知道是否创建成功。通常都是成功。 /// /// /// /// 不能和之前的长连接和短连接混用,详细参考 Demo程序 /// public OperateResult ConnectServer( AlienSession session ) { isPersistentConn = true; isUseSpecifiedSocket = true; if (session != null) { AlienSession?.Socket?.Close( ); if (string.IsNullOrEmpty( ConnectionId )) { ConnectionId = session.DTU; } if (ConnectionId == session.DTU) { CoreSocket = session.Socket; IsSocketError = false; AlienSession = session; return InitializationOnConnect( session.Socket ); } else { IsSocketError = true; return new OperateResult( ); } } else { IsSocketError = true; return new OperateResult( ); } } /// /// 在长连接模式下,断开服务器的连接,并切换到短连接模式 /// /// 关闭连接,不需要查看IsSuccess属性查看 /// /// 直接关闭连接即可,基本上是不需要进行成功的判定 /// /// public OperateResult ConnectClose( ) { OperateResult result = new OperateResult( ); isPersistentConn = false; InteractiveLock.Enter( ); // 额外操作 result = ExtraOnDisconnect( CoreSocket ); // 关闭信息 CoreSocket?.Close( ); CoreSocket = null; InteractiveLock.Leave( ); return result; } #endregion #region Initialization And Extra /// /// 连接上服务器后需要进行的初始化操作 /// /// 网络套接字 /// 是否初始化成功,依据具体的协议进行重写 /// /// 有些协议不需要握手信号,比如三菱的MC协议,Modbus协议,西门子和欧姆龙就存在握手信息,此处的例子是继承本类后重写的西门子的协议示例 /// /// protected virtual OperateResult InitializationOnConnect( Socket socket ) { return OperateResult.CreateSuccessResult( ); } /// /// 在将要和服务器进行断开的情况下额外的操作,需要根据对应协议进行重写 /// /// 网络套接字 /// /// 目前暂无相关的示例,组件支持的协议都不用实现这个方法。 /// /// 当断开连接时额外的操作结果 protected virtual OperateResult ExtraOnDisconnect( Socket socket ) { return OperateResult.CreateSuccessResult( ); } #endregion #region Core Communication /*************************************************************************************** * * 主要的数据交互分为4步 * 1. 连接服务器,或是获取到旧的使用的网络信息 * 2. 发送数据信息 * 3. 接收反馈的数据信息 * 4. 关闭网络连接,如果是短连接的话 * **************************************************************************************/ /// /// 获取本次操作的可用的网络套接字 /// /// 是否成功,如果成功,使用这个套接字 private OperateResult GetAvailableSocket( ) { if (isPersistentConn) { // 如果是异形模式 if (isUseSpecifiedSocket) { if(IsSocketError) { return new OperateResult( StringResources.Language.ConnectionIsNotAvailable ); } else { return OperateResult.CreateSuccessResult( CoreSocket ); } } else { // 长连接模式 if (IsSocketError || CoreSocket == null) { OperateResult connect = ConnectServer( ); if (!connect.IsSuccess) { IsSocketError = true; return OperateResult.CreateFailedResult( connect ); } else { IsSocketError = false; return OperateResult.CreateSuccessResult( CoreSocket ); } } else { return OperateResult.CreateSuccessResult( CoreSocket ); } } } else { // 短连接模式 return CreateSocketAndInitialication( ); } } /// /// 连接并初始化网络套接字 /// /// 带有socket的结果对象 private OperateResult CreateSocketAndInitialication( ) { OperateResult result = CreateSocketAndConnect( new IPEndPoint( IPAddress.Parse( ipAddress ), port ), connectTimeOut ); if (result.IsSuccess) { // 初始化 OperateResult initi = InitializationOnConnect( result.Content ); if (!initi.IsSuccess) { result.Content?.Close( ); result.IsSuccess = initi.IsSuccess; result.CopyErrorFromOther( initi ); } } return result; } /// /// 指示如何创建一个新的消息对象 /// /// 消息对象 protected virtual INetMessage GetNewNetMessage() { return null; } /// /// 在其他指定的套接字上,使用报文来通讯,传入需要发送的消息,返回一条完整的数据指令 /// /// 指定的套接字 /// 发送的完整的报文信息 /// /// 无锁的基于套接字直接进行叠加协议的操作。 /// /// /// 假设你有一个自己的socket连接了设备,本组件可以直接基于该socket实现modbus读取,三菱读取,西门子读取等等操作,前提是该服务器支持多协议,虽然这个需求听上去比较变态,但本组件支持这样的操作。 /// /// /// 接收的完整的报文信息 public virtual OperateResult ReadFromCoreServer( Socket socket, byte[] send ) { TNetMessage netMessage = new TNetMessage( ); netMessage.SendBytes = send; // send OperateResult sendResult = Send( socket, send ); if (!sendResult.IsSuccess) { socket?.Close( ); return OperateResult.CreateFailedResult( sendResult ); } if (receiveTimeOut < 0) return OperateResult.CreateSuccessResult( new byte[0] ); // receive msg OperateResult resultReceive = ReceiveByMessage( socket, receiveTimeOut, netMessage ); if (!resultReceive.IsSuccess) { socket?.Close( ); return new OperateResult( StringResources.Language.ReceiveDataTimeout + receiveTimeOut ); } // check if (!netMessage.CheckHeadBytesLegal( Token.ToByteArray( ) )) { socket?.Close( ); return new OperateResult( StringResources.Language.CommandHeadCodeCheckFailed ); } // Success return OperateResult.CreateSuccessResult( resultReceive.Content ); } /// /// 使用底层的数据报文来通讯,传入需要发送的消息,返回一条完整的数据指令 /// /// 发送的完整的报文信息 /// 接收的完整的报文信息 /// /// 本方法用于实现本组件还未实现的一些报文功能,例如有些modbus服务器会有一些特殊的功能码支持,需要收发特殊的报文,详细请看示例 /// /// /// 此处举例有个modbus服务器,有个特殊的功能码0x09,后面携带子数据0x01即可,发送字节为 0x00 0x00 0x00 0x00 0x00 0x03 0x01 0x09 0x01 /// /// public OperateResult ReadFromCoreServer( byte[] send ) { var result = new OperateResult( ); InteractiveLock.Enter( ); // 获取有用的网络通道,如果没有,就建立新的连接 OperateResult resultSocket = GetAvailableSocket( ); if (!resultSocket.IsSuccess) { IsSocketError = true; if (AlienSession != null) AlienSession.IsStatusOk = false; InteractiveLock.Leave( ); result.CopyErrorFromOther( resultSocket ); return result; } OperateResult read = ReadFromCoreServer( resultSocket.Content, send ); if (read.IsSuccess) { IsSocketError = false; result.IsSuccess = read.IsSuccess; result.Content = read.Content; result.Message = StringResources.Language.SuccessText; } else { IsSocketError = true; if (AlienSession != null) AlienSession.IsStatusOk = false; result.CopyErrorFromOther( read ); } InteractiveLock.Leave( ); if (!isPersistentConn) resultSocket.Content?.Close( ); return result; } #endregion #region Object Override /// /// 返回表示当前对象的字符串 /// /// 字符串信息 public override string ToString( ) { return $"NetworkDoubleBase<{typeof( TNetMessage )}, {typeof( TTransform )}>"; } #endregion } }