C# 实现的多线程异步Socket数据包接收器框架 (转)

转自:http://www.cnblogs.com/wcfgroup/archive/2008/10/06/1304512.html

几天前在博问中看到一个C# Socket问题,就想到笔者2004年做的一个省级交通流量接收服务器项目,当时的基本求如下:

  • 接收自动观测设备通过无线网卡、Internet和Socket上报的交通量数据包
  • 全年365*24运行的自动观测设备5分钟上报一次观测数据,每笔记录约2K大小
  • 规划全省将有100个左右的自动观测设备(截止2008年10月还只有30个)

      当时,VS2003才发布年多,笔者也是接触C#不久。于是Google了国内国外网,希望找点应用C#解决Socket通信问题的思路和代码。最后,找到了两篇帮助最大的文章:一篇是国人写的Socket接收器框架,应用了独立的客户端Socket会话(Session)概念,给笔者提供了一个接收服务器的总体框架思路;另一篇是美国人写的,提出了多线程、分段接收数据包的技术方案,描述了多线程、异步Socket的许多实现细节,该文坚定了笔者采用多线程和异步方式处理Socket接收器的技术路线。

     具体实现和测试时笔者还发现,在Internet环境下的Socket应用中,需要系统有极强的容错能力:没有办法控制异常,就必须允许它们存在(附加源代码中可以看到,try{}catch{}语句较多)。对此,笔者设计了一个专门的检查和清理线程,完成无效或超时会话的清除和资源释放工作。

     依稀记得,国内框架作者的名称空间有ibm,认为是IBM公司职员,通过邮件后才知道其人在深圳。笔者向他请教了几个问题,相互探讨了几个技术关键点。可惜,现在再去找,已经查不到原文和邮件了。只好借此机会,将本文献给这两个素未谋面的技术高人和同行,也盼望拙文或源码能给读者一点有用的启发和帮助。

1、主要技术思路

     整个系统由三个核心线程组成,并由.NET线程池统一管理:

  • 侦听客户端连接请求线程:ListenClientRequest(),循环侦听客户端连接请求。如果有,检测该客户端IP,看是否是同一观测设备,然后建立一个客户端TSession对象,并通过Socket异步调用方法BeginReceive()接收数据包、EndReceive()处理数据包
  • 数据包处理线程:HandleDatagrams(),循环检测数据包队列_datagramQueue,完成数据包解析、判断类型、存储等工作
  • 客户端状态检测线程:CheckClientState(),循环检查客户端会话表_sessionTable,判断会话对象是否有效,设置超时会话关闭标志,清楚无效会话对象及释放其资源

2、主要类简介

     系统主要由3个类组成:

  • TDatagramReceiver(数据包接收服务器):系统的核心进程类,建立Socket连接、处理与存储数据包、清理系统资源,该类提供全部的public属性和方法
  • TSession(客户端会话):由每个客户端的Socket对象组成,有自己的数据缓冲区,清理线程根据该对象的最近会话时间判断是否超时
  • TDatagram(数据包类):判断数据包类别、解析数据包

3、关键函数和代码

     下面简介核心类TDatagramReceiver的关键实现代码。

3.1  系统启动

      系统启动方法StartReceiver()首先清理资源、创建数据库连接、初始化若干计数值,然后创建服务器端侦听Socket对象,最后调用静态方法ThreadPool.QueueUserWorkItem()在线程池中创建3个核心处理线程。

 

view plain copy to clipboard print ?
  1. /// <summary>  
  2. ///  启动接收器  
  3. /// </summary>  
  4. public bool StartReceiver()  
  5. {  
  6.     try  
  7.     {  
  8.         _stopReceiver = true;  
  9.   
  10.         this.Close();  
  11.   
  12.         if (!this.ConnectDatabase()) return false;  
  13.   
  14.         _clientCount = 0;  
  15.         _datagramQueueCount = 0;  
  16.         _datagramCount = 0;  
  17.         _errorDatagramCount = 0;  
  18.         _exceptionCount = 0;  
  19.   
  20.         _sessionTable = new Hashtable(_maxAllowClientCount);  
  21.         _datagramQueue = new Queue<TDatagram>(_maxAllowDatagramQueueCount);  
  22.   
  23.         _stopReceiver = false;  // 循环中均要该标志  
  24.   
  25.         if (!this.CreateReceiverSocket())  //建立服务器端 Socket 对象  
  26.         {  
  27.             return false;  
  28.         }  
  29.   
  30.         // 侦听客户端连接请求线程, 使用委托推断, 不建 CallBack 对象  
  31.         if (!ThreadPool.QueueUserWorkItem(ListenClientRequest))  
  32.         {  
  33.             return false;  
  34.         }  
  35.   
  36.         // 处理数据包队列线程  
  37.         if (!ThreadPool.QueueUserWorkItem(HandleDatagrams))  
  38.         {  
  39.             return false;  
  40.         }  
  41.   
  42.         // 检查客户会话状态, 长时间未通信则清除该对象  
  43.         if (!ThreadPool.QueueUserWorkItem(CheckClientState))  
  44.         {  
  45.             return false;  
  46.         }  
  47.   
  48.         _stopConnectRequest = false;  // 启动接收器,则自动允许连接  
  49.     }  
  50.     catch  
  51.     {  
  52.         this.OnReceiverException();  
  53.         _stopReceiver = true;  
  54.     }  
  55.     return !_stopReceiver;  
  56. }  

 

下面是创建侦听Socket对象的方法代码。

 

view plain copy to clipboard print ?
  1. /// <summary>  
  2. /// 创建接收服务器的 Socket, 并侦听客户端连接请求  
  3. /// </summary>  
  4. private bool CreateReceiverSocket()  
  5. {  
  6.     try  
  7.     {  
  8.         _receiverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
  9.         _receiverSocket.Bind(new IPEndPoint(IPAddress.Any, _tcpSocketPort));  // 绑定端口  
  10.         _receiverSocket.Listen(_maxAllowListenQueueLength);  // 开始监听  
  11.   
  12.         return true;  
  13.     }  
  14.     catch  
  15.     {  
  16.         this.OnReceiverException();  
  17.         return false;  
  18.     }  
  19. }  

 

3.2  侦听客户端连接请求

      服务器端循环等待客户端连接请求。一旦有请求,先判断客户端连接数是否超限,接着检测该客户端IP地址,一切正常后建立TSession对象,并调用异步方法接收客户端Socket数据包。

      代码中,Socket读到数据时的回调AsyncCallback委托方法EndReceiveData()完成数据接收工作,正常情况下启动另一个异步BeginReceive()调用。

      .NET中,每个异步方法都有自己的独立线程,异步处理其实也基于多线程机制的。下面代码中的异步套异步调用,既占用较大的系统资源,也给处理带来意想不到的结果,更是出现异常时难以控制和处理的关键所在。

 

view plain copy to clipboard print ?
  1. /// <summary>  
  2. /// 循环侦听客户端请求,由于要用线程池,故带一个参数  
  3. /// </summary>  
  4. private void ListenClientRequest(object state)  
  5. {  
  6.     Socket client = null;  
  7.     while (!_stopReceiver)  
  8.     {  
  9.         if (_stopConnectRequest)  //  停止客户端连接请求  
  10.         {  
  11.             if (_receiverSocket != null)  
  12.             {  
  13.                 try  
  14.                 {  
  15.                     _receiverSocket.Close();  // 强制关闭接收器  
  16.                 }  
  17.                 catch  
  18.                 {  
  19.                     this.OnReceiverException();  
  20.                 }  
  21.                 finally  
  22.                 {  
  23.                     // 必须为 null,否则 disposed 对象仍然存在,将引发下面的错误  
  24.                     _receiverSocket = null;  
  25.                 }  
  26.             }  
  27.             continue;  
  28.         }  
  29.         else  
  30.         {  
  31.             if (_receiverSocket == null)  
  32.             {  
  33.                 if (!this.CreateReceiverSocket())  
  34.                 {  
  35.                     continue;  
  36.                 }  
  37.             }  
  38.         }  
  39.   
  40.         try  
  41.         {  
  42.             if (_receiverSocket.Poll(_loopWaitTime, SelectMode.SelectRead))  
  43.             {  
  44.                 // 频繁关闭、启动时,这里容易产生错误(提示套接字只能有一个)  
  45.                 client = _receiverSocket.Accept();  
  46.   
  47.                 if (client != null && client.Connected)  
  48.                 {  
  49.                     if (this._clientCount >= this._maxAllowClientCount)  
  50.                     {  
  51.                         this.OnReceiverException();  
  52.   
  53.                         try  
  54.                         {  
  55.                             client.Shutdown(SocketShutdown.Both);  
  56.                             client.Close();  
  57.                         }  
  58.                         catch { }  
  59.                     }  
  60.                     else if (CheckSameClientIP(client))  // 已存在该 IP 地址  
  61.                     {  
  62.                         try  
  63.                         {  
  64.                             client.Shutdown(SocketShutdown.Both);  
  65.                             client.Close();  
  66.                         }  
  67.                         catch { }  
  68.                     }  
  69.                     else  
  70.                     {  
  71.                         TSession session = new TSession(client);  
  72.                         session.LoginTime = DateTime.Now;  
  73.   
  74.                         lock (_sessionTable)  
  75.                         {  
  76.                             int preSessionID = session.ID;  
  77.                             while (true)  
  78.                             {  
  79.                                 if (_sessionTable.ContainsKey(session.ID))  // 有可能重复该编号  
  80.                                 {  
  81.                                     session.ID = 100000 + preSessionID;  
  82.                                 }  
  83.                                 else  
  84.                                 {  
  85.                                     break;  
  86.                                 }  
  87.                             }  
  88.                             _sessionTable.Add(session.ID, session);  // 登记该会话客户端  
  89.                             Interlocked.Increment(ref _clientCount);  
  90.                         }  
  91.   
  92.                         this.OnClientRequest();  
  93.   
  94.                         try  // 客户端连续连接或连接后立即断开,易在该处产生错误,系统忽略之  
  95.                         {  
  96.                             // 开始接受来自该客户端的数据  
  97.                             session.ClientSocket.BeginReceive(session.ReceiveBuffer, 0,   
  98.                                 session.ReceiveBufferLength, SocketFlags.None, EndReceiveData, session);  
  99.                         }  
  100.                         catch  
  101.                         {  
  102.                             session.DisconnectType = TDisconnectType.Exception;  
  103.                             session.State = TSessionState.NoReply;  
  104.                         }  
  105.                     }  
  106.                 }  
  107.                 else if (client != null)  // 非空,但没有连接(connected is false)  
  108.                 {  
  109.                     try  
  110.                     {  
  111.                         client.Shutdown(SocketShutdown.Both);  
  112.                         client.Close();  
  113.                     }  
  114.                     catch { }  
  115.                 }  
  116.             }  
  117.         }  
  118.         catch  
  119.         {  
  120.             this.OnReceiverException();  
  121.   
  122.             if (client != null)  
  123.             {  
  124.                 try  
  125.                 {  
  126.                     client.Shutdown(SocketShutdown.Both);  
  127.                     client.Close();  
  128.                 }  
  129.                 catch { }  
  130.             }  
  131.         }  
  132.         // 该处可以适当暂停若干毫秒  
  133.     }  
  134.     // 该处可以适当暂停若干毫秒  
  135. }  

 

3.3  处理数据包

      该线程循环查看数据包队列,完成数据包的解析与存储等工作。具体实现时,如果队列中没有数据包,可以考虑等待若干毫秒,提高CPU利用率。

 

view plain copy to clipboard print ?
  1. private void HandleDatagrams(object state)  
  2. {  
  3.     while (!_stopReceiver)  
  4.     {  
  5.         this.HandleOneDatagram();  // 处理一个数据包  
  6.   
  7.         if (!_stopReceiver)  
  8.         {  
  9.             // 如果连接关闭,则重新建立,可容许几个连接错误出现  
  10.             if (_sqlConnection.State == ConnectionState.Closed)  
  11.             {  
  12.                 this.OnReceiverWork();  
  13.   
  14.                 try  
  15.                 {  
  16.                     _sqlConnection.Open();  
  17.                 }  
  18.                 catch  
  19.                 {  
  20.                     this.OnReceiverException();  
  21.                 }  
  22.             }  
  23.         }  
  24.     }  
  25. }  
  26.   
  27. /// <summary>  
  28. /// 处理一个包数据,包括:验证、存储  
  29. /// </summary>  
  30. private void HandleOneDatagram()  
  31. {  
  32.     TDatagram datagram = null;  
  33.   
  34.     lock (_datagramQueue)  
  35.     {  
  36.         if (_datagramQueue.Count > 0)  
  37.         {  
  38.             datagram = _datagramQueue.Dequeue();  // 取队列数据  
  39.             Interlocked.Decrement(ref _datagramQueueCount);  
  40.         }  
  41.     }  
  42.   
  43.     if (datagram == nullreturn;  
  44.   
  45.     datagram.Clear();  
  46.     datagram = null;  // 释放对象  
  47. }  

 

3.4  检查与清理会话

      本线程负责处理建立连接后的客户端会话TSession或Socket对象的关闭与资源清理工作,其它方法中出现异常等情况,尽可能标记相关TSession对象的属性NoReply=true,表示该会话已经无效、需要清理。

       检查会话队列并清理资源分3步:第一步,Shutdown()客户端Socket,此时可能立即触发某些Socket的异步方法EndReceive();第二步,Close()客户端Socket,释放占用资源;第三步,从会话表中清除该会话对象。其中,第一步完成后,某个TSession也许不会立即到第二步,因为可能需要处理其异步结束方法。

      需要指出, 由于涉及多线程处理,需要频繁加解锁操作,清理工作前先建立一个会话队列列副本sessionTable2,检查与清理该队副本列列的TSession对象。

 

view plain copy to clipboard print ?
  1. /// <summary>  
  2. /// 检查客户端状态(扫描方式,若长时间无数据,则断开)  
  3. /// </summary>  
  4. private void CheckClientState(object state)  
  5. {  
  6.     while (!_stopReceiver)  
  7.     {  
  8.         DateTime thisTime = DateTime.Now;  
  9.   
  10.         // 建立一个副本 ,然后对副本进行操作  
  11.         Hashtable sessionTable2 = new Hashtable();  
  12.         lock (_sessionTable)  
  13.         {  
  14.             foreach (TSession session in _sessionTable.Values)  
  15.             {  
  16.                 if (session != null)  
  17.                 {  
  18.                     sessionTable2.Add(session.ID, session);  
  19.                 }  
  20.             }  
  21.         }  
  22.   
  23.         foreach (TSession session in sessionTable2.Values)  // 对副本进行操作  
  24.         {  
  25.             Monitor.Enter(session);  
  26.             try  
  27.             {  
  28.                 if (session.State == TSessionState.NoReply)  // 分三步清除一个 Session  
  29.                 {  
  30.                     session.State = TSessionState.Closing;  
  31.                     if (session.ClientSocket != null)  
  32.                     {  
  33.                         try  
  34.                         {  
  35.                             // 第一步:shutdown  
  36.                             session.ClientSocket.Shutdown(SocketShutdown.Both);  
  37.                         }  
  38.                         catch { }  
  39.                     }  
  40.                 }  
  41.                 else if (session.State == TSessionState.Closing)  
  42.                 {  
  43.                     session.State = TSessionState.Closed;  
  44.                     if (session.ClientSocket != null)  
  45.                     {  
  46.                         try  
  47.                         {  
  48.                             // 第二步: Close  
  49.                             session.ClientSocket.Close();  
  50.                         }  
  51.                         catch { }  
  52.                     }  
  53.                 }  
  54.                 else if (session.State == TSessionState.Closed)  
  55.                 {  
  56.   
  57.                     lock (_sessionTable)  
  58.                     {  
  59.                         // 第三步:remove from table  
  60.                         _sessionTable.Remove(session.ID);  
  61.                         Interlocked.Decrement(ref _clientCount);  
  62.                     }  
  63.   
  64.                     this.OnClientRequest();  
  65.                     session.Clear();  // 清空缓冲区  
  66.                 }  
  67.                 else if (session.State == TSessionState.Normal)  // 正常的会话   
  68.                 {  
  69.   
  70.                     TimeSpan ts = thisTime.Subtract(session.LastDataReceivedTime);  
  71.                     if (Math.Abs(ts.TotalSeconds) > _maxSocketDataTimeout)  // 超时,则准备断开连接  
  72.                     {  
  73.                         session.DisconnectType = TDisconnectType.Timeout;  
  74.                         session.State = TSessionState.NoReply;  // 标记为将关闭、准备断开  
  75.                     }  
  76.                 }  
  77.             }  
  78.             finally  
  79.             {  
  80.                 Monitor.Exit(session);  
  81.             }  
  82.         }  // end foreach  
  83.   
  84.         sessionTable2.Clear();  
  85.     }  // end while  
  86. }   

 

4 、结语

     基于多线程处理的系统代价是比较大的,需要经常调用加/解锁方法lock()或Monitor.Enter(),需要经常创建处理线程等。从实际运行效果看,笔者的实现方案有较好的稳定性:2005年4月到5月间,在一个普通PC机器上连续运行30多天不出一点故障。同时,笔者采用了时序区间判重等算法,有效地提高了系统处理与响应速度。测试表明,在普通的PC机器(P4 2.0)上,可以做到0.5秒处理一个数据包,如果优化代码和服务器,还有较大的性能提升空间。

     上面的代码是笔者实现的省级公路交通流量数据服务中心(DSC)项目中的接收服务器框架部分,整个系统还包括:数据转发交通部的转发服务器、数据远程查询客户端、综合报表数据处理系统、数据在线发布系统、系统运行监控系统等。

     实际的接收服务器类及其辅助类超过3K行,整个系统则超过了60K。因为是早期实现的程序,难免有代码粗糙、方法欠妥的感觉,只有留待下个版本完善扩充了。由于与甲方有保密合同和版权保护等,不可能公开全部源代码,删减也有不当之处,读者发现时请不吝指正。下面是带详细注释的代码下载URL。

你可能感兴趣的:(C# 实现的多线程异步Socket数据包接收器框架 (转))