网络上有非常详尽的文章,见http://blog.csdn.net/piggyxp/article/details/6922277,感谢他。
完成端口是MS提供的异步通知队列模型,可用于文件读写,管道,网络通讯等,这里只涉及到网络(socket)使用完成端口。
使用windows socket库需使用如下初始化
int WSAStartup(
__in WORD wVersionRequested,
__out LPWSADATA lpWSAData
);
退出时需要做如下清理
int WSACleanup(void);
HANDLE WINAPI CreateIoCompletionPort(
__in HANDLE FileHandle,
__in HANDLE ExistingCompletionPort,
__in ULONG_PTR CompletionKey,
__in DWORD NumberOfConcurrentThreads
);
需要关联的句柄,可以是文件/管道/套接字。设置为NULL表示新创建一个IOCP句柄。
新创建IOCP的情况下,此处设置为NULL,否则应设置为想关联之IOCP句柄。
用户自定义参数,与每一个句柄相关联。此处用于可以记录每一个套接字相关的信息。
完成端口最大调度工作线程数。当有网络数据到达时,完成队列会唤醒工作线程进行处理。这个参数用于记录可同时唤醒的线程数。置0标识使用计算机CPU核心个数。
SOCKET WSASocket(
__in int af,
__in int type,
__in int protocol,
__in LPWSAPROTOCOL_INFO lpProtocolInfo,
__in GROUP g,
__in DWORD dwFlags
);
WSASocket(AF_INET,
SOCK_STREAM,
IPPROTO_TCP,
NULL,
0,
WSA_FLAG_OVERLAPPED);
唯一需注意的是要使用WSA_FLAG_OVERLAPPED参数。接下来关联到完成端口,这里使用了一个对象来记录套接字相关的信息。
CreateIoCompletionPort((HANDLE)m_sk, hiocp, (ULONG_PTR)this, 0));
SYSTEM_INFO si = {0};
GetSystemInfo(&si);
for (DWORD i = 0; i < si.dwNumberOfProcessors * 2; i++)
{
HANDLE hThread = CreateThread(NULL,
0,
workThreadProc,
(LPVOID)g_hiocp,
0,
NULL);
if (NULL == hThread)
{
//error handle...
}
phThreads[i] = hThread;
}
这里先获取系统CPU核心数量,然后建立CPU核心数*2个数的工作线程。CPU核心数*2是个推荐值,在IOCP一轮的调度下,最多会唤醒CPU核心数个工作线程进行处理,不排除某些处理线程处理时间较长,或者进入等待状态。这里设置2倍的线程数就可以保证IOCP有足够的线程可以调度,满足数据处理的高效性。
投递accept的步骤如下
for (DWORD j = 0; j < si.dwNumberOfProcessors * 2; j++)
{
IocpSocket* icskNew = icskLsn->postAccept(); //投递accept请求
icskNew->attachIocp(g_hiocp); // 绑定IOCP
g_iocpMgr.add(icskNew); // 添加至活动socket列表
}
在postAccept中使用异步AcceptEx调用,原型如下
BOOL AcceptEx(
__in SOCKET sListenSocket,
__in SOCKET sAcceptSocket,
__in PVOID lpOutputBuffer,
__in DWORD dwReceiveDataLength,
__in DWORD dwLocalAddressLength,
__in DWORD dwRemoteAddressLength,
__out LPDWORD lpdwBytesReceived,
__in LPOVERLAPPED lpOverlapped
);
使用accpet时,调用会等待,当新的连接请求到达时,会返回一个新的连接socket。而AcceptEx是异步操作,函数调用后马上返回,那么新的连接socket就必须预先创建好。
// 创建流套接字
WSASocket(AF_INET,
SOCK_STREAM,
IPPROTO_TCP,
NULL,
0,
WSA_FLAG_OVERLAPPED);
AcceptEx原型如下
BOOL AcceptEx(
__in SOCKET sListenSocket,
__in SOCKET sAcceptSocket,
__in PVOID lpOutputBuffer,
__in DWORD dwReceiveDataLength,
__in DWORD dwLocalAddressLength,
__in DWORD dwRemoteAddressLength,
__out LPDWORD lpdwBytesReceived,
__in LPOVERLAPPED lpOverlapped
);
监听套接字
预创建的套接字,用于新的连接,未被绑定或连接
数据接收缓冲区。依次存放:本地地址,远端地址,接收的数据
接收数据缓冲区长度,不包括本地地址和远端地址
本地地址缓冲区长度,大小为:地址长度+16bytes
远端地址缓冲区长度,大小为:地址长度+16bytes
接收到的数据的长度。异步调用下,此参数不会返回数值,可以置NULL。
OVERLAPPED重叠结构,不可以为空
这里需要注意的有两点。
第一是缓冲区的配置。accept异步事件到达时,会携带有连接信息及第一帧数据,所以这里的缓冲区的数据结构为本地地址+16bytes+远端地址+16bytes+第一帧数据。数据可以不用自己解析,MS提供了GetAcceptExSockaddrs函数。
第二个是OVERLAPPED重叠结构。windows很多的异步操作需要使用这个结构。每个OVERLAPPED与一个操作关联,也就是说如果需要投递多个操作,就需要分配多个OVERLAPPED结构。当然一个操作事件处理完毕后,可以重用这个“已处理完”的OVERLAPPED。传入时直接将结构初始化为0即可。
由以上两点,这里可以编制一个自定义的“异步请求”结构体,记录每个操作需要的信息
#define OPRT_BUF_SIZE 4096
#define OVP_FLAG_ACCEPT 1
#define OVP_FLAG_RECEIVE 2
#define OVP_FLAG_SEND 3
struct OverlapRequest
{
OverlapRequest() {ZeroMemory(this, sizeof(OverlapRequest));}
DWORD flag; // 记录本次操作的标识
OVERLAPPED ov; // 重叠结构
BYTE buf[OPRT_BUF_SIZE]; // 接收缓冲区
IocpSocket* icsk; // accept情况下,此处记录新接入的socket
};
投递好accept请求后,需要将新的socket关联至IOCP。当然这个也可以放在工作线程中accept事件处理的时候进行。具体的关联操作与监听套接字一致。
销毁的动作放到后面再述说,这里先切换到工作线程,讲一下工作线程的处理。
工作线程的结构如下
// work threads
DWORD WINAPI workThreadProc(LPVOID lpParam)
{
DWORD dwBytes;
OVERLAPPED* ov;
IocpSocket* icsk;
OverlapRequest* req;
while (TRUE)
{
BOOL ret = GetQueuedCompletionStatus(g_hiocp,
&dwBytes,
(PULONG_PTR)&icsk,
&ov,
INFINITE);
if (NULL == icsk)
{
//收到退出信号
break;
}
if (ret)
{
// 获取OverlapRequest
req = CONTAINING_RECORD(ov, OverlapRequest, ov);
switch (req->flag)
{
case OVP_FLAG_ACCEPT:
// 1.继续发送新的accept请求
// 2.分发新连接的receive请求(如果之前没绑定IOCP,此处需将新连接绑定IOCP)
// 3.解析,处理数据
break;
case OVP_FLAG_RECEIVE:
if (dwBytes > 0)
{
// 1.继续发送receive请求
// 2.处理数据
}
else
{
// 远端关闭连接
// 关闭此连接,从全局活动连接表中移除
}
break;
case OVP_FLAG_SEND:
// 1.处理数据
// 2.新的发送请求
break;
}
}
else
{
// 移除连接
}
}
return 0;
}
BOOL WINAPI GetQueuedCompletionStatus(
__in HANDLE CompletionPort,
__out LPDWORD lpNumberOfBytes,
__out PULONG_PTR lpCompletionKey,
__out LPOVERLAPPED* lpOverlapped,
__in DWORD dwMilliseconds
);
完成端口句柄
返回接收数据大小
与socket关联的用户自定义数据
操作相关的OVERLAPPED结构。宏CONTAINING_RECORD可将其转换为我们自定义的结构地址,即是OverlapRequest对象。
等待超时的时间。INFINITE表示无超时。
函数GetQueuedCompletionStatus等待套接字事件并将结果返回。在accept事件中,收到的数据包含有本地地址,远端地址,第一帧数据,可以使用函数GetAcceptExSockaddrs提取各部分信息。注意在每个事件处理后,如果需要更进一步的数据,需要手动的再次分发请求。
资源的销毁主要有两方面,一是工作线程的结束,在一个就是活动连接的关闭。
BOOL WINAPI PostQueuedCompletionStatus(
__in HANDLE CompletionPort,
__in DWORD dwNumberOfBytesTransferred,
__in ULONG_PTR dwCompletionKey,
__in LPOVERLAPPED lpOverlapped
);
函数PostQueuedCompletionStatus可以直接将事件投递到工作线程。dwCompletionKey和lpOverlapped将直接反映在GetQueuedCompletionStatus函数返回的结果中。因此我们可以投递NULL值以指示工作线程退出。
DWORD dwCount = si.dwNumberOfProcessors * 2;
for (DWORD i = 0; i < dwCount; i++)
PostQueuedCompletionStatus(g_hiocp, 0, NULL, NULL);
for (DWORD j = 0; j < dwCount; j++)
{
WaitForSingleObject(phThreads[j], INFINITE);
CloseHandle(phThreads[j]);
}
建立全局的列表,socket创建时加入,socket销毁移出。
关闭IOCP句柄,cleanup windows socket库
这个是个人做demo的记录。我觉得精简扼要的步骤好过详细的解释。正如先长出了主干,枝叶茂密是最终的事情。其他诸如需要注意的如异常处理,多线程访问资源的同步。
代码地址http://download.csdn.net/detail/janvi/4806053