c# AcceptEx与完成端口(IOCP)结合的示例
前言
在windows平台下实现高性能网络服务器,iocp(完成端口)是唯一选择。编写网络服务器面临的问题有:
1 快速接收客户端的连接。
2 快速收发数据。
3 快速处理数据。本文主要解决第一个问题。
acceptex函数定义
bool acceptex( socket slistensocket, socket sacceptsocket, pvoid lpoutputbuffer, dword dwreceivedatalength, dword dwlocaladdresslength, dword dwremoteaddresslength, lpdword lpdwbytesreceived, lpoverlapped lpoverlapped );
为什么要用acceptex
传统的accept函数能满足大部分场景的需要;但在某些极端条件下,必须使用acceptex来实现。两个函数的区别如下:
1)accept是阻塞的;在一个端口监听,必须启动一个专用线程调用accept。当然也可以用迂回的方式,绕过这个限制,处理起来会很麻烦,见文章单线程实现同时监听多个端口。acceptex是异步的,可以同时对很多端口监听(监听端口的数量没有上限的限制)。采用迂回的方式,使用accept监听,一个线程最多监听64个端口。这一点可能不是acceptex最大优点,毕竟同时对多个端口监听的情况非常少见。
2)acceptex可以返回更多的数据。a)acceptex可以返回本地和对方ip地址和端口;而不需要调用函数getsockname和getpeername获取网络地址了。b)acceptex可以再接收到一段数据后,再返回。这种做法有利有弊,一般不建议这样做。
3)acceptex是先准备套接字(socket)后接收。为了应对突发的连接高峰,可以多次投放acceptex。accept是事后建立socket,就是tcp三次握手完成后,accept调用才返回,再生成socket。生成套接字是相对比较耗时的操作,accept的方式无法及时处理突发连接。对于acceptex的处理方式为建议做如下处理:一个线程负责创建socket,一个线程负责处理acceptex返回。
以上仅仅通过文字说明了acceptex的特点。下面通过具体代码,逐一剖析。我将acceptex的处理封装到类iocpacceptex中。编写该类时,尽量做到高内聚低耦合,使该类可以方便的被其他模块使用。
iocpacceptex外部功能说明
class iocpacceptex { public: iocpacceptex(); ~iocpacceptex(); //设置回调接口。当accept成功,调用回调接口。 void setcallback(iacceptcallback* callback); // 增加监听端口 void addlistenport(uint16 port); //启动服务 bool start(); void stop(); 。。。以下代码省略 } #define post_accept 1 //使用iocpacceptex类,必须实现该接口。接收客户端的连接 class iacceptcallback { public: virtual void onacceptclient(socket hsocketclient, uint16 nlistenport) = 0; };
该类的调用函数很简单,对外接口也很明确。说明该类的职责很清楚,这也符合单一职责原则。
实现步骤说明
acceptex不但需要与监听端口绑定,还需要与完成端口绑定。所以程序的第一步是创建完成端口:
a)创建完成端口
m_hiocp = createiocompletionport(invalid_handle_value, null, null, 0); if (m_hiocp == null) return false;
b)监听端口创建与绑定
//生成套接字 socket serversocket = wsasocket(af_inet, sock_stream, ipproto_tcp, null, 0, wsa_flag_overlapped); if (serversocket == invalid_socket) { return false; } //绑定 sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = af_inet; addr.sin_addr.s_addr = inaddr_any ; addr.sin_port = htons(port); if (bind(serversocket, (sockaddr *)&addr, sizeof(addr)) != 0) { closesocket(serversocket); serversocket = invalid_socket; return false; } //启动监听 if (listen(serversocket, somaxconn) != 0) { closesocket(serversocket); serversocket = invalid_socket; return false; } //监听端口与完成端口绑定 if (createiocompletionport((handle)serversocket, m_hiocp, (ulong_ptr)this, 0) == null) { closesocket(serversocket); serversocket = invalid_socket; return false; }
c)投递acceptex
struct acceptoverlapped { overlapped overlap; int32 optype; socket serversocket; socket clientsocket; char lpoutputbuf[128]; dword dwbytes; }; int iocpacceptex::newaccept(socket serversocket) { //创建socket socket _socket = socket(af_inet, sock_stream, ipproto_tcp); acceptoverlapped *ov = new acceptoverlapped(); zeromemory(ov,sizeof(acceptoverlapped)); ov->optype = post_accept; ov->clientsocket = _socket; ov->serversocket = serversocket; //存放网络地址的长度 int addrlen = sizeof(sockaddr_in) + 16; int bretval = acceptex(serversocket, _socket, ov->lpoutputbuf, 0,addrlen, addrlen, &ov->dwbytes, (lpoverlapped)ov); if (bretval == false) { int error = wsagetlasterror(); if (error != wsa_io_pending) { closesocket(_socket); return 0; } } return 1; }
acceptex是非阻塞操作,调用会立即返回。当有客户端连接时,怎么得到通知。答案是通过完成端口返回。注意有一个步骤:监听端口与完成端口绑定,就是serversocket与m_hiocp绑定,所以当有客户端连接serversocket时,m_hiocp会得到通知。需要生成线程,等待完成端口的通知。
d)通过完成端口,获取通知
dword dwbytestransferred; ulong_ptr key; bool rc; int error; acceptoverlapped *lpperiodata = null; while (m_bserverstart) { error = no_error; rc = getqueuedcompletionstatus( m_hiocp, &dwbytestransferred, &key, (lpoverlapped *)&lpperiodata, infinite); if (rc == false) { error = 0; if (lpperiodata == null) { dword lasterror = getlasterror(); if (lasterror == wait_timeout) { continue; } else { assert(false); return lasterror; } } } if (lpperiodata != null) { switch (lpperiodata->optype) { case post_accept: { oniocpaccept(lpperiodata, dwbytestransferred, error); } break; } } else { } } return 0;
dword winapi iocpacceptex::acceptexthreadpool(pvoid pcontext) { threadpoolparam *param = (threadpoolparam*)pcontext; param->piocpacceptex->newaccept(param->servesocket); delete param; return 0; } int iocpacceptex::oniocpaccept(acceptoverlapped *acceptdata, int translen, int error) { m_iacceptcallback->onacceptclient(acceptdata->clientsocket, acceptdata->serversocket); //当一个acceptex返回,需要投递一个新的acceptex。 //使用线程池好像有点小题大做。前文已说过,套接字的创建相对是比较耗时的操作。 //如果不在线程池投递acceptex,acceptex的优点就被抹杀了。 threadpoolparam *param = new threadpoolparam(); param->piocpacceptex = this; param->servesocket = acceptdata->serversocket; queueuserworkitem(acceptexthreadpool, this, 0); delete acceptdata; return 0; }
后记
采用完成端口是提高io处理能力的一个途径(广义上讲,通讯操作也是io)。为了提高io处理能力,windows提供很多异步操作函数,这些函数都与完成端口关联,所以这一类处理的思路基本一致。学会了acceptex的使用,可以做到触类旁通的效果。
以上就是c# acceptex与完成端口(iocp)结合的示例的详细内容,更多关于c# acceptex与完成端口(iocp)结合的资料请关注其它相关文章!
下一篇: 人脸识别技术未来或将成安防产业新星