c# AcceptEx与完成端口(IOCP)结合的示例
- 作者: 好男人不止曾小贤丶20302294
- 来源: 51数据库
- 2021-08-31
前言
在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)结合的资料请关注其它相关文章!
