网络模型之IOCP服务器实例二

Posted CPP编程客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络模型之IOCP服务器实例二相关的知识,希望对你有一定的参考价值。

0x0. 前言

这是IOCP的末篇了,本次的实例使用IOCP配合扩展函数来实现服务器,并对之前的版本做一些优化,比如这里使用了内存池,日志记录,所以这也是效率最好的一个版本,作为一个例子来说已经很完整了。

因为前面已经介绍了较多的基础内容,并且也写出了实例一,所以这篇大部分内容都不会再详细讲解相关内容了,这写起来太费时间了。取而代之的是代码会全部列出来,若大家有哪里看不懂,那就说明前三篇未全部理解,这时应该再看看前面的文章。当然,若对代码有疑问,或哪里我漏写了,或有bug,可以加我vx11406488,有时间了会答复的。

0x1. 类预览

首先来预览下类的定义,看看我们需要做些什么:

 1#include <thread>  // 线程库
2#include <mutex>  // 线程同步互斥量
3#include <string>  // 不用说
4#include <memory>  // 用到智能指针
5#include <cassert>  // 断言
6#include <vector>  // 使用vector保存连接的用户
7#include <algorithm>  // 算法库
8#include <boost/pool/singleton_pool.hpp>  // 内存池
9#include <boost/format.hpp>  // 格式化字符
10#include <WinSock2.h>  // winsock2
11#include <MSWSock.h>  // 扩展函数
12#include "utilities.h"  // 包含了一些自己写的小工具,这里用到Log日志工具
13#pragma comment(lib, "ws2_32.lib")
14
15namespace network
16{
17    class IOCPServer;
18
19    using namespace utilities;
20
21    // 缓冲区大小
22    const int BUF_SIZE = 1024;
23
24    // 操作类型
25    enum class OP_TYPE {
26        OP_ACCEPT,
27        OP_READ,
28        OP_WRITE
29    };
30
31    // 单句柄数据
32    typedef struct {
33        SOCKET hSock;
34        SOCKADDR_IN sockAddr;
35    }PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
36
37    // 单IO数据
38    typedef struct {
39        WSAOVERLAPPED overlapped;
40        WSABUF wsaBuf;
41        char buf[BUF_SIZE];
42        OP_TYPE opType;
43        SOCKET clntSock;
44    }PER_IO_DATA, *LPPER_IO_DATA;
45
46    class IOCPServer
47    {

48    public:
49        using handle_spl     =  boost::singleton_pool<PER_HANDLE_DATA, sizeof(PER_HANDLE_DATA)>;
50        using io_spl         =  boost::singleton_pool<PER_IO_DATA, sizeof(PER_IO_DATA)>;
51        using handle_ptr     =  std::shared_ptr<PER_HANDLE_DATA>;
52        using phandle_vector =  std::vector<LPPER_HANDLE_DATA>;
53
54        // 构造函数
55        IOCPServer(int port);
56        ~IOCPServer();
57        void Accept();
58        void Run();
59
60    private:
61        void InitSock();  // 初始化套接字
62        void CreateIocp();  // 创建IOCP
63        bool LoadExtensionFunc();  // 加载扩展函数
64        void AcceptEx();  // 接受操作
65        void AcceptDelivery();  // 投递接受操作
66        void AcceptHandler(LPPER_IO_DATA);  // 接受处理
67        void RequestHandler();  // 请求处理
68        void RecvMsg(SOCKET, LPPER_IO_DATA);  // 接收消息
69        void SendMsg(SOCKET, LPPER_IO_DATA, const std::string& msg);  // 发送消息
70        void CloseSock(LPPER_HANDLE_DATA, LPPER_IO_DATA);  // 关闭套接字
71
72    private:
73        handle_ptr m_servSock;  // 服务器单句柄数据
74        phandle_vector m_vAcceptedSock;  // 保存连进来的用户信息 
75        LPPER_HANDLE_DATA m_clntSock;  // 客户端单句柄数据
76        LPPER_IO_DATA m_ioInfo;  // 单IO数据
77        LPFN_ACCEPTEX m_lpfnAcceptEx;  // AcceptEx函数指针
78        LPFN_GETACCEPTEXSOCKADDRS m_lpfnGetAcceptExSockaddrs;  // GetAccetExSockaddrs函数指针
79        HANDLE m_hCompPort;  // 完成端口
80        int m_nThreads;  // 线程数量
81        int m_nPort;  // 端口
82        std::mutex m_mtx;  // 同步的互斥量
83        Log log;  // 日志类
84    };
85}

大多函数名都和之前的版本保持一致,所以大家应该不会觉得太陌生,这里来说说和之前不同的部分。

先是操作类型,这里是如此定义的:

1enum class OP_TYPE {
2    OP_ACCEPT,  // 接受操作
3    OP_READ,  // 读取操作
4    OP_WRITE  // 写入操作
5};

这里使用了作用域限制的enum(scoped enum),这是C++11的东西,它的语法就是在enum后面加一个class,这样这些enums就有了作用域限制,亦即访问变成了OP_TYPE::OP_READ,于是避免了枚举元素直接包含到namespace下。这里还加了一个类型OP_ACCEPT,用于标记接受操作,后面会用到。

PER_IO_DATA中还添加了一个clntSock成员用于在”小纸条“上保存远程套接字,亦即客户端套接字。

1typedef struct {
2    WSAOVERLAPPED overlapped;
3    WSABUF wsaBuf;
4    char buf[BUF_SIZE];
5    OP_TYPE opType;
6    SOCKET clntSock;
7}PER_IO_DATA, *LPPER_IO_DATA;

接着使用声明别名简化了几个名字较长的类型,这也是C++11的东西,一般来说和typedef没什么不同,但typedef不支持模板化,而且在声明模板别名时也不如using方便:

1using handle_spl     =  boost::singleton_pool<PER_HANDLE_DATA, sizeof(PER_HANDLE_DATA)>;
2using io_spl         =  boost::singleton_pool<PER_IO_DATA, sizeof(PER_IO_DATA)>;
3using handle_ptr     =  std::shared_ptr<PER_HANDLE_DATA>;
4using phandle_vector =  std::vector<LPPER_HANDLE_DATA>;

这里还使用了boost库中的内存池singleton_pool,之前我们都是使用的new来动态分配,在客户端退出时再delete掉,这样就会有大量的申请释放动作,影响效率。其实退出客户端的内存不用急着释放,可以接着准备给后面连接的用户使用,这样就会省掉许多申请释放的操作,统一从池中分配,最后再统一释放,这就是内存池的作用。

在定义中多了许多扩展函数的类型,这些上篇讲过,主要是如何运用到IOCP中,在后面我们再来看实现。

0x2. 日志工具(Log)

在这个版本中不再使用cout来输出了,我们将相关信息记录到文件中,这样不仅可以方便查看,而且可以保存的更久。

这里使用的是我自己以前写的一个小工具,包含在"utilities.h”中,其中有一个Log类,专门用于日志记录。

这个Log工具使用起来非常简单,就像这样:

1Log log;
2log.d("");  // 调试信息
3log.i("");  // 普通信息
4log.w("");  // 警告信息
5log.e("");  // 错误信息

其中重载了operator<<,所以也可以这样使用:

1LOG_DEBUG << "" << "";  // 调度信息
2LOG_INFO << "";              // 普通信息
3LOG_WARNING << "";        // 警告信息
4LOG_ERROR << "";            // 错误信息

若没有指定目录,它会在当前程序目录下新建:

1"log/当前日期/log_xx.log"

xx对应Log等级,若是普通信息则为log_i.log,调试信息则为log_d.log,总共4个文件。这样我们就把日志类型分开了,有错误时直接看"log_e.log",有警告时直接看"log_w.log"。当然,也可以使用SetLogDestination函数来手动设置这4个文件的输出目录。

若在调试时我们想直接在dos中看,那么可以调用ENABLE_LOG_TOSTERR函数并设置为true,再想输出到文件可以重新设为false。

输出的内容中可能处于各个不同的程序,如何区分呢?可以使用ENABLE_LOG_TAG函数来为不同程序设置不同标记,设置后日志的内容为:

1"当前时间--标记内容:日志内容"

0x3. 构造与析构函数

这两个函数未做什么改变,和第一版的实现一样,可以简单的看下:

 1network::IOCPServer::IOCPServer(int port)
2    : m_nPort(port)
3{
4    m_servSock = std::make_shared<PER_HANDLE_DATA>();
5    assert(m_servSock != nullptr);
6    ZeroMemory(m_servSock.get(), sizeof(PER_HANDLE_DATA));
7
8    InitSock();
9
10    ENABLE_LOG_TAG("IOCPServer");
11}

在这里我使用ENABLE_LOG_TAG开启了日志的标志功能,并将其设为"IOCPServer",这样日志记录的时候就会包含这个标志了。

析构函数:

 1network::IOCPServer::~IOCPServer()
2{
3    for (int i = 0; i < m_nThreads; ++i)
4    {
5        PostQueuedCompletionStatus(m_hCompPort, 0xFFFFFFFFNULLNULL);
6    }
7
8    std::for_each(m_vAcceptedSock.begin(), m_vAcceptedSock.end(),
9        [&](LPPER_HANDLE_DATA& hd) {
10        closesocket(hd->hSock);
11        hd->hSock = INVALID_SOCKET;
12    });
13
14    WSACleanup();
15}

0x4. 初始化套接字与创建完成端口

InitSock函数用于初始化套接字相关,其实现如下:

 1void network::IOCPServer::InitSock()
2{
3    WSADATA wsaData;
4    auto ret = WSAStartup(0x0202, &wsaData);
5    assert(ret == 0);
6
7    // 创建支持重叠IO的套接字
8    m_servSock->hSock = WSASocketW(PF_INET, SOCK_STREAM, 0NULL0, WSA_FLAG_OVERLAPPED);
9
10    // 绑定
11    m_servSock->sockAddr.sin_family = AF_INET;
12    m_servSock->sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
13    m_servSock->sockAddr.sin_port = htons(m_nPort);
14
15    ret = bind(m_servSock->hSock, (SOCKADDR*)&m_servSock->sockAddr, sizeof(SOCKADDR_IN));
16    assert(ret != SOCKET_ERROR);
17
18    // 创建CP对象
19    CreateIocp();
20
21    // 连接服务器套接字与CP对象
22    CreateIoCompletionPort((HANDLE)m_servSock->hSock, m_hCompPort, (ULONG_PTR)m_servSock.get(), NULL);
23
24    // 监听
25    ret = listen(m_servSock->hSock, 5);
26    assert(ret != SOCKET_ERROR);
27}

在这里,和版本一不同的地方在于22行将服务器套接字与CP对象也进行了关联,这样在处理消息的线程中我们可以获得到服务端的接收操作,因为已经不用accept函数了,所以不能在那里处理了。

CreateIocp函数用于创建CP对象并开启线程,内容和实例一中相同,所以不多作解释了:

 1void network::IOCPServer::CreateIocp()
2{
3    // 创建CP对象
4    m_hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 000);
5
6    SYSTEM_INFO sysInfo;
7    GetSystemInfo(&sysInfo);
8    m_nThreads = sysInfo.dwNumberOfProcessors * 2;
9    for (int i = 0; i < m_nThreads; ++i)
10    {
11        std::thread t(&IOCPServer::RequestHandler, this);
12        t.detach();
13    }
14}

0x5. 加载扩展函数

要使用Accept和GetAcceptExSockaddrs函数,首先通过WSAIoctl函数加载到函数指针上,这是上篇的内容,这里只是专门放到了一个函数中处理:

 1bool network::IOCPServer::LoadExtensionFunc()
2{
3    GUID guidAcceptEx = WSAID_ACCEPTEX;
4    GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
5    DWORD dwBytes;
6
7    // 加载AcceptEx函数
8    auto ret = WSAIoctl(m_servSock->hSock,
9        SIO_GET_EXTENSION_FUNCTION_POINTER,
10        &guidAcceptEx, sizeof(guidAcceptEx),
11        &m_lpfnAcceptEx, sizeof(m_lpfnAcceptEx),
12        &dwBytes, NULLNULL
13    );
14
15    boost::format fmt;
16    if (ret == SOCKET_ERROR)
17    {
18        if (WSAGetLastError() != WSA_IO_PENDING)
19        {
20            fmt = boost::format("%s%d") % "Load AcceptEx faild! Error code:" % WSAGetLastError();
21            log.e(fmt.str());
22            return false;
23        }
24    }
25
26    // 加载GetAcceptExSockaddrs函数
27    ret = WSAIoctl(m_servSock->hSock,
28        SIO_GET_EXTENSION_FUNCTION_POINTER,
29        &guidGetAcceptExSockaddrs, sizeof(guidGetAcceptExSockaddrs),
30        &m_lpfnGetAcceptExSockaddrs, sizeof(m_lpfnGetAcceptExSockaddrs),
31        &dwBytes, NULLNULL
32    );
33    if (ret == SOCKET_ERROR)
34    {
35        fmt = boost::format("%s%d") % "Load GetAcceptExSockaddrs failed! Error code:" % WSAGetLastError();
36        log.e(fmt.str());
37        return false;
38    }
39
40    return true;
41}

加载事项和上篇所说一致,这里首次使用了Log来记录错误消息,因为普通字符串无法和DWORD直接相+,所以这里使用了boost中的另一个工具format,format可以格式化不同的内容,就像printf一样,使用%号来连接后面的内容,即%s%d的实值,使用起来十分简单。通过使用其中的str函数,可以获得到字符串形式的内容。

0x6. 投递接受处理

AcceptDelivery函数中,投递了10个接收处理来准备接受连接的用户:

1void network::IOCPServer::AcceptDelivery()
2{
3    for (int i = 0; i < 10; ++i)
4    {
5        AcceptEx();
6    }
7}

具体功能是通过AcceptEx函数实现的:

 1void network::IOCPServer::AcceptEx()
2{
3    // 从内存池中申请内存
4    m_ioInfo = (LPPER_IO_DATA)io_spl::malloc();
5    assert(io_spl::is_from(m_ioInfo));
6
7
8    ZeroMemory(m_ioInfo, sizeof(PER_IO_DATA));
9    m_ioInfo->wsaBuf.buf = m_ioInfo->buf;
10    m_ioInfo->wsaBuf.len = BUF_SIZE;
11    m_ioInfo->opType = OP_TYPE::OP_ACCEPT;
12
13    // 为将要接受的连接创建客户机套接字
14    m_ioInfo->clntSock = WSASocketW(PF_INET, SOCK_STREAM, 0NULL0, WSA_FLAG_OVERLAPPED);
15
16    DWORD dwBytes;
17    auto ret = m_lpfnAcceptEx(m_servSock->hSock,
18        m_ioInfo->clntSock,
19        m_ioInfo->wsaBuf.buf,
20        m_ioInfo->wsaBuf.len - (sizeof(SOCKADDR_IN) + 16) * 2// 若不希望AcceptEx建立连接后等待用户发送数据,则设为0
21        sizeof(SOCKADDR_IN) + 16,
22        sizeof(SOCKADDR_IN) + 16,
23        &dwBytes,
24        &m_ioInfo->overlapped
25    );
26
27    if (!ret)
28    {
29        if (WSAGetLastError() != WSA_IO_PENDING)
30        {
31            auto fmt = boost::format("%s%d") % "AcceptEx failed! Error code:" % WSAGetLastError();
32            log.e(fmt.str());
33        }
34    }
35}

这里我们从内存池中为”小纸条“申请内存,singleton_pool中提供了malloc函数可以从内存池中申请内存,使用is_from函数可以确认该块内存是否是从该内存池中申请的。

接着初始化了”小纸条“,并为客户端先准备好套接字,原理在扩展函数那篇也已经讲过了,所以这里也不再赘述。

主要来看看扩展函数AcceptEx的使用:

1auto ret = m_lpfnAcceptEx(m_servSock->hSock,  // 服务端套接字
2    m_ioInfo->clntSock,  // 客户机套接字
3    m_ioInfo->wsaBuf.buf,  // 用于接收数据的缓冲区
4    m_ioInfo->wsaBuf.len - (sizeof(SOCKADDR_IN) + 16) * 2// 若不希望AcceptEx建立连接后等待用户发送数据,则设为0
5    sizeof(SOCKADDR_IN) + 16,  // 本地套接字地址结构大小
6    sizeof(SOCKADDR_IN) + 16,  // 远程套接字地址结构大小
7    &dwBytes,  // 实际收到的数据大小
8    &m_ioInfo->overlapped  // 重叠结构
9);

注释已经很清楚的解释了各个参数的意义,扩展函数AcceptEx是通过WSAIoctl函数获取到函数指针m_lpfnAcceptEx里的,这个函数是异步的,所以不会阻塞。

我们传入为每个客户端准备的”小纸条“(m_ioInfo),系统去替我们执行耗时操作,处理完了它会在上面记下我们需要的内容,稍后我们可以在处理消息函数中通过GetQueuedCompletionStatus函数拿到。

0x7. 处理用户请求

程序的主要逻辑都集中在了RequestHandler函数中,也是我们前面开启的线程函数,异步处理的消息我们都能在这里获得到。实现如下:

 1void network::IOCPServer::RequestHandler()
2{
3    LPPER_HANDLE_DATA lpHandleInfo;
4    LPPER_IO_DATA lpIoInfo;
5    DWORD dwBytesTrans;
6    for (;;)
7    {
8        auto ret = GetQueuedCompletionStatus(m_hCompPort, &dwBytesTrans,
9            (LPDWORD)&lpHandleInfo, (LPOVERLAPPED*)&lpIoInfo, INFINITE);
10        if (ret == 0)
11        {
12            if (WSAGetLastError() == WAIT_TIMEOUT)
13                continue;
14            else
15            {
16                auto fmt = boost::format("%s%d") % "GetQueuedCompletionStatus failed! Error code:" % GetLastError();
17                log.e(fmt.str());
18                break;
19            }
20        }
21
22        std::lock_guard<std::mutex> lg(m_mtx);
23
24        if (dwBytesTrans == 0xFFFFFFFF)
25        {
26            break;
27        }
28
29        switch (lpIoInfo->opType)
30        {
31        case OP_TYPE::OP_ACCEPT:
32            AcceptHandler(lpIoInfo);
33            break;
34        case OP_TYPE::OP_READ:
35        {
36            if (dwBytesTrans == 0)  // EOP
37            {
38                CloseSock(lpHandleInfo, lpIoInfo);
39                continue;
40            }
41
42            // 将客户端消息转发回去
43            std::string msg(lpIoInfo->buf);
44            log.i("received:" + msg);
45
46            SendMsg(lpHandleInfo->hSock, lpIoInfo, msg);
47        }
48            break;
49        case OP_TYPE::OP_WRITE:
50            RecvMsg(lpHandleInfo->hSock, lpIoInfo);
51            break;
52        }
53    }
54}

关于IOCP的主要原理我们在实例一中已经写过了,所以这里不会再说重复的内容,主要来看有关扩展函数的部分。

我们通过OP_ACCEPT操作类型获取到连接用户的请求,这里的数据就是扩展函数AcceptEx投递过来的,对于连接进来的用户,我们专门用AcceptHandler函数来处理,其实现如下:

 1void network::IOCPServer::AcceptHandler(LPPER_IO_DATA lpIoInfo)
2{
3    // 使客户端套接字具有和服务器套接相同的属性
4    setsockopt(lpIoInfo->clntSock, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
5        (char*)&lpIoInfo->clntSock, sizeof(SOCKET));
6
7    SOCKADDR_IN *lpLocalSockAddr = nullptr, *lpRemoteSockAddr = nullptr;
8    int LocalSockAddr, RemoteSockAddr;
9
10    m_lpfnGetAcceptExSockaddrs(lpIoInfo->wsaBuf.buf,
11        lpIoInfo->wsaBuf.len - (sizeof(SOCKADDR_IN) + 16) * 2,
12        sizeof(SOCKADDR_IN) + 16,
13        sizeof(SOCKADDR_IN) + 16,
14        (SOCKADDR**)&lpLocalSockAddr, &LocalSockAddr,
15        (SOCKADDR**)&lpRemoteSockAddr, &RemoteSockAddr
16        );
17
18    auto fmt = boost::format("%s%d") % "connected client..." % lpIoInfo->clntSock;
19    log.i(fmt.str());
20
21    // 从内存池分配内存
22    m_clntSock = (LPPER_HANDLE_DATA)handle_spl::malloc();
23    assert(handle_spl::is_from(m_clntSock));
24
25    // 写入连接进来的客户端套接字和地址
26    m_clntSock->hSock = lpIoInfo->clntSock;
27    memcpy(&m_clntSock->sockAddr, lpRemoteSockAddr, sizeof(SOCKADDR_IN));
28
29    // 连接客户端套接字与CP对象
30    CreateIoCompletionPort((HANDLE)m_clntSock->hSock, m_hCompPort, (ULONG_PTR)m_clntSock, NULL);
31
32    // 保存用户信息
33    m_vAcceptedSock.push_back(m_clntSock);
34
35    fmt = boost::format("%s%d") % "waiting for data arrived..." % m_clntSock->hSock;
36    log.i(fmt.str());
37
38    if (lpIoInfo->overlapped.InternalHigh != 0)
39    {
40        std::string msg(lpIoInfo->buf);
41
42        // 输出普通日志
43        log.i("received:" + msg);
44
45        SendMsg(m_clntSock->hSock, lpIoInfo, msg);
46    }
47    else
48    {
49        RecvMsg(m_clntSock->hSock, lpIoInfo);
50    }
51
52    // 处理好一位客户后,投递下一个连接等待
53    AcceptEx();
54}

由于我们在扩展函数AcceptEx中设置了同时接受第一份数据,所以其实在连接时就已经获得了客户端发来的第一份数据,而这份数据就保存在"小纸条”中的缓冲区里,而大小保存在哪里呢?在重叠IO中我们就说边可以直接通过重叠结构的InternalHigh字段获取到的。若包含第一份数据,我们则输出该数据,然后再转发给客户端;若没有包含,我们就直接接收下一份数据。

处理好一位用户之后,再调用AcceptEx函数投递下一个连接等待,以此处理所有的用户请求。

0x8. 接收与发送消息

这部分和实例一完全没有区别,就只列出实现了:

 1void network::IOCPServer::RecvMsg(SOCKET sock, LPPER_IO_DATA lpIoInfo)
2{
3    ZeroMemory(lpIoInfo, sizeof(PER_IO_DATA));
4    lpIoInfo->wsaBuf.buf = lpIoInfo->buf;
5    lpIoInfo->wsaBuf.len = BUF_SIZE;
6    lpIoInfo->opType = OP_TYPE::OP_READ;
7
8    DWORD recvBytes, flags = 0;
9    WSARecv(sock, &lpIoInfo->wsaBuf, 1, &recvBytes, &flags, &lpIoInfo->overlapped, NULL);
10}
11
12void network::IOCPServer::SendMsg(SOCKET sock, LPPER_IO_DATA lpIoInfo, const std::string& msg)
13{
14    ZeroMemory(lpIoInfo, sizeof(PER_IO_DATA));
15    memcpy(lpIoInfo->buf, msg.c_str(), msg.size());
16    lpIoInfo->wsaBuf.buf = lpIoInfo->buf;
17    lpIoInfo->wsaBuf.len = msg.size();
18    lpIoInfo->opType = OP_TYPE::OP_WRITE;
19
20    WSASend(sock, &lpIoInfo->wsaBuf, 1NULL0, &lpIoInfo->overlapped, NULL);
21}

0x9. 清除释放

CloseSock函数也与实例一区别不大,只是这里释放内存是归还给内存池,而非直接delete:

 1void network::IOCPServer::CloseSock(LPPER_HANDLE_DATA lpHandleInfo, LPPER_IO_DATA lpIoInfo)
2{
3    // 输出普通日志
4    auto fmt = boost::format("%s%d") % "disconnected client...." % lpHandleInfo->hSock;
5    log.i(fmt.str());
6
7    auto pos = std::find_if(m_vAcceptedSock.begin(), m_vAcceptedSock.end(),
8        [&](LPPER_HANDLE_DATA& hd) {
9        return hd->hSock == lpHandleInfo->hSock;
10    });
11
12    if (pos != m_vAcceptedSock.end())
13    {
14        closesocket((*pos)->hSock);
15        m_vAcceptedSock.erase(pos);
16
17        // 归还从内存池申请的内存
18        if(handle_spl::is_from(lpHandleInfo))
19            handle_spl::free(lpHandleInfo);
20        if(io_spl::is_from(lpIoInfo))
21            io_spl::free(lpIoInfo);
22    }
23}

0xA. 总结

本篇算是一个综合性较强的应用,若能全部理解,那么对于IOCP模型你就理解的差不多了。Windows的网络模型在这里也就全部介绍完了,后面主要介绍网络库的使用,而要理解网络库,必须得有这些知识做基础,这些后面再说吧。大家可以发现这个例子也仅仅是个基本的网络服务器雏形罢了,要想写一个完整的项目,随便都得上千行了,那其中还包含许多sql,加密解密,界面等等技术,要是游戏的服务器那涉及的就更多了。这些东西后面有时间我都会总结一些文章的,等介绍了更多基础东西我们再来写一个真正完整的项目吧。

以上是关于网络模型之IOCP服务器实例二的主要内容,如果未能解决你的问题,请参考以下文章

winsock编程IOCP模型实现代码

Libevent源码分析--- IOCP

Libevent源码分析--- IOCP

[杂烩]Windows IOCP与Linux的epoll机制对比

IOCP模型

IOCP模型EPOLL模型的比较以及游戏服务器端的一些建议