一切都是最好的安排,14年的6月份,开始把通用通信协议栈纳入一个构件层面的规划,到第一版颤颤兢兢下线,再到后续的项目应用,及面对灵活性的优化,到性能的优化,转眼间经历了有一年半的时间,到本月的TCPServer开始在项目中使用,基本上整个通信组件中的所有通信模块和协议栈都开始应用于产品和项目中了。
几次大的变更总结:
1、第一版,通用通信协议栈的规划设计阶段,在几年的工控行业应用中,做了很多面向设备的通信协议开发,但是在开发过程中,发现与设备交互因为设备厂家的协议不统一,造成了协议代码编写时,也及其的不统一,彼时需要一个通用的通信构件部分,能把大部分的通信封包解包的过程自动化,开发人员可以只关心具体的协议接口编写即可,至于数据包的调度、中转、分发、缓存、排队等等,都可以不去考虑,抱着这个目的,开始了最早的通用通信组件开发。
同时第一版还要解决架构设计问题,调度设计问题等,由于对TCP、UDP的会话连接,前期有过一定的准备,包括线程池、调度服务的设计都已做过,但没有统一到一起,初始架构也就开始慢慢拼凑了。
因为是要打造一款底层的通用通信组件,那就与具体的设备和上层业务无关。作为设备和业务数据的桥梁转化部分,就需要通过软件工程,进行剥离,软件设计的核心就是封装变化,如何能把变化抽象化,最终落到具体的实现上其实是要实现4个变化部分,业务系统的结构化数据,包括发给设备的请求数据、从设备接收后的应答数据,但是有了这两部分还不行,设备是没法识别内存中的结构化数据的,那还需要一部分转化,怎样从请求数据转化到符合设备通信的字节流数据,以及从设备反馈的字节流数据,转化为应答结构化数据,剩余其他的部分就全部是具体的通信相关了,可以封装起来了。整个通信部分,就可以分成两大部分,输入和输出,因此通信接口也就定义出来了。
同时围绕输入输出部分,有对应的状态反馈和回调,则引入了状态通知部分。
下面就是定义的通信接口,但是在第一版中因为想设计的太高大上,预估不足,曾经加了很多接口函数进去,后来发现通信就是一入一出两部分,围绕这个,把太多的会引起歧义的接口部分全部去掉了。
///
/// 通信接口
///
/// 请求数据类型
/// 应答数据类型
public interface ICommunication
where TRequest:BaseRequestData
where TResponse : BaseResponseData
{
///
/// 启动
/// 如果打开失败,会自动尝试关闭掉
///
void Open();
///
/// 是否开启
///
bool IsOpen { get; }
///
/// 请求发送
/// 不建议与异步请求交错调用,会引起线程冲突
///
/// 请求发送的数据
bool Request(TRequest request);
///
/// 异步请求数据,在其他线程上
/// 不建议与同步请求交错调用,会引起线程冲突
///
/// 请求发送的数据
/// 处理异常信息
bool TryASyncRequest(TRequest request,out Exception ex);
///
/// 请求数据,根据会话ID发送,
/// 一般用于并发处理场合,根据ID来确认具体的发送会话,
/// 用于服务监听应答场合
/// 不建议与异步请求交错调用,会引起线程冲突
///
/// 会话ID
/// 请求发送的数据
bool Request(string sessionID,TRequest request);
///
/// 广播数据,面向所有的连接,用于小连接数场合
///
/// 请求数据
void RequestBroadCast(TRequest requestData);
///
/// 发送请求完成事件,同步到应答处理线程上,用于发送请求与应答请求,在应答线程上进行复合处理
///
event RequestCompletedHandler OnRequestCompleted;
///
/// 数据应答
/// 异步线程上消息队列响应
///
event ResponseHandler OnResponseBuffer;
///
/// 异常信息响应
/// 异步线程上消息队列响应
///
event InfoHandler OnResponseInfo;
///
/// 操作状态
///
event ResponseHandler OnOperateStaus;
///
/// 会话构建后触发
///
event SessionHandler OnSessionAdded;
///
/// 会话从链表中移除后触发
///
event SessionHandler OnSessionRemoved;
///
/// 关闭
///
void Close();
///
/// 监听的地址和端口
///
string EndPort { get; set; }
}
接口是服务于应用的边界的,但是最终还是要转化到具体的实现,包括:TCP客户端长短连接、TCP 服务端并发模型中的长短连接、UDP通信、串口通信等,具体的通信是要按照接口规范实现的,同时还要实现连接会话管理、调度管理等,分析中发现会话管理、调度管理等,已经是与具体的通信方式无关了,因此再抽象一层,在一个通用的抽象类中实现调度相关,这里包括了连接池、会话池、调度池、缓存排队、数据包分发等一系列核心实现。
///
/// 通信基类
///
/// 请求数据类型
/// 应答数据类型
public abstract class BaseCommunication
: ICommunication
where TRequest : BaseRequestData
where TResponse : BaseResponseData
{
///
/// 响应处理消息队列,用于应答排队
///
private IMessageFIFO responseMessageFIFO = null;
///
/// 是否正在关闭
///
protected volatile bool onClosing = false;
///
/// 启动
/// 如果打开失败,会自动尝试关闭掉
///
public abstract void Open();
///
/// 是否开启
///
public bool IsOpen
{
get
{
if (responseMessageFIFO == null)
{
return false;
}
return responseMessageFIFO.IsOpened;
}
}
///
/// 关闭
///
public abstract void Close();
#region 请求写入数据
///
/// 请求发送的数据
///
/// 请求数据
public virtual bool Request(TRequest data)
{
throw new NotImplementedException(string.Format("{0}不支持Request的调用", this.GetType().FullName));
}
///
/// 请求数据,根据会话ID发送,
/// 一般用于并发处理场合,根据ID来确认具体的发送会话,
/// 用于服务监听应答场合
///
/// 会话ID
/// 请求发送的数据
public virtual bool Request(string sessionID, TRequest request)
{
throw new NotImplementedException(string.Format("{0}不支持Request的调用", this.GetType().FullName));
}
///
/// 异步请求数据,在其他线程上
///
/// 请求的数据
public virtual bool TryASyncRequest(TRequest request,out Exception ex)
{
ex=new Exception(string.Format("{0}不支持ASyncRequest的调用", this.GetType().FullName));
return false;
}
///
/// 请求广播发送数据
///
/// 请求数据
public virtual void RequestBroadCast(TRequest requestData)
{
throw new NotImplementedExcepti
具体的通信层实现部分:
///
/// 异步高性能TCP监听服务
///
/// 请求消息构建类型
/// 应答消息过滤器类型
/// 请求数据
/// 应答数据
public class TCPAsyncServerCommunication
: BaseCommunication,IDisposable
where TInputMessageFilter : BaseInPutMessageFilter
where TOutputMessageFilter : BaseOutPutMessageFilter
where TRequest : BaseRequestData
where TResponse : BaseResponseData
{
///
/// 用于向SocketAsyncEventArgs统一进行内存分配和管理
///
BufferManager BufferManager;
///
/// 用于限制最大的连接数量
///
SemaphoreSlim MaxConnectionsEnforcer;
///
/// 在同一时刻控制最大并发的发送数量,通过SocketAsyncEventArgs
///
SemaphoreSlim MaxSaeaSendEnforcer;
///
/// 在同一时刻控制最大并发的连接数量
///
SemaphoreSlim MaxAcceptOpsEnforcer;
///
/// 用于监听进入的连接
///
Socket ListenSocket;
///
/// 等待连接栈
///
SocketAsyncEventArgsPool AcceptPoolEventArgs;
///
/// 接收数据栈
///
SocketAsyncEventArgsPool RecivePoolEventArgs;
///
/// 发送栈
///
SocketAsyncEventArgsPool SendPoolEventArgs;
///
/// TCP连接地址
///
IPEndPoint end;
///
/// 网络连接的IP地址和端口号
///
string endPort;
///
/// 已建立的会话
///
private Dictionary> dicConnectSession = new Dictionary>();
///
/// 连接处理锁
///
private object acceptLock = new object();
///
/// 用于Socket监听的托管队列
///
private IMessageFIFO acceptFIFO = MessageFIFOFactory.Create(MessageFIFOType.IOCPMessageFIFO);
///
/// 监听信号量
///
private ManualResetEvent acceptWaitResetEvent = new ManualResetEvent(false);
///
/// 通信端口初始化参数
///
protected TCPAsyncServerParameters communicationParamerters;
///
/// 发送广播的消息队列
///
private IMessageFIFO broadCastFIFO = MessageFIFOFactory.Create(MessageFIFOType.IOCPMessageFIFO);
///
/// 连接模式构造
///
///通信参数
private TCPAsyncServerCommunication(TCPAsyncServerParameters pa)
{
this.communicationParamerters = pa;
this.end = new IPEndPoint(IPAddress.Parse(pa.Ip), pa.PortNO);
this.endPort = string.Format("{0}:{1}", pa.Ip, pa.PortNO);
broadCastFIFO.Excute += broadCastFIFO_Excute;
acceptFIFO.Excute += acceptFIFO_Exceute;
}
///
/// 广播数据处理
///
///
void broadCastFIFO_Excute(object obj)
{
TRequest requestData = null;
if (obj is TRequest)
{
requestData = (TRequest)obj;
}
if (requestData == null)
{
return;
}
try
{
List> listConnectSession = new List>();
lock (acceptLock)
{
if (dicConnectSession.Count > 0)
{
foreach (var item in dicConnectSession)
{
上述是通信组件的一个具体通信实现的关键类,可以看到把四个变化部分,全部用泛型做了替代,这样就可以实现组件与具体的实现无关性,从而实现反转控制,利用具体的实例化传入参数,进行调度的实例化加载。
简单的字符串发送广播应用示例:
请求数据部分
public class TcpServerTestRequest:Trace.Common.Communication.Request.BaseRequestData
{
private string broadCastData;
///
/// 广播数据
///
public string BroadCastData
{
get
{
return broadCastData;
}
set
{
broadCastData = value;
}
}
}
应答数据部分
public class TcpServerTestResponse:Trace.Common.Communication.Response.BaseResponseData
{
private string message;
public string Message
{
get
{
return message;
}
set
{
message = value;
}
}
}
消息输入过滤器:
public class TcpServerTestInputFilter
:Trace.Common.Communication.Message.BaseInPutMessageFilter
{
public override byte[] ProcessToSendBuffer(TcpServerTestRequest requestData)
{
return System.Text.Encoding.Default.GetBytes(requestData.BroadCastData);
}
}
public class TcpServerTestOuptFilter
:Trace.Common.Communication.Message.BaseOutPutMessageFilter
{
private IMessageFilterHelper messageFilterHelper = null;
///
/// 包尾
///
private byte[] end = new byte[] { 0x0D,0x0A };
public TcpServerTestOuptFilter()
{
messageFilterHelper = new MessageFilterFixEndHelper(end);
}
protected override void InputBufferData(string sessionID, byte[] buffer)
{
messageFilterHelper.InputBufferData(buffer);
}
protected override TcpServerTestResponse GetNextResponse()
{
byte[] tempBuffer = messageFilterHelper.GetNextData();
if (tempBuffer == null || tempBuffer.Length == 0)
{
return null;
}
string str=System.Text.Encoding.Default.GetString(tempBuffer);
TcpServerTestResponse temp = new TcpServerTestResponse();
temp.Message = str;
return temp;
}
}
调用实现:
///
/// 接收连接
///
ICommunication iReceiveCommunication;
private void butStart_Click(object sender, EventArgs e)
{
SendData sendData = new SendData();
sendData.SendCommand = SendCommand.Open;
this.ImessageFIFOInput(sendData);
}
private void Start()
{
try
{
string ip = this.tB_IP.Text.Trim();
int receivePort = Convert.ToInt32(this.tBReceivePort.Text.Trim());
BaseCommunicationParameters receivePa = new TCPServerParameters(ip, receivePort);
if (iReceiveCommunication == null)
{
iReceiveCommunication = CommunicationFactory.Create(receivePa);
iReceiveCommunication.OnResponseBuffer += iReceiveCommunication_OnResponseBuffer;
iReceiveCommunication.OnResponseInfo += iReceiveCommunication_OnResponseInfo;
iReceiveCommunication.OnOperateStaus += iSendCommunication_OnOperateStaus;
iReceiveCommunication.OnSessionAdded += iReceiveCommunication_OnSessionAdded;
iReceiveCommunication.OnSessionRemoved += iReceiveCommunication_OnSessionRemoved;
}
if (iReceiveCommunication.IsOpen == false)
{
iReceiveCommunication.Open();
}
}
catch (Exception ex)
{
this.Invoke(new MessShow(ShowMessage),ex.Message);
return;
}
this.Invoke(new MessShow(ShowMessage), "started");
}
void iReceiveCommunication_OnSessionRemoved(SessionInfo info)
{
this.Invoke(new MessShow(ShowMessage), string.Format("会话ID:{0},远程端口:{1} 主动断开连接,时间:{2}", info.SessionID, info.RemotePort, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")));
}
void iReceiveCommunication_OnSessionAdded(SessionInfo info)
{
this.Invoke(new MessShow(ShowMessage), string.Format("会话ID:{0},远程端口:{1}成功建立连接,时间:{2}", info.SessionID,info.RemotePort, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")));
}
void iReceiveCommunication_OnResponseBuffer(object sender, TcpServerTestResponse e)
{
this.Invoke(new MessShow(ShowMessage), string.Format("会话ID:{0},接收数据:{1},接收时间:{2}",e.SessionID.ToString(),e.Message,DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff")));
}
void iSendCommunication_OnOperateStaus(object sender, OperateStatus e)
{
if (e == OperateStatus.Opening)
{
this.Invoke(new MessShow(ShowMessage), "请求启动");
}
else if (e == OperateStatus.Opened)
{
this.Invoke(new MessShow(ShowMessage), "启动成功");
}
else if (e == OperateStatus.Closing)
{
this.Invoke(new MessShow(ShowMessage), "请求关闭");
}
else if (e == OperateStatus.Closed)
{
this.Invoke(new MessShow(ShowMessage), "关闭成功");
}
}
void iReceiveCommunication_OnResponseInfo(object sender, ResponseInfo e)
{
if (e.ResponseInfoType == ResponseInfoType.NotifyInfo)
{
this.Invoke(new MessShow(ShowMessage), e.Message.Replace("\r\n", ""));
}
else if (e.ResponseInfoType == ResponseInfoType.Exception)
{
this.Invoke(new MessShow(ShowMessage), e.Exception.Message.Replace("\r\n", ""));
}
else if (e.ResponseInfoType == ResponseInfoType.ReceiveLog)
{
if (cB_ReceiveLog.Checked == true)
{
this.Invoke(new MessShow(ShowMessage), e.Message.Replace("\r\n", ""));
}
}
else if (e.ResponseInfoType == ResponseInfoType.SendLog)
{
if (cB_SendLog.Checked == true)
{
this.Invoke(new MessShow(ShowMessage), e.Message.Replace("\r\n", ""));
}
}
}
private void butStop_Click(object sender, EventArgs e)
{
SendData sendData = new SendData();
sendData.SendCommand = SendCommand.Close;
ImessageFIFOInput(sendData);
}
private void Stop()
{
try
{
if (iReceiveCommunication != null)
{
if (iReceiveCommunication.IsOpen == true)
{
iReceiveCommunication.Close();
}
}
this.Invoke(new MessShow(ShowMessage),"stoped");
}
catch (Exception ex)
{
this.Invoke(new MessShow(ShowMessage), ex.Message);
}
}
2、第一版中基本上已经实现了整个设计部分的功能,但在迁入到项目中时遇到了问题,项目应用中需要对输入和应答,配对反馈回请求端,但是由于设计时就考虑了吞吐性,输入和应答部分,是独立在不同的线程上,如果要操作就需要在全局变量上处理,势必要加入锁机制或队列,这不是一个好的注意,曾经试过很多方式,最终把请求部分合并到应答线程上排队,即发送之前,先进行请求数据的同步处理,让其同步到应答线程上,应答时再合并,整个响应和调度速度比起全局锁处理,效率瞬间提高很多,此处的应答线程不是连接会话的数据回馈线程,连接会话上的数据需要二次在这个线程上同步,因为连接会话由底层线程池发起,其具体的线程是不确定的,无法使用线程池中的线程作为应答线程。还遇到一个问题,是原来的经验或思维定势造成的,在一些与设备交互场合,需要实现一个同步的调用过程,即发送了指令给设备,设备给出答复之后,原调用函数再返回,否则需要等待超时,因为这个机制的引入,一直想着也放在通信的连接会话上去处理,刚开始还没发现问题,后来再大协议块或密集协议块中,发现了这种机制的问题,数据连包或业务层处理超时,都会引起同步函数的不确定性或数据丢包,在考虑了很长一段时间后,意识到应该放入过滤器层,这层因为是面向设备的应用层,才能完整的知道设备动作而不是时间间隔字节流数据。这个地方因为是没有太多可借鉴的,曾经纠结了很长一段时间。
3、日志完善,随着在项目中更多的设备通信迁移进来,发现在很多时候,尤其是与设备有过多交互时,记录日志是一个很麻烦的事情,因为日志的记录,必定要记下原因,或发起者等,这些信息不能全部传到组件中,既然不能传入,那能不能回调出去,在调用部分之外进行复核,这是引入了跟踪ID,这是借鉴阿里中间团队的一个思路,他们也曾经面临了数据在内存中的快速追踪定位问题,引入跟踪ID之后,就可以实现不论转了多少圈,经过多少调度,还能找到原始起点。这部分曾经引起了组件的一次大变更,也可以说是去组件代码大变更,代码量变小,目的性更明确了。
4、应用完善,将不同的场合再次分类,抽象。