IOCP 服务端案例

IOCP

Epoll 和 IOCP性能比较

    每种操作系统(内核级)都会提供特有的I/O模型以提高性能。其中Linux的Epoll、BSD的Kqueue、Windows的IOCP。它们都是在操作系统级别上提供支持并且完成相关功能。那么问题来了:到底是Epoll优于IOCP 还是后者优于前者呢? 个人认为至少对于目前的我来说,这两种模型都是非常优秀的(因为我也不知道谁更优秀)。个人认为不管是Eopll还是IOCP都具有各自独特的优点,但是这并非左右了服务器性能的因素。至少他们的工作机制不同,所以对于Epoll或者IOCP,各位自行判断。

知识补给

    IOCP不仅是负责I/O工作,也还有至少创建1个线程并使其负责全部I/O的前后处理。理解IOCP的时候不要把目光着重集中到线程上,而是要注意一下两点:
    1、I/O是否是以非阻塞模式工作?
    2、怎样确定非阻塞模式的I/O是否完成了工作?

首先介绍相关函数

#include <windows.h>
HANDLE CreateIoCompletionPort(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR CompletionKey,DWORD NumberOfConcurrentThreads);

    FileHandle:创建CP对象时传递 INVALID_HANDLE_VALUE。
    ExistingCompletionPort:创建CP对象时传递NULL。
    CompletionKey:创建CP对象时传递0。
    NumberOfConcurrentThreads:分配给CP对象的用于处理I/O的线程数。如该参数为2时,则分配给CP对象的可以同时运行的线程数最多为2个。如果为0,则系统中的CPU个数就是可同时运行的最大线程数。

ps:该函数可以用于创建也可以用于连接。连接参数说明在下面

    FileHandle:要连接到的CP对象套接字句柄
    ExistingCompletionPort:要连接套接字的CP对象句柄
    CompletionKey:传递已完成I/O相关信息
    NumberOfConcurrentThreads:无论传递何值,只要该函数的第二个参数非NULL就会自动忽略。

确认完成端口完成的I/O和线程的I/O处理;

#include <windows.h>
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort,LPDWORD lpNumberOfBytes,PULONG_PTR lpCompletionKey,LPOVERLAPPED * lpOverlapped,DWORD dwMilliseconds);

    CompletionPort:注册有已经完成I/O信息的CP对象句柄
    lpNumberOfBytes:用于保存I/O过程中传输的数据大小的变量地址值
    lpCompletionKey:用于保存CreateIoCompletionPort函数的第三个参数的变量地址值。
    lpOverlapped:用于保存调用WSASend、WSARecv函数时传递的OVERLAPPED结构体地址的变量地址值。
    dwMilliseconds:超时信息,超过该指定时间后返回FALSE并跳出函数。传递INFINITE时,程序将阻塞,知道已经有完成I/O信息写入CP对象。

*下面上主题菜,注意两个结构体的定义,并且留意结构体是何时被分配空间、何时被传递、如何被使用的。

#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <WinSock2.h>
#include <Windows.h>;

#define BUF_SIZE 100
#define READ 3
#define WRITE 5

/*注意这两个结构体是何时分配空间,如何被传递,如何被使用*/
typedef struct {                            //  *保存与客户端相连接套接字的结构体。
    SOCKET nClientSock;
    SOCKADDR_IN pClientAdr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

typedef struct {                            //  *将I/O中使用的缓冲和重叠I/O中需要的OVERLAPPED结构体变量封装到同一结构体中进行定义。
    OVERLAPPED overlapped_;
    WSABUF wsabuf_;
    char buffer[BUF_SIZE];
    int rwMode;                             //  Read or Write;
}PER_IO_DATA, *LPPER_IO_DATA;
/*在观察着两个结构时,请明确:结构体变量地址值与第一个成员的地址值相同*/

//  本应该为 DWORD 的类型,应该是API更新了,使用 unsigned
unsigned WINAPI EchoThreadMain (LPVOID lpComPort);
void Error_Handing (char *message);

int main () {

    WSADATA wsaDATA;
    HANDLE pComPort;
    SYSTEM_INFO sysInfo;
    LPPER_IO_DATA ioInfo;
    LPPER_HANDLE_DATA handleInfo;

    SOCKET nServSock;
    SOCKADDR_IN servAdr;
    int recvByte, i, flags = 0;

    if ( WSAStartup (MAKEWORD (2, 2), &wsaDATA) != 0 )
        Error_Handing ("Server Verision Fail.");
    //  创建CP对象,最后一个参数为0,取系统最大线程数(相当于CPU的核数)
    pComPort = CreateIoCompletionPort (INVALID_HANDLE_VALUE, NULL, 0, 0);
    GetSystemInfo (&sysInfo);   //  获取当前系统信息,没有太大意义,记住就好
    for ( i = 0; i < sysInfo.dwNumberOfProcessors; i++ ) {
        //  dwNumberOfProcessors 写入CPU个数 双核为2 四核为4,这个for循环创建了与CPU个数相当的线程,另外创建线程时传递前面的CP对象句柄,线程将通过该句柄访问CP对象。白话就是 CP对象将通过该句柄分配到线程。
        _beginthreadex (NULL, 0, EchoThreadMain, (LPVOID) pComPort, 0, NULL);
    }

    nServSock = WSASocket (AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    memset (&servAdr, 0, sizeof (servAdr));
    servAdr.sin_family = AF_INET;
    servAdr.sin_addr.s_addr = inet_addr ("192.168.1.101");
    servAdr.sin_port = htons (9130);

    bind (nServSock, (SOCKADDR*) &servAdr, sizeof (servAdr));
    listen (nServSock, 5);

    while ( 1 ) {
        SOCKET nClientSock;
        SOCKADDR_IN pClientAdr;
        int addrLen = sizeof (pClientAdr);

        nClientSock = accept (nServSock, (SOCKADDR*) &pClientAdr, &addrLen);
        /*动态分配PER_HANDLE_DATA 结构体,并且写入客户端套接字核客户端地址信息*/
        handleInfo = (LPPER_HANDLE_DATA) malloc (sizeof (PER_HANDLE_DATA));
        handleInfo->nClientSock = nClientSock;
        memcpy (&( handleInfo->pClientAdr ), &pClientAdr, addrLen);

        /*连接前者创建的CP对象和前者创建的SOCKET套接字。针对套接字的重叠I/O完成时,已完成信息会写入连接的CP对象,这会将引起GetQueue..函数的返回。注意第三参数的值。其值时前者声明并且初始化的结构体变量地址值,它同样是在GetQueued...函数返回时得到*/
        CreateIoCompletionPort ((HANDLE) nClientSock, pComPort, (DWORD) handleInfo, 0);
        /*动态分配PER_IO_DATA结构体变量空间。相当于同时准备了WSARecv函数中需要的OVERLAPPED结构体变量、WSABUF变量以及缓冲*/
        ioInfo = (LPPER_IO_DATA) malloc (sizeof (PER_IO_DATA));
        memset (&( ioInfo->overlapped_ ), 0, sizeof (OVERLAPPED));
        ioInfo->wsabuf_.buf = ioInfo->buffer;
        ioInfo->wsabuf_.len = BUF_SIZE;
        ioInfo->rwMode = READ;  //  IOCP不会帮我们区分完成输入和完成输出状态。只通知I/O完成状态,因此需要额外的变量来记录这两种状态。PER_IO_DATA结构体中的rwMode就是用于该功能记录
        /*WSARecv 第六个参数为OVERLAPPED变量地址值,该值可以在GetQueued...函数返回时得到。但是该结构体地址值与第一个成员的地址值相同,相当于传入了PER_IO_DATA变量地址值*/
        WSARecv (handleInfo->nClientSock, &( ioInfo->wsabuf_ ), 1, (unsigned long*) &recvByte, (unsigned long*) &flags, &( ioInfo->overlapped_ ), NULL);
    }

    return 0;
}

//  由线程运行的函数
unsigned WINAPI EchoThreadMain (LPVOID lpComPort) {
    HANDLE hComPort = (HANDLE) lpComPort;
    SOCKET sock;
    DWORD byteTrans;
    LPPER_HANDLE_DATA handleInfo;
    LPPER_IO_DATA ioInfo;
    DWORD flags;

    while ( 1 ) {
        //  在I/O完成且已经注册相关信息时返回(最后一个参数为INFINITE)。另外返回时可以通过第三和第四个参数得到之前的2个信息。
        GetQueuedCompletionStatus (hComPort, &byteTrans, (LPDWORD) &handleInfo, (LPOVERLAPPED*) &ioInfo, INFINITE);
        sock = handleInfo->nClientSock;
        /*指针ioInfo中保存的既是OVERLAPPED变量地址值,也是PER_IO_DATA变量地址值。因此可以检查rwMode成员中的值判断是输入完成还是输出完成*/
        if ( ioInfo->rwMode == READ ) {
            puts ("Message received!");
            if ( byteTrans == 0 ) {
                closesocket (sock);
                free (handleInfo);
                free (ioInfo);
                continue;
            }

            /*将服务器收到的消息发送给客户端*/
            memset (&( ioInfo->overlapped_ ), 0, sizeof (OVERLAPPED));
            ioInfo->wsabuf_.len = byteTrans;
            ioInfo->rwMode = WRITE;
            WSASend (sock, &( ioInfo->wsabuf_ ), 1, NULL, 0, ( &ioInfo->overlapped_ ), NULL);

            /*再次发送消息后接受客户端消息*/
            ioInfo = (LPPER_IO_DATA) malloc (sizeof (PER_IO_DATA));
            memset (&ioInfo->overlapped_, 0, sizeof (OVERLAPPED));
            ioInfo->wsabuf_.len = BUF_SIZE;
            ioInfo->wsabuf_.buf = ioInfo->buffer;
            ioInfo->rwMode = READ;
            WSARecv (sock, &( ioInfo->wsabuf_ ), 1, NULL, &flags, &( ioInfo->overlapped_ ), NULL);
        } else {    //  完成的I/O为输出时执行的else区域
            puts ("message sent!");
            free (ioInfo);
        }
    }
    return 0;
}
void Error_Handing (char *message) {
    fputs (message, stderr);
    fputc ('\n', stderr);
    exit (1);
}

客户端就不提供了,前一章已经实现了客户端,虽然是不是回声,但是做测试时可以的了,也可以百度下一些TCP/UDP连接测试工具进行测试。IOCP多看看代码,多理解。

最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口(IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据(发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值