网络模型之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, 0xFFFFFFFF, NULL, NULL);
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, 0, NULL, 0, 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, 0, 0, 0);
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, NULL, NULL
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, NULL, NULL
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, 0, NULL, 0, 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, 1, NULL, 0, &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服务器实例二的主要内容,如果未能解决你的问题,请参考以下文章