因为学习的需要,要求一个高性能的Socket服务器来提供多而繁杂的客户端连接请求,参考了许多资料和各位的思想,自己琢磨出了一套方案,觉的可行,于是拿出来晒晒,希望大家一起学习改进。(这个方案的1.0版本已经贴出来了,但是由于本人觉的1.0不太完美,做了下改进,本篇讲的主要是2.0)
1.0的文章参考:http://www.cnblogs.com/niuchenglei/archive/2009/07/23/1529462.html
1.0和2.0性能上基本没有变化,只是针对某些地方做了改进性的修改,本篇主要介绍原理,并贴出部分代码,上一篇是一个Overview。
设计原则:使用.net的SocketAsyncEventArgs(原因是这个比较简单,而且性能也很好,当然要是c++的话就用IOCP了)。考虑到能快速的反应用户的连接请求我采用了连接池的技术,类似于sqlserver的连接池,当然我的“池”还不够好,为了能快速的处理接受的数据我又加入了一个缓冲区池,说白了就是给每一个连接对象事先开辟好了空间。在传输方面,为了保证数据的有效性我们采用客户端和服务器端的验证(当然也不是太复杂)。
具体分析:分析的顺序是自底向上的
1.MySocketAsyncEventArgs类:这个类是一个继承自System.Net.Socket.SocketAsyncEventArgs类,是由于特定情况需要而添加了一些外加属性的类。
1 | internal sealed class MySocketAsyncEventArgs : SocketAsyncEventArgs |
4 | private string Property; |
5 | internal MySocketAsyncEventArgs( string property){ |
6 | this .Property = property; |
UID:用户标识符,用来标识这个连接是那个用户的。
Property:标识该连接是用来发送信息还是监听接收信息的。param:Receive/Send,MySocketAsyncEventArgs类只带有一个参数的构造函数,说明类在实例化时就被说明是用来完成接收还是发送任务的。
2.SocketAsyncEventArgsWithId类:该类是一个用户的连接的最小单元,也就是说对一个用户来说有两个SocketAsyncEventArgs对象,这两个对象是一样的,但是有一个用来发送消息,一个接收消息,这样做的目的是为了实现双工通讯,提高用户体验。默认的用户标识是"-1”,状态是false表示不可用
01 |
internal sealed class SocketAsyncEventArgsWithId:IDisposable |
03 |
private string uid = "-1" ; |
04 |
private bool state = false ; |
05 |
private MySocketAsyncEventArgs receivesaea; |
06 |
private MySocketAsyncEventArgs sendsaea; |
13 |
ReceiveSAEA.UID = value; |
UID:用户标识,跟MySocketAsyncEventArgs的UID是一样的,在对SocketAsycnEventArgsWithId的UID属性赋值的时候也对MySocketAsyncEventArgs的UID属性赋值。
State:表示连接的可用与否,一旦连接被实例化放入连接池后State即变为True
3.SocketAsyncEventArgsPool类:这个类才是真正的连接池类,这个类真正的为server提供一个可用的用户连接,并且维持这个连接直到用户断开,并把不用的连接放回连接池中供下一用户连接。
这个类是最核心的东西了,当然它设计的好坏影响着总体性能的好坏,它的各项操作也可能成为整个服务器性能的瓶颈。Pool包含有几个成员:
- Stack<SocketAsyncEventArgsWithId> pool : 从字面意思上就知道这是一个连接栈,用来存放空闲的连接的,使用时pop出来,使用完后push进去。
- IDictionary<string, SocketAsyncEventArgsWithId> busypool :这个也很好理解,busypool是一个字典类型的,用来存放正在使用的连接的,key是用户标识,设计的目的是为了统计在线用户数目和查找相应用户的连接,当然这是很重要的,为什么设计成字典类型的,是因为我们查找时遍历字典的关键字就行了而不用遍历每一项的UID,这样效率会有所提高。
- string[] keys:这是一个存放用户标识的数组,起一个辅助的功能。
- Count属性:返回连接池中可用的连接数。
- OnlineUID属性:返回在线用户的标识列表。
- Pop(string uid)方法:用于获取一个可用连接给用户。
- Push(SocketAsyncEventArgsWithId item)方法:把一个使用完的连接放回连接池。
- FindByUID(string uid)方法:查找在线用户连接,返回这个连接。
- BusyPoolContains(string uid)方法:判断某个用户的连接是否在线。
01 | internal sealed class SocketAsyncEventArgsPool:IDisposable |
03 | internal Stack<SocketAsyncEventArgsWithId> pool; |
04 | internal IDictionary< string , SocketAsyncEventArgsWithId> busypool; |
12 | return this .pool.Count; |
16 | internal string [] OnlineUID |
22 | busypool.Keys.CopyTo(keys, 0); |
27 | internal SocketAsyncEventArgsPool(Int32 capacity) |
29 | keys = new string [capacity]; |
30 | this .pool = new Stack<SocketAsyncEventArgsWithId>(capacity); |
31 | this .busypool = new Dictionary< string , SocketAsyncEventArgsWithId>(capacity); |
33 | internal SocketAsyncEventArgsWithId Pop( string uid) |
35 | if (uid == string .Empty || uid == "" ) |
37 | SocketAsyncEventArgsWithId si = null ; |
47 | internal void Push(SocketAsyncEventArgsWithId item) |
50 | throw new ArgumentNullException( "SocketAsyncEventArgsWithId对象为空" ); |
51 | if (item.State == true ) |
53 | if (busypool.Keys.Count != 0) |
55 | if (busypool.Keys.Contains(item.UID)) |
56 | busypool.Remove(item.UID); |
58 | throw new ArgumentException( "SocketAsyncEventWithId不在忙碌队列中" ); |
61 | throw new ArgumentException( "忙碌队列为空" ); |
70 | internal SocketAsyncEventArgsWithId FindByUID( string uid) |
72 | if (uid == string .Empty || uid == "" ) |
74 | SocketAsyncEventArgsWithId si = null ; |
75 | foreach ( string key in this .OnlineUID) |
85 | internal bool BusyPoolContains( string uid) |
89 | return busypool.Keys.Contains(uid); |
Note:这个类的设计缺陷是使用了太多的lock语句,对对象做了太多的互斥操作,所以我尽量的把lock内的语句化简或挪到lock外部执行。
4.BufferManager类:该类是一个管理连接缓冲区的类,职责是为每一个连接维持一个接收数据的区域。它的设计也采用了类似与池的技术,先实例化好多内存区域,并把每一块的地址放入栈中,每执行依次pop时拿出一块区域来给SocketAsyncEventArgs对象作为Buffer.
01 |
internal sealed class BufferManager:IDisposable |
03 |
private Byte[] buffer; |
04 |
private Int32 bufferSize; |
05 |
private Int32 numSize; |
06 |
private Int32 currentIndex; |
07 |
private Stack<Int32> freeIndexPool; |
08 |
internal Boolean SetBuffer(SocketAsyncEventArgs args) |
10 |
if ( this .freeIndexPool.Count > 0) |
12 |
args.SetBuffer( this .buffer, this .freeIndexPool.Pop(), this .bufferSize); |
16 |
if (( this .numSize - this .bufferSize) < this .currentIndex) |
20 |
args.SetBuffer( this .buffer, this .currentIndex, this .bufferSize); |
21 |
this .currentIndex += this .bufferSize; |
5.RequestHandler类:这里代码就不贴了,这个类也比较简单。比如发送方要发送的内容为:hello,nice to meet you那么真正发送的内容是:[length=22]hello,nice to meet you,length后的数字是字符串的长度,接收方接收到消息后根据长度检验和获取信息。
强烈推荐这篇文章:http://www.cnblogs.com/JimmyZhang/archive/2008/09/16/1291854.html
6.SocketListener类:终于到了最重要的部分了,也是一个对外部真正有意义的类,这个类监听用户的连接请求并从连接池取出一个可用连接给用户,并且时刻监听用户发来的数据并处理。在设计这个类时为了迎合双工通信我把监听的任务放到另一个线程中去,这也是我为什么要给每个用户两个SocketAsyncEventArgs的原因。当然两个线程是不够的还要异步。比较重要的语句我都用粗体标注了。socket的方法都是成对出现的,ReceiveAsync对应OnReceiveCompleted,SendAsync对应OnSendCompleted,所以理解起来也不算太难,只是要注意一点:就是接收和发送消息是在两个线程里的。
001 | public sealed class SocketListener:IDisposable |
006 | private BufferManager bufferManager; |
010 | private Socket listenSocket; |
014 | private static Mutex mutex = new Mutex(); |
018 | private Int32 numConnections; |
022 | private Int32 numConcurrence; |
026 | private ServerState serverstate; |
030 | private const Int32 opsToPreAlloc = 1; |
034 | private SocketAsyncEventArgsPool readWritePool; |
038 | private Semaphore semaphoreAcceptedClients; |
042 | private RequestHandler handler; |
046 | /// <param name="IP"></param> |
047 | /// <returns></returns> |
048 | public delegate string GetIDByIPFun( string IP); |
052 | private GetIDByIPFun GetIDByIP; |
056 | /// <param name="info"></param> |
057 | public delegate void ReceiveMsgHandler( string uid, string info); |
061 | public event ReceiveMsgHandler OnMsgReceived; |
065 | public delegate void StartListenHandler(); |
069 | public event StartListenHandler StartListenThread; |
073 | /// <param name="successorfalse"></param> |
074 | public delegate void SendCompletedHandler( string uid, string exception); |
078 | public event SendCompletedHandler OnSended; |
082 | public Int32 NumConnections |
084 | get { return this .numConnections; } |
089 | public Int32 MaxConcurrence |
091 | get { return this .numConcurrence; } |