0 前言 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |
在程序设计与实际应用中,Socket数据包接收服务器够得上一个经典问题了:需要计算机与网络编程知识(主要是Socket),与业务处理逻辑密切(如:包组成规则),同时还要兼顾系统运行的稳定、效率、安全与管理等。具体应用时,在满足业务处理逻辑要求的基础上,存在侧重点:有些需要考虑并发与效率,有些需要强调稳定与可靠等等。虽然.NET 2.0 Framework上的IOCP(I/O完成端口)异步技术可以有效解决并发等问题,但完全的异步模式也缺乏一些控制上的灵活性,例如:Socket暂停操作等。
本文介绍的是一个传统Socket数据包服务器解决方案,该方案改自笔者2005年底的一个交通部省级公路交通流量数据服务器中心(DSC)项目。当时.NET Framework 2.0 与 Visual Studio 2005 发布没多久,笔者接触C#的时间不长。于是Google了国内国外网,希望找点应用C#解决Socket通信问题的思路和代码。最后,找到了两篇帮助最大的文章:一篇是国人2005年3月写的Socket接收器框架——在C#中使用异步Socket编程实现TCP网络服务的C/S的通讯构架(一)(分(一)、(二)两篇),该文应用了客户端Socket会话(Session)概念;另一篇是美国人写的,提出了多线程、分段接收数据包的技术方案,描述了多线程、异步Socket的许多实现细节,该文坚定了笔者采用多线程和异步方式处理Socket接收器的技术路线。第一个版本EMTASS 1.0(EMTASS,Extensible Multi-Thread Asynchronous Socket Server)于2006年初完成并投入使用。
今年暑假,笔者修改了原Socket接收服务器代码,即EMTASS 1.1。最近,又按框架的可扩展性、可重用性等要求重新构思和设计了EMTASS,即EMTASS 2.0。下面的介绍共分六个部分:
1 总体思路与架构 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |
总体构思上,主要考虑多线程、异步Socket和可扩展性三个方面。
在Internet环境下的Socket应用中,客户端和网络容易出现异常,此时必须释放异常退出的Socket资源。考虑到服务器的高并发能力,一般采取包接收和处理分开的策略:将接收到的包添加到包队列,然后处理队列中的数据包。当然,侦听远程客户端的连接请求可以用Socket的AcceptAsync()异步方法(IOCP,I/O完成端口由此开始)。考虑到暂停、关闭同步操作,仍然用一个线程。这样,清理资源、处理数据包、侦停客户连接请求就是组成了EMTASS架构的三个核心线程,它们由.NET线程池统一管理:
.NET Framework中的Socket具有完整的异步处理能力:侦听后异步接收(AcceptAsync())、数据异步接收(BeginReceive())、数据异步发送(BeginSend())等。EMTASS框架采取了异步接收和发送方式,并封装在TSessionBase类中。在EMTASS的版本1.0、1.1中,这些方法在主类TSocketServerBase中实现,显然不符合类封装原则。
可扩展性主要考虑不同的业务处理逻辑和应用场景,即:数据包格式、数据存储方法、数据库服务器等。框架EMTASS的可扩展性体现在类的泛型与抽象设计、方法虚拟和保护等方面:
(图1 主要类层次关系)
按应用类别分,EMTASS主要有四组类:Socket服务器类、Session会话类、Database数据库类和枚举类型。
(图2 事件参数类层次关系)
EMTASS框架的事件包括三类:第一,普通事件,如:服务器启动与停止;第二,异常事件,接收与发送数据异常、数据库连接或数据存储异常等;第三,与会话相关事件,如:增加会话对象、接收到一个合法数据包等。异常与会话结合即是会话异常事件。通过泛型委托EventHandler可以定义类事件,其中的事件参数类型如下:
2 关键实现技术 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |
下面介绍主要类TSocketServerBase和辅助类TSessionBase、TDatabaseBase中的主要实现方法。
该类包括了全部的对外接口和事件,主要实现前面介绍过的三个线程。
.NET 提供了线程池方法 ThreadPool.QueueUserWorkItem() 自动将委托对象添加到系统线程池,见如下的实现代码:
if (!ThreadPool.QueueUserWorkItem(this.StartServerListen)) return false; if (!ThreadPool.QueueUserWorkItem(this.CheckDatagramQueue)) return false; if (!ThreadPool.QueueUserWorkItem(this.CheckSessionTable)) return false;
其中, 客户端连接请求侦听方法StartServerListen()、数据包队列检查方法CheckDatagramQueue()和会话表检查方法CheckSessionTable()均使用循环处理方式,循环条件是m_serverClosed为false。只有该类的Close()方法可以中断这三个线程。在Close()方法中设置m_serverClosed为true终止线程的同时,还需要考虑线程退出的同步问题,此时使用手工事件信号对象ManualResetEvent。参考如下数据包队列检查线程方法的代码:
private void CheckDatagramQueue(object state) { m_checkDatagramQueueResetEvent.Reset(); while (!m_serverClosed) { lock (m_sessionDictionary) { // ...其它代码 } } m_checkDatagramQueueResetEvent.Set(); }
上述代码是不安全的,一般要需要try{}finally{}保证事件信号对象Reset()与Set()匹配。但EMTASS中的三个线程方法均有自己的异常处理方式,不会抛出异常。下面是关闭服务器方法Close()的主要代码。在设置变量了m_serverCloed为true后,使用了三个事件信号等待,同步三个线程的正常终止。
private void Close() { if (m_serverClosed) { return; } m_serverClosed = true; m_serverListenPaused = true; m_checkServerListenResetEvent.WaitOne(); // 等待3个线程 m_checkSessionTableResetEvent.WaitOne(); m_checkDatagramQueueResetEvent.WaitOne(); // ...其它代码 }
建立会话对象后,三种情况需要终止会话:1)关闭服务器;2)会话异常;3)会话超时。第1种情况将强制终止会话,第2、3种情况需要清理线程终止会话并释放其资源。为防止立即关闭Socket引发的异常,系统分3个步骤完成:1)标记该会话为Invalid,此时停止一切与该会话的处理操作;2)调用Shutdown()方法:Shutdown会话Socket,标记会话状态为Shutdown;3)调用Close()方法:清除会话缓冲区和数据包队列,释放Socket资源,从会话表中删除该对象。具体操作可以参考TSocketServerBase类中的CheckSessionTable()方法。
EMTASS框架的所有事件,包括TSessionBase类和TDatabaseBase类的事件,都通过服务器类TSocketServerBase对外发布。在创建会话对象或数据库对象时,直接传递其事件给TSocketServerBase的相同委托事件,见如下代码举例:
session.DatagramAccepted += new EventHandler(this.OnDatagramAccepted); session.DatagramHandled += new EventHandler(this.OnDatagramHandled);
上述代码中,将TSessionBase派生类对象session的两个事件直接绑定(使用+=方法)到当前TSocketServerBase对象上。具体实现代码可以参考TSocketServerBase的初始化方法Initiate()和添加会话对象方法AddSession()。
该抽象类包括客户端Socket、数据接收缓冲区和数据包队列等成员,封装了所有与Socket通信的方法。该类还包括数据包处理方法:数据包解析保护方法ResolveSessionBuffer()和数据包分析虚拟方法AnalyzeDatagram()。
TSessionBase包括两个数据接收缓冲区和一个数据包队列:
该方法是protected的,可以根据数据包结构与具体业务逻辑重写代码。在TSessionBase类中实现的包组成规则是:开始字符是<结束字符是>。特别指出,Socket通信中有两个必须考虑的著名问题:
该抽象方法是TSessionBase类必须重写的方法,也是EMTASS框架扩展的主要接口,应该完成如下基本任务:
该抽象类定义了3个数据库异常处理事件:DatabaseOpenException、DatabaseCloseExeption和DatabaseException,以及4个public方法:Open()、Close()、Clear()和Store()。其中,Open()是抽象方法,在派生类中可以增加自己的代码(见demo的实现部分),Close()方法关闭数据库连接,Clear()方法在Close()中被调用——关闭数据库前清理相关资源,虚方法Store()用于数据存储。EMTASS框架给出了该基类的两个派生类:TSqlServerBase和TOleDatabaseBase,可以满足一般的数据库应用需求。
3 架构使用简介 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |
EMTASS框架的使用包括如下步骤:
泛型类TSocketServerBase提供了EMTASS框架的所有对外接口(属性、方法和事件),包括内联TSessionBase对象和TDatabaseBase对象的对外属性和事件。
有两个重载版本,默认端口3130。考虑到可扩展性,必须给出数据库连接串,见如下代码:
public TSocketServerBase(string dbConnectionString) { this.Initiate(dbConnectionString); } public TSocketServerBase(int tcpPort, string dbConnectionString) { m_servertPort = tcpPort; this.Initiate(dbConnectionString); }
构造函数中的方法Initiate()完成具体的初始化任务。
ServerPort
:服务器端口号,默认值为3130Closed
:服务器已经关闭ListenPaused
:服务器暂时停止客户端连接请求LoopWaitTime
:Socket.Listen方法中的等待时间(ms),默认值为25msMaxDatagramSize
:允许数据包的最大长度,默认值为1024KMaxListenQueueLength
:最大侦听队列长度,默认值为16MaxReceiveBufferSize
:允许数据包接收缓冲区的最大长度,默认值为16KMaxSameIPCount
:允许同地址IP的会话Socket个数,默认值为64MaxSessionTableLength
:允许最大会话表长度,默认值为1024MaxSessionTimeout
:允许最大的会话超时间隔(s),默认值为120sErrorDatagramCount
:错误数据包个数ReceivedDatagramCount
:接收数据包个数ServerExceptionCount
:服务器异常次数SessionCount
:当前会话个数SessionExeptionCount
:会话异常个数SessionCoreInfoList
:当前会话表信息清单Start()
:启动服务器Stop()
:关闭服务器PauseListen()
:暂停侦听连接请求ResumeListen()
:恢复侦听连接请求Dispose()
:关闭服务器并释放系统资源CloseSession()
:关闭一个会话CloseAllSessions()
:关闭全部会话SendToSession()
:给一个会话发送消息SendToAllSessions()
:给所有会话发送消息DatabaseCloseException
:数据库关闭异常DatabaseException
:数据库异常DatabaseOpenException
:数据库打开异常DatagramAccepted
:接受了一个完整数据包DatagramDelimiterError
:数据包界限符错误DatagramError
:数据包错误DatagramHandled
:处理了一个数据包DatagramOversizeError
:数据包超长错误ServerStarted
:服务器启动后ServerClosed
:服务器关闭后ServerListenPaused
:服务器暂停连接请求后ServerListenResumed
:服务器恢复连接请求后ServerException
:服务器异常SessionRejected
:连接请求被拒绝SessionConnected
:建立一个会话连接SessionDisConnected
:断开一个会话连接SessionReceiveException
:会话接收数据异常SessionSendException
:会话发送数据异常SessionTimeout
:会话超时
下载包中包括EMTASS框源代码和Demo。其中,VS2005的Demo解决方案文件为EMTASS.sln,包含两个项目:服务器项目和客户端项目。/bin/文件夹下的编译文件可直接运行:先启动服务器,然后运行客户端。
服务器端包括两个部分:第一,接收服务器窗体程序;第二,Access数据库。服务器端窗体程序包含如下实现:
服务器端的主要代码如下:
public partial class SocketServerDemo : Form { TSocketServerBase m_socketServer; public SocketServerDemo() { InitializeComponent(); } private void SocketServerDemo_Load(object sender, EventArgs e) { cb_maxDatagramSize.SelectedIndex = 1; // 数据库连接字符串 string connStr = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source = DemoAccessDatabase.mdb;"; m_socketServer = new TSocketServerBase(connStr); // 服务器对象 m_socketServer.MaxDatagramSize = 1024 * int.Parse(cb_maxDatagramSize.Text); // 包最大长度 this.AttachServerEvent(); // 附加服务器全部事件 } private void SocketServerDemo_FormClosing(object sender, FormClosingEventArgs e) { m_socketServer.Dispose(); // 关闭服务器进程 } private void AttachServerEvent() { m_socketServer.ServerStarted += this.SocketServer_Started; m_socketServer.ServerClosed += this.SocketServer_Stoped; m_socketServer.ServerListenPaused += this.SocketServer_Paused; m_socketServer.ServerListenResumed += this.SocketServer_Resumed; m_socketServer.ServerException += this.SocketServer_Exception; m_socketServer.SessionRejected += this.SocketServer_SessionRejected; m_socketServer.SessionConnected += this.SocketServer_SessionConnected; m_socketServer.SessionDisconnected += this.SocketServer_SessionDisconnected; m_socketServer.SessionReceiveException += this.SocketServer_SessionReceiveException; m_socketServer.SessionSendException += this.SocketServer_SessionSendException; m_socketServer.DatagramDelimiterError += this.SocketServer_DatagramDelimiterError; m_socketServer.DatagramOversizeError += this.SocketServer_DatagramOversizeError; m_socketServer.DatagramAccepted += this.SocketServer_DatagramReceived; m_socketServer.DatagramError += this.SocketServer_DatagramrError; m_socketServer.DatagramHandled += this.SocketServer_DatagramHandled; m_socketServer.DatabaseOpenException += this.SocketServer_DatabaseOpenException; m_socketServer.DatabaseCloseExcpetion += this.SocketServer_DatabaseCloseException; m_socketServer.DatabaseExcpetion += this.SocketServer_DatabaseException; m_socketServer.ShowDebugMessage += this.SocketServer_ShowDebugMessage; } //...其它代码 }
下面是服务器端Demo运行图片
创建一个TcpClient对象,模拟远程客户端与服务器通信。下面是客户端Demo运行图片:
4 一般测试结果 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |
显然,这种测试环境和结果有待进一步验证,但存在较大的改进空间。特别,数据包队列最大值一般不超过5,表明服务器接收到包后立即处理,并发性能比较好。
5 总结与展望 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |
本文介绍的EMTASS 2.0 是笔者一段工作的总结,也是学习基于.NET的类设计、组件设计、模式设计等的一个小结。笔者的目标就是不断修改和完善,设计与实现一个可靠与稳定的、有良好可扩展性的和易于使用的Socket数据包接收服务器框架。由于最初的代码和思路均来自他人的开源架构和设计构思,EMTASS也仿效一般开源做法:公布源码和设计思路。
基于EMTASS 1.0的服务器有连续运行30天完全正常的记录,而框架EMTASS 2.0 虽然具有可扩展性,也进行了一般的测试,但没有投入实际运行,需要时间检验和实践验证。当前,.NET 3.0及3.5 Framwwork提供的IOCP(完成端口)具有更好的异步并发处理能力,笔者将结合新的运行平台,完善与升级EMTASS,并公布完善与升级计划。如果有读者使用EMTASS 2.0时发现问题,或有更好的建议或想法,请不吝指正。
6 版本与源码 |
>>[前言]、[第1节]、[第2节]、[第3节]、[第4节]、[第5节]、[第6节] |