完成端口 IOCP DEMO (网络)

网络上有非常详尽的文章,见http://blog.csdn.net/piggyxp/article/details/6922277,感谢他。

完成端口是MS提供的异步通知队列模型,可用于文件读写,管道,网络通讯等,这里只涉及到网络(socket)使用完成端口。

A.完成端口使用主要步骤

  • 主线程端步骤
    1. 创建完成端口
    2. 创建监听套接字,绑定完成端口
    3. 创建工作者线程
    4. 投递accept请求
    5. ------销毁资源开始-----
    6. 发送退出通知至工作线程,等待工作线程退出
    7. 关闭完成端口
    8. 关闭所有活动的套接字
  • 工作者线程步骤
    1. 等待完成端口事件
    2. 完成事件到来,根据不同的事件处理
    3. 投递新的请求

B.主线程步骤

使用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
);

FileHandle

需要关联的句柄,可以是文件/管道/套接字。设置为NULL表示新创建一个IOCP句柄。

ExistingCompletionPort

新创建IOCP的情况下,此处设置为NULL,否则应设置为想关联之IOCP句柄。

CompletionKey

用户自定义参数,与每一个句柄相关联。此处用于可以记录每一个套接字相关的信息。

NumberOfConcurrentThreads

完成端口最大调度工作线程数。当有网络数据到达时,完成队列会唤醒工作线程进行处理。这个参数用于记录可同时唤醒的线程数。置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请求

投递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
);
sListenSocket

监听套接字

sAcceptSocket

预创建的套接字,用于新的连接,未被绑定或连接

lpOutputBuffer

数据接收缓冲区。依次存放:本地地址,远端地址,接收的数据

dwReceiveDataLength

接收数据缓冲区长度,不包括本地地址和远端地址

dwLocalAddressLength

本地地址缓冲区长度,大小为:地址长度+16bytes

dwRemoteAddressLength

远端地址缓冲区长度,大小为:地址长度+16bytes

lpdwBytesReceived

接收到的数据的长度。异步调用下,此参数不会返回数值,可以置NULL。

lpOverlapped

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事件处理的时候进行。具体的关联操作与监听套接字一致。


销毁的动作放到后面再述说,这里先切换到工作线程,讲一下工作线程的处理。

C.工作线程

工作线程的结构如下

// 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
);
CompletionPort

完成端口句柄

lpNumberOfBytes

返回接收数据大小

lpCompletionKey

与socket关联的用户自定义数据

lpOverlapped

操作相关的OVERLAPPED结构。宏CONTAINING_RECORD可将其转换为我们自定义的结构地址,即是OverlapRequest对象。

dwMilliseconds

等待超时的时间。INFINITE表示无超时。

函数GetQueuedCompletionStatus等待套接字事件并将结果返回。在accept事件中,收到的数据包含有本地地址远端地址第一帧数据,可以使用函数GetAcceptExSockaddrs提取各部分信息。注意在每个事件处理后,如果需要更进一步的数据,需要手动的再次分发请求。

D.资源销毁

资源的销毁主要有两方面,一是工作线程的结束,在一个就是活动连接的关闭。

工作线程的结束

BOOL WINAPI PostQueuedCompletionStatus(
  __in          HANDLE CompletionPort,
  __in          DWORD dwNumberOfBytesTransferred,
  __in          ULONG_PTR dwCompletionKey,
  __in          LPOVERLAPPED lpOverlapped
);

函数PostQueuedCompletionStatus可以直接将事件投递到工作线程。dwCompletionKeylpOverlapped将直接反映在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

你可能感兴趣的:(coding,windows/winCE)