最近看了不少Internet Protocol、TCP/IP Protocol和.Net Network Programming方面的资料,也动手做了一些实践,把所得总结为如下的文字。
1. Overview of Socket in .Net
如果做大型的分布式应用,且要求有很高的实时性,通常我们会使用TCP/IP协议来让client和server进行通信——传递命令和数据(比如XML Stream)。这个时候我们就需要使用异步socket了。.Net Framework提供了Socket类,此类对WinSock进行了比较好的包装,隐藏了很多细节,大大简化了我们需要做的工作;另外还提供了更高层次的抽象的类——TcpListener类和TcpClient类。而且,.Net Framework还提供了NetworkStream来简化读取和发送数据的工作。但是TcpListener、TcpClient和NetworkStream都是使用blocking call的方式,而这并不是我们想要的——我们需要异步的方式——高实时性要求我们必须使用异步Socket。再者,我们还要注意到TCP协议的特点——它不会帮我们区分消息的边界——比如我们分别发送了10K和20K字节的数据出去,到了接收端的缓冲区中,这两条消息将是30K字节的一大块数据——这就需要我们自己建立一个机制来区分不同的消息。相对的,UDP协议将会为我们做区分消息边界的工作,就省去了区分不同的消息的工作。所以我们在做使用TCP协议通信的程序的时候,我们通常需要先定一个自己的通信协议。另外,还有一些实现上的细节需要注意的:多线程、server如何支持连接无限数量(当然是有限资源范围内的无限)的client、如何确定client已经断开了socket连接、如何区别client socket连接和server如何给特定的client发送信息,是否需要Queue来暂存消息等等问题。
2. Consider and Discuss
其实上面已经提到了一些需要考量的问题。我把我认为需要考虑的问题列举如下:
1、 需要适应的网络环境。比如局域网、广域网和Internet。Internet环境中通常需要考虑穿透防护墙问题,秘密通信问题(加密),而局域网环境中通常不需要考虑防护墙问题。
2、 Server支持连接无限量的client,且能区分开每个client和给特定的client发送消息。
3、 缓存消息。比如使用消息队列来存放消息。如果客户端数量非常大,那使用队列(能把消息存放到磁盘中)来保存消息(这些消息是需要业务模块去处理的)就有必要了,这样即使down机了也能恢复回去。
4、 Server和client的通信协议。这里的协议不是指TCP这样的协议,而是我们的应用中约定的协议,比如消息格式。
5、 如何使用线程,即如何管理线程来实现异步通信。
6、 如何确定client已经断开了socket连接。
3. Implement Asynchronous Socket
就我自己的了解,实现异步Socket大致有四种方法:
1、 使用Socket类的异步方法。
2、 使用Socket类的同步方法,但自己创建Thread来实现异步通信。
3、 使用Socket类的同步方法,但使用.Net Framework的ThreadPool类来实现异步通信。
4、 使用Socket类的同步方法,但使用自己写的ThreadPool来实现异步通信。
下面就这几种方法分别说明之。
3.1. AsyncCallback Method
此种方法可能是大家用的比较多的一种方法,也是一种最简单的方法。MSDN中讲如何实现异步socket的时候就讲的是使用AsyncCallback Method——使用socket类提供的BeginXXX、EndXXX这样的方法。这样方法实际上都是使用了线程池——.net framework中的ThreadPool类。此类在一个Process中只能有一个实例,任何BeginXXX、EndXXX方法都使用了此类,默认最大线程数量是25个,当然也可以修改此限制,具体做法为请看这里。
下面是一些AsyncCallback Method的例子:
3.2. Synchronous Method & Thread
使用.net ThreadPool类的话,自己不能对Thread进行控制。而且此ThreadPool类有一些缺点(比如不适合于长时间执行某操作的场合)。这个时候我们可以直接创建Thread来实现异步,这个时候我们自己需要去做更多的工作——使用一个Thread去接收TCP连接,对于每个连接还需要开一个Thread去接收数据,另外还需要注意线程的同步、资源的释放和各种异常情况的处理;另外,性能也是一个必须重视的问题。
3.3. Synchronous Method & .Net ThreadPool
其实这么做和使用AsyncCallback Method实质上是一样的——都是使用了.Net ThreadPool。和Synchronous Method & Thread不同的就是不用自己去管理线程了,而把线程管理的复杂性交给ThreadPool类来做了,它为我们使用多线程提供了便利。而且,.Net ThreadPool充分利用CPU的时间片,提供了比较高的性能,也有人做了测试证明使用ThreadPool通常会获比直接创建和管理多个线程更为理想的性能。
3.4. Synchronous Method & Own ThreadPool
如果不想使用.Net ThreadPool类,那我们完全可以自己写一个ThreadPool。.Net ThreadPool有一些缺点,其中主要的几个为:
1、 一个Process中只能有一个实例,它在各个AppDomain是共享的。ThreadPool只提供了静态方法,不仅我们自己添加进去的WorkItem使用这个Pool,而且.net framework中那些BeginXXX、EndXXX之类的方法都会使用此Pool。
2、 所支持的Callback不能有返回值。WaitCallback只能带一个object类型的参数,没有任何返回值。
3、 不能方便地修改最小和最大线程数。ThreadPool最大线程数为25个,有的情况下可能不够用的。如果要修改限制还颇费周折——需要使用COM。
4、 不适合用在长期执行某任务的场合。我们常常需要做一个Service来提供不间断的服务(除非服务器down掉),但是使用ThreadPool并不合适。
我们自己写ThreadPool的话,可以使用Hashtable来存放Thread,再实现WorkItem和WorkItemQueue这样的类来排队Work Item,使用WaitHandle来做同步。早已经有人因不满.net ThreadPool而编写了自己的ThreadPool,个人认为比较好的有SmartThreadPool、XYThreadPool。
把对线程的管理交给ThreadPool后,我们就可以专注于通信方面的工作了。
4. My Asynchronous Socket Lib
我实现的一个简单的TCP Asynchronous Socket Lib有如下特点:
1、 适合于局域网和Internet的网络环境。
2、 Server能接收无限量的client连接——当然受server资源的限制。
3、 Server会保持client的连接一段时间(timeout),以便于server向client发送消息。这样可以保证即使client端在防火墙内,server也能给client发送消息。
4、 使用Length-leading的消息格式。通常区分TCP消息边界有三种方式:(1)发送固定长度的消息(2)使用Length-leading的消息(3)使用消息分隔符。
5、 对于server,接收到的消息存放到queue中,并触发事件。即使server down掉,消息也不丢失。
6、 对于server,待发送的消息也存入queue中,并触发事件。
7、 使用一个Thread Pool(不是.net framework提供的ThreadPool)来管理线程。分别有线程去做接收连接请求、检测连接是否可用和有无消息、接收消息和检查超时的工作。
8、 提供很多事件,比如OnServerStart、OnNewClient、OnReceiveMessage、OnServerError和OnServerStop等等。
9、 检查client连接。Client自己断开了连接后,在server端从socket的Available属性并不能判断出来,可行的办法是使用Poll方法,然后看是否能peek到数据;或者接收数据的时候捕捉异常。就我自己实践的结果,使用Poll方法会有一些麻烦:我是将client连接进来的socket存放到Hashtable中的,但是调用了Poll方法后,会使此Hashtable被改变,从而导致迭代此Hashtable失败。CodeGuru上的例子介绍了使用异常的方法。
希望此文能对大家有一点帮助,也希望有经验的朋友多多批评指正。