高性能socket设计实现
2010-08-31 14:33 田志良 阅读(20306) 评论(17) 编辑 收藏 举报因为学习的需要,要求一个高性能的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 2 3 4 5 6 7 8 | internal sealed class MySocketAsyncEventArgs : SocketAsyncEventArgs { internal string UID; private string Property; internal MySocketAsyncEventArgs( string property){ this .Property = property; } } |
UID:用户标识符,用来标识这个连接是那个用户的。
Property:标识该连接是用来发送信息还是监听接收信息的。param:Receive/Send,MySocketAsyncEventArgs类只带有一个参数的构造函数,说明类在实例化时就被说明是用来完成接收还是发送任务的。
2.SocketAsyncEventArgsWithId类:该类是一个用户的连接的最小单元,也就是说对一个用户来说有两个SocketAsyncEventArgs对象,这两个对象是一样的,但是有一个用来发送消息,一个接收消息,这样做的目的是为了实现双工通讯,提高用户体验。默认的用户标识是"-1”,状态是false表示不可用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | internal sealed class SocketAsyncEventArgsWithId:IDisposable { private string uid = "-1" ; private bool state = false ; private MySocketAsyncEventArgs receivesaea; private MySocketAsyncEventArgs sendsaea; internal string UID { get { return uid; } set { uid = value; ReceiveSAEA.UID = value; SendSAEA.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)方法:判断某个用户的连接是否在线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | internal sealed class SocketAsyncEventArgsPool:IDisposable { internal Stack<SocketAsyncEventArgsWithId> pool; internal IDictionary< string , SocketAsyncEventArgsWithId> busypool; private string [] keys; internal Int32 Count { get { lock ( this .pool) { return this .pool.Count; } } } internal string [] OnlineUID { get { lock ( this .busypool) { busypool.Keys.CopyTo(keys, 0); } return keys; } } internal SocketAsyncEventArgsPool(Int32 capacity) { keys = new string [capacity]; this .pool = new Stack<SocketAsyncEventArgsWithId>(capacity); this .busypool = new Dictionary< string , SocketAsyncEventArgsWithId>(capacity); } internal SocketAsyncEventArgsWithId Pop( string uid) { if (uid == string .Empty || uid == "" ) return null ; SocketAsyncEventArgsWithId si = null ; lock ( this .pool) { si = this .pool.Pop(); } si.UID = uid; si.State = true ; //mark the state of pool is not the initial step busypool.Add(uid, si); return si; } internal void Push(SocketAsyncEventArgsWithId item) { if (item == null ) throw new ArgumentNullException( "SocketAsyncEventArgsWithId对象为空" ); if (item.State == true ) { if (busypool.Keys.Count != 0) { if (busypool.Keys.Contains(item.UID)) busypool.Remove(item.UID); else throw new ArgumentException( "SocketAsyncEventWithId不在忙碌队列中" ); } else throw new ArgumentException( "忙碌队列为空" ); } item.UID = "-1" ; item.State = false ; lock ( this .pool) { this .pool.Push(item); } } internal SocketAsyncEventArgsWithId FindByUID( string uid) { if (uid == string .Empty || uid == "" ) return null ; SocketAsyncEventArgsWithId si = null ; foreach ( string key in this .OnlineUID) { if (key == uid) { si = busypool[uid]; break ; } } return si; } internal bool BusyPoolContains( string uid) { lock ( this .busypool) { return busypool.Keys.Contains(uid); } } } |
Note:这个类的设计缺陷是使用了太多的lock语句,对对象做了太多的互斥操作,所以我尽量的把lock内的语句化简或挪到lock外部执行。
4.BufferManager类:该类是一个管理连接缓冲区的类,职责是为每一个连接维持一个接收数据的区域。它的设计也采用了类似与池的技术,先实例化好多内存区域,并把每一块的地址放入栈中,每执行依次pop时拿出一块区域来给SocketAsyncEventArgs对象作为Buffer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | internal sealed class BufferManager:IDisposable { private Byte[] buffer; private Int32 bufferSize; private Int32 numSize; private Int32 currentIndex; private Stack<Int32> freeIndexPool; internal Boolean SetBuffer(SocketAsyncEventArgs args) { if ( this .freeIndexPool.Count > 0) { args.SetBuffer( this .buffer, this .freeIndexPool.Pop(), this .bufferSize); } else { if (( this .numSize - this .bufferSize) < this .currentIndex) { return false ; } args.SetBuffer( this .buffer, this .currentIndex, this .bufferSize); this .currentIndex += this .bufferSize; } return true ; } } |
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,所以理解起来也不算太难,只是要注意一点:就是接收和发送消息是在两个线程里的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | public sealed class SocketListener:IDisposable { /// <summary> /// 缓冲区 /// </summary> private BufferManager bufferManager; /// <summary> /// 服务器端Socket /// </summary> private Socket listenSocket; /// <summary> /// 服务同步锁 /// </summary> private static Mutex mutex = new Mutex(); /// <summary> /// 当前连接数 /// </summary> private Int32 numConnections; /// <summary> /// 最大并发量 /// </summary> private Int32 numConcurrence; /// <summary> /// 服务器状态 /// </summary> private ServerState serverstate; /// <summary> /// 读取写入字节 /// </summary> private const Int32 opsToPreAlloc = 1; /// <summary> /// Socket连接池 /// </summary> private SocketAsyncEventArgsPool readWritePool; /// <summary> /// 并发控制信号量 /// </summary> private Semaphore semaphoreAcceptedClients; /// <summary> /// 通信协议 /// </summary> private RequestHandler handler; /// <summary> /// 回调委托 /// </summary> /// <param name="IP"></param> /// <returns></returns> public delegate string GetIDByIPFun( string IP); /// <summary> /// 回调方法实例 /// </summary> private GetIDByIPFun GetIDByIP; /// <summary> /// 接收到信息时的事件委托 /// </summary> /// <param name="info"></param> public delegate void ReceiveMsgHandler( string uid, string info); /// <summary> /// 接收到信息时的事件 /// </summary> public event ReceiveMsgHandler OnMsgReceived; /// <summary> /// 开始监听数据的委托 /// </summary> public delegate void StartListenHandler(); /// <summary> /// 开始监听数据的事件 /// </summary> public event StartListenHandler StartListenThread; /// <summary> /// 发送信息完成后的委托 /// </summary> /// <param name="successorfalse"></param> public delegate void SendCompletedHandler( string uid, string exception); /// <summary> /// 发送信息完成后的事件 /// </summary> public event SendCompletedHandler OnSended; /// <summary> /// 获取当前的并发数 /// </summary> public Int32 NumConnections { get { return this .numConnections; } } /// <summary> /// 最大并发数 /// </summary> public Int32 MaxConcurrence { get { return this .numConcurrence; } } /// <summary> /// 返回服务器状态 /// </summary> public ServerState State { get { return serverstate; } } /// <summary> /// 获取当前在线用户的UID /// </summary> public string [] OnlineUID { get { return readWritePool.OnlineUID; } } /// <summary> /// 初始化服务器端 /// </summary> /// <param name="numConcurrence">并发的连接数量(1000以上)</param> /// <param name="receiveBufferSize">每一个收发缓冲区的大小(32768)</param> public SocketListener(Int32 numConcurrence, Int32 receiveBufferSize, GetIDByIPFun GetIDByIP) { serverstate = ServerState.Initialing; this .numConnections = 0; this .numConcurrence = numConcurrence; this .bufferManager = new BufferManager(receiveBufferSize * numConcurrence * opsToPreAlloc, receiveBufferSize); this .readWritePool = new SocketAsyncEventArgsPool(numConcurrence); this .semaphoreAcceptedClients = new Semaphore(numConcurrence, numConcurrence); handler = new RequestHandler(); this .GetIDByIP = GetIDByIP; } /// <summary> /// 服务端初始化 /// </summary> public void Init() { this .bufferManager.InitBuffer(); SocketAsyncEventArgsWithId readWriteEventArgWithId; for (Int32 i = 0; i < this .numConcurrence; i++) { readWriteEventArgWithId = new SocketAsyncEventArgsWithId(); readWriteEventArgWithId.ReceiveSAEA.Completed += new EventHandler<SocketAsyncEventArgs>(OnReceiveCompleted); readWriteEventArgWithId.SendSAEA.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted); //只给接收的SocketAsyncEventArgs设置缓冲区 this .bufferManager.SetBuffer(readWriteEventArgWithId.ReceiveSAEA); this .readWritePool.Push(readWriteEventArgWithId); } serverstate = ServerState.Inited; } /// <summary> /// 启动服务器 /// </summary> /// <param name="data">端口号</param> public void Start(Object data) { Int32 port = (Int32)data; IPAddress[] addresslist = Dns.GetHostEntry(Environment.MachineName).AddressList; IPEndPoint localEndPoint = new IPEndPoint(addresslist[addresslist.Length - 1], port); this .listenSocket = new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); if (localEndPoint.AddressFamily == AddressFamily.InterNetworkV6) { this .listenSocket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, false ); this .listenSocket.Bind( new IPEndPoint(IPAddress.IPv6Any, localEndPoint.Port)); } else { this .listenSocket.Bind(localEndPoint); } this .listenSocket.Listen(100); this .StartAccept( null ); //开始监听已连接用户的发送数据 StartListenThread(); serverstate = ServerState.Running; mutex.WaitOne(); } /// <summary> /// 开始监听线程的入口函数 /// </summary> public void Listen() { while ( true ) { string [] keys = readWritePool.OnlineUID; foreach ( string uid in keys) { if (uid != null && readWritePool.busypool[uid].ReceiveSAEA.LastOperation != SocketAsyncOperation.Receive) { Boolean willRaiseEvent = (readWritePool.busypool[uid].ReceiveSAEA.UserToken as Socket).ReceiveAsync(readWritePool.busypool[uid].ReceiveSAEA); if (!willRaiseEvent) ProcessReceive(readWritePool.busypool[uid].ReceiveSAEA); } } } } /// <summary> /// 发送信息 /// </summary> /// <param name="uid">要发送的用户的uid</param> /// <param name="msg">消息体</param> public void Send( string uid, string msg) { if (uid == string .Empty || uid == "" || msg == string .Empty || msg == "" ) return ; SocketAsyncEventArgsWithId socketWithId = readWritePool.FindByUID(uid); if (socketWithId == null ) //说明用户已经断开 //100 发送成功 //200 发送失败 //300 用户不在线 //其它 表示异常的信息 OnSended(uid, "300" ); else { MySocketAsyncEventArgs e = socketWithId.SendSAEA; if (e.SocketError == SocketError.Success) { int i = 0; try { string message = @"[lenght=" + msg.Length + @"]" + msg; byte [] sendbuffer = Encoding.Unicode.GetBytes(message); e.SetBuffer(sendbuffer, 0, sendbuffer.Length); Boolean willRaiseEvent = (e.UserToken as Socket).SendAsync(e); if (!willRaiseEvent) { this .ProcessSend(e); } } catch (Exception ex) { if (i <= 5) { i++; //如果发送出现异常就延迟0.01秒再发 Thread.Sleep(10); Send(uid, msg); } else { OnSended(uid, ex.ToString()); } } } else { OnSended(uid, "200" ); this .CloseClientSocket(((MySocketAsyncEventArgs)e).UID); } } } /// <summary> /// 停止服务器 /// </summary> public void Stop() { if (listenSocket!= null ) listenSocket.Close(); listenSocket = null ; Dispose(); mutex.ReleaseMutex(); serverstate = ServerState.Stoped; } private void StartAccept(SocketAsyncEventArgs acceptEventArg) { if (acceptEventArg == null ) { acceptEventArg = new SocketAsyncEventArgs(); acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); } else acceptEventArg.AcceptSocket = null ; this .semaphoreAcceptedClients.WaitOne(); Boolean willRaiseEvent = this .listenSocket.AcceptAsync(acceptEventArg); if (!willRaiseEvent) { this .ProcessAccept(acceptEventArg); } } private void OnAcceptCompleted( object sender, SocketAsyncEventArgs e) { this .ProcessAccept(e); } private void ProcessAccept(SocketAsyncEventArgs e) { if (e.LastOperation != SocketAsyncOperation.Accept) //检查上一次操作是否是Accept,不是就返回 return ; if (e.BytesTransferred <= 0) //检查发送的长度是否大于0,不是就返回 return ; string UID = GetIDByIP((e.AcceptSocket.RemoteEndPoint as IPEndPoint).Address.ToString()); //根据IP获取用户的UID if (UID == string .Empty || UID == null || UID == "" ) return ; if (readWritePool.BusyPoolContains(UID)) //判断现在的用户是否已经连接,避免同一用户开两个连接 return ; SocketAsyncEventArgsWithId readEventArgsWithId = this .readWritePool.Pop(UID); readEventArgsWithId.ReceiveSAEA.UserToken = e.AcceptSocket; readEventArgsWithId.SendSAEA.UserToken = e.AcceptSocket; Interlocked.Increment( ref this .numConnections); this .StartAccept(e); } private void OnReceiveCompleted( object sender, SocketAsyncEventArgs e) { ProcessReceive(e); } private void OnSendCompleted( object sender, SocketAsyncEventArgs e) { ProcessSend(e); } private void ProcessReceive(SocketAsyncEventArgs e) { if (e.LastOperation != SocketAsyncOperation.Receive) return ; if (e.BytesTransferred > 0) { if (e.SocketError == SocketError.Success) { Int32 byteTransferred = e.BytesTransferred; string received = Encoding.Unicode.GetString(e.Buffer, e.Offset, byteTransferred); //检查消息的准确性 string [] msg = handler.GetActualString(received); foreach ( string m in msg) OnMsgReceived(((MySocketAsyncEventArgs)e).UID, m); //可以在这里设一个停顿来实现间隔时间段监听,这里的停顿是单个用户间的监听间隔 //发送一个异步接受请求,并获取请求是否为成功 Boolean willRaiseEvent = (e.UserToken as Socket).ReceiveAsync(e); if (!willRaiseEvent) ProcessReceive(e); } } else this .CloseClientSocket(((MySocketAsyncEventArgs)e).UID); } private void ProcessSend(SocketAsyncEventArgs e) { if (e.LastOperation != SocketAsyncOperation.Send) return ; if (e.BytesTransferred > 0) { if (e.SocketError == SocketError.Success) OnSended(((MySocketAsyncEventArgs)e).UID, "100" ); else OnSended(((MySocketAsyncEventArgs)e).UID, "200" ); } else this .CloseClientSocket(((MySocketAsyncEventArgs)e).UID); } private void CloseClientSocket( string uid) { if (uid == string .Empty || uid == "" ) return ; SocketAsyncEventArgsWithId saeaw = readWritePool.FindByUID(uid); if (saeaw == null ) return ; Socket s = saeaw.ReceiveSAEA.UserToken as Socket; try { s.Shutdown(SocketShutdown.Both); } catch (Exception) { //客户端已经关闭 } this .semaphoreAcceptedClients.Release(); Interlocked.Decrement( ref this .numConnections); this .readWritePool.Push(saeaw); } #region IDisposable Members public void Dispose() { bufferManager.Dispose(); bufferManager = null ; readWritePool.Dispose(); readWritePool = null ; } #endregion } |
关于所有的类已经介绍完了,相信各位都已经很明白了,如果不是太清楚就看源代码,别忘了源代码是最好的文档!
当然这个2.0仍然还有很多缺陷,比如职责划分不太OO,运行不太稳定,处理异常能力较差,处理超负载的连接能力较差,主动拒绝,可测试性差等,希望大家多给点建议,改进才对啊。
源代码下载:https://files.cnblogs.com/niuchenglei/socketlib.rar
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?