IOCP模型C++入门级服务端搭建

效果展示

Windows平台打开DOS界面(cmd命令)输入:netstat -anot | findstr 端口号,即可查看端口是否被占用。


源码示例

TIPS:函数API的注解出自Microsoft官方文档。

UNetCore.h

#ifndef UNETCORE_H_
#define UNETCORE_H_//表示当前IO内核使用的是select模型
//#define U_SELECT_NETCORE//表示当前IO内核使用的是IOCP模型
#define U_IOCP_NETCORE#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <functional>namespace U
{//EventLoop抽象类,事件循环机制class IEventLoop{public:virtual bool Init() = 0;virtual void LoopOnce() = 0;virtual void UnInit() = 0;};//用于管理连接上来的客户端//TODOclass ITcpSocket{public:virtual void Init(IEventLoop* loop) = 0;};//响应客户端连接回调函数typedef std::function<void(ITcpSocket*)> FTcpServerCB;//ITcpServer抽象类,服务端class ITcpServer{public:/// <summary>/// 初始化TCP服务,与循环事件进行关联/// </summary>/// <param name="loop">事件循环机制</param>/// <param name="cb">当用户连接成功后的回调函数</param>/// <returns></returns>virtual bool Init(IEventLoop* loop, FTcpServerCB cb) = 0;/// <summary>/// 初始化socket/// </summary>/// <param name="ip">服务端IP</param>/// <param name="port">监听端口号</param>/// <returns>错误返回false</returns>virtual bool Listen(const char* ip, int port) = 0;};//加载Windows网络库void InitNetCore();//卸载Windows网络库void UnNetCore();//实例化事件循环机制IEventLoop* CreateEventLoop();//实例化服务端类ITcpServer* CreateTcpServer();
}
#endif // !UNETCORE_H_

IOCPEventLoop.h

#include "UNetCore.h"#ifdef U_IOCP_NETCORE
#include <WinSock2.h>
#include <iostream>
#include <unordered_map>
#pragma comment(lib,"ws2_32")
#pragma comment(lib, "Mswsock")
#pragma comment(lib, "shlwapi")
#pragma comment(lib, "psapi")#ifndef EVENTLOOP_H_
#define EVENTLOOP_H_namespace U
{//保存服务端单元的信息。即与服务端句柄关联,保存至事件循环机制中class sEvent{public://该服务端是属于什么类型的(TCP、UDP、PIPE)enum class Type{E_TCPSERVER,//UDP,PIPEE_TCPCLIENT,};Type type;SOCKET sock;//保存服务端句柄//由于sEvent类必须要附带服务端类,那么如果不使用联合体,会造成过度的内存资源浪费,即这里使用联合体保存各种类型服务端类的指针。union{class TcpServer* tcpServer;//class UcpServer* ucpServer;//...};};class EventLoop :public IEventLoop{private:std::unordered_map<SOCKET, sEvent*> _events;HANDLE _iocp;public:bool Init() override;void LoopOnce() override;void UnInit() override;//添加监听的循环事件,压入sEvent单元void AddEvent(sEvent* event);//将文件描述符与完成端口进行关联bool AssioIOCP(SOCKET sock, void* ptr);};
}#endif // !EVENTLOOP_H_
#endif // U_SELECT_NETCORE

IOCPEventLoop.cpp

CreateIoCompletionPort

作用:
  创建输入/输出(I/O)完成端口并将其与指定的文件句柄(文件描述符)相关联,或创建尚未与文件句柄(文件描述符)关联的I/O完成端口,以便稍后关联。

  将打开的文件句柄(文件描述符)的实例与I/O完成端口相关联,使进程能够接收涉及该文件句柄(文件描述符)的异步I/O操作完成通知。

HANDLE WINAPI CreateIoCompletionPort(_In_ HANDLE FileHandle, 文件句柄(文件描述符)_In_opt_ HANDLE ExistingCompletionPort, 现有的完成端口_In_ ULONG_PTR CompletionKey, 完成密钥_In_ DWORD NumberOfConcurrentThreads 并发线程数
);

参数:

  • FileHandle,打开的文件句柄或INVALID_HANDLE_VALUE。如果指定了INVALID_HANDLE_VALUE,该函数将创建I/O完成端口,而无需将其与
    文件句柄(文件描述符)相关联。在这种情况下,ExistingCompletionPort参数必须设置为NULL,并且忽略CompletionKey参数。
  • ExistingCompletionPort,现有I/O完成端口或NULL的句柄。如果此参数指定现有的I/O完成端口,则函数将其与FileHandle参数指定的句柄关联。如果成功,该函数将返回现有I/O完成端口的句柄(注意,这里不会创建新的I/O完成端口)。
    如果此参数为NULL,则该函数将创建新的I/O完成端口,如果成功,该函数会将句柄返回到新的I/O完成端口。
  • CompletionKey,指定文件句柄(文件描述符)的每个I/O完成数据包中包含的每个句柄用户定义完成密钥。
  • NumberOfConcurrentThreads,操作系统允许并发处理I/O完成端口的I/O完成数据包最大线程数。如果ExistingCompletionPort参数不为NULL,则忽略该参数。如果此参数为零,则系统允许与系统中存在处理器的并发运行线程数一样多。

返回值:

  如果函数成功,则返回值是I/O完成端口的句柄。

  • 如果ExistingCompletionPort参数为NULL,则返回值为新句柄。
  • 如果ExistingCompletionPort参数是有效的I/O完成端口句柄,则返回值是I/O完成端口句柄本身。
  • 如果FileHandle参数是有效的句柄,则该文件句柄现有与返回的I/O完成端口相关联。

  如果函数失败,则返回值为NULL。

GetQueuedCompletionStatus

  获取排队队列的完成状态。
作用:

  尝试从指定的I/O完成端口排出I/O完成数据包。如果没有完成的数据包排队,则该函数会等待完成端口相关的待处理的I/O操作。(即发生阻塞)

  要一次处理多个I/O完成数据包,可以使用GetQueuedCompletionStatusEx函数。

BOOL WINAPI GetQueuedCompletionStatus(_In_ HANDLE CompletionPort, I/O完成端口_Out_ LPDWORD lpNumberOfBytesTransferred, 传输字节的数量_Out_ PULONG_PTR lpCompletionKey, 完成密钥_Out_ LPOVERLAPPED* lpOverlapped, 一个重叠的结构体_In_ DWORD dwMilliseconds 时间
);

参数:

  • CompletionPort,完成I/O端口句柄,创建一个I/O完成端口需要使用CreateIoCompletionPort函数。

  • lpNumberOfBytesTransferred,一个指向变量的指针,该变量接收完成I/O端口操作中的传输字节数。在客户端、服务端连接成功后数据交互时使用(recv,send)。

  • lpCompletionKey,一个指向变量的指针,该变量接收与I/O操作已完成的文件句柄关联的完成密钥值。

  • lpOverlapped,一个指向变量的指针,该变量接收启动完成的I/O操作时指定的重叠结构的地址。

  • dwMilliseconds,等待完成数据包出现在完成端口的毫秒数(即在这段时间内该应用进程可以做其他事情,无需阻塞等待)。

    • 如果完成的数据包在指定的时间内未出现,则函数超时,返回FALSE。并设置*lpOverlapped = NULL。lpOverlapped为一个二级指针。
    • 如果dwMilliseconds为INFINITE(无限的),那么该函数永远不会超时。如果dwMilliseconds为0,并且没有I/O操作要脱离,则该函数将会立即超时。

      即如果我们未设置时间参数,在这个过程中如果没有I/O操作,那么会直接返回。
      而如果我们设置了时间参数,该时间到达之后不管是否有I/O操作,都会返回相应的结果(TRUE或FALSE),但是需要注意,必须要等待该时间才会有返回结果。这个时间段中是阻塞的。(在时间段内如果有I/O操作发生则直接返回)
       而如果我们设置时间参数为无限大,那么将会永远阻塞,直至有I/O操作发生则退出阻塞。

返回值:

  成功则返回TRUE,否则返回FALSE。

  获取扩展的错误信息可以调用GetLastError函数。

源码:

#include "IOCPEventLoop.h"
#include "IOCPTcpServer.h"#ifdef U_IOCP_NETCOREnamespace U
{void InitNetCore(){WSADATA wsa;WSAStartup(MAKEWORD(2, 2), &wsa);//TODO 可以添加异常导出库,在程序崩溃生成崩溃信息}void UnNetCore(){WSACleanup();}IEventLoop* CreateEventLoop(){return new EventLoop;}
}bool U::EventLoop::Init()
{//CreateIoCompletionPort函数的作用://创建输入/输出(I/O)完成端口并将其与指定的文件句柄(文件描述符)相关联,//或创建尚未与文件句柄(文件描述符)关联的I/O完成端口,以便稍后关联。//将打开的文件句柄(文件描述符)的实例与I/O完成端口相关联,使进程能够接收涉及该文件句柄(文件描述符)的异步I/O操作完成通知。/*HANDLE WINAPI CreateIoCompletionPort(_In_ HANDLE FileHandle, 文件句柄(文件描述符)_In_opt_ HANDLE ExistingCompletionPort, 现有的完成端口_In_ ULONG_PTR CompletionKey, 完成密钥_In_ DWORD NumberOfConcurrentThreads 并发线程数);参数:FileHandle,打开的文件句柄或INVALID_HANDLE_VALUE。如果指定了INVALID_HANDLE_VALUE,该函数将创建I/O完成端口,而无需将其与文件句柄(文件描述符)相关联。在这种情况下,ExistingCompletionPort参数必须设置为NULL,并且忽略CompletionKey参数。ExistingCompletionPort,现有I/O完成端口或NULL的句柄。如果此参数指定现有的I/O完成端口,则函数将其与FileHandle参数指定的句柄关联。如果成功,该函数将返回现有I/O完成端口的句柄(注意,这里不会创建新的I/O完成端口)。如果此参数为NULL,则该函数将创建新的I/O完成端口,如果成功,该函数会将句柄返回到新的I/O完成端口。CompletionKey,指定文件句柄(文件描述符)的每个I/O完成数据包中包含的每个句柄用户定义完成密钥。NumberOfConcurrentThreads,操作系统允许并发处理I/O完成端口的I/O完成数据包最大线程数。如果ExistingCompletionPort参数不为NULL,则忽略该参数。如果此参数为零,则系统允许与系统中存在处理器的并发运行线程数一样多。返回值:如果函数成功,则返回值是I/O完成端口的句柄。· 如果ExistingCompletionPort参数为NULL,则返回值为新句柄。· 如果ExistingCompletionPort参数是有效的I/O完成端口句柄,则返回值是I/O完成端口句柄本身。· 如果FileHandle参数是有效的句柄,则该文件句柄现有与返回的I/O完成端口相关联。如果函数失败,则返回值为NULL。*///第一步,创建尚未与文件句柄(文件描述符)关联的I/O完成端口,以便稍后关联//由于目前没有文件描述符,即创建I/O完成端口,同时开辟一个线程(主线程)_iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);if (INVALID_HANDLE_VALUE == _iocp){std::cout << "创建完成端口失败" << std::endl;return false;}std::cout << "创建完成端口成功" << std::endl;return true;
}//采用IOCP IO模型
void U::EventLoop::LoopOnce()
{//第五步,获取排列队列的完成状态/*GetQueuedCompletionStatus,获取排队队列的完成状态。作用:尝试从指定的I/O完成端口排出I/O完成数据包。如果没有完成的数据包排队,则该函数会等待完成端口相关的待处理的I/O操作。(即发生阻塞)要一次处理多个I/O完成数据包,可以使用GetQueuedCompletionStatusEx函数。BOOL WINAPI GetQueuedCompletionStatus(_In_ HANDLE CompletionPort, I/O完成端口_Out_ LPDWORD lpNumberOfBytesTransferred, 传输字节的数量_Out_ PULONG_PTR lpCompletionKey, 完成密钥_Out_ LPOVERLAPPED* lpOverlapped, 一个重叠的结构体_In_ DWORD dwMilliseconds 时间);参数:CompletionPort,完成I/O端口句柄,创建一个I/O完成端口需要使用CreateIoCompletionPort函数。lpNumberOfBytesTransferred,一个指向变量的指针,该变量接收完成I/O端口操作中的传输字节数。在客户端、服务端连接成功后数据交互时使用(recv,send)。lpCompletionKey,一个指向变量的指针,该变量接收与I/O操作已完成的文件句柄关联的完成密钥   值。lpOverlapped,一个指向变量的指针,该变量接收启动完成的I/O操作时指定的重叠结构的地址。dwMilliseconds,等待完成数据包出现在完成端口的毫秒数(即在这段时间内该应用进程可以做其他事情,无需阻塞等待)。如果完成的数据包在指定的时间内未出现,则函数超时,返回FALSE。并设置*lpOverlapped = NULL。lpOverlapped为一个二级指针。如果dwMilliseconds为INFINITE(无限的),那么该函数永远不会超时。如果dwMilliseconds为0,并且没有I/O操作要脱离,则该函数将会立即超时。即如果我们未设置时间参数,在这个过程中如果没有I/O操作,那么会直接返回。而如果我们设置了时间参数,该时间到达之后不管是否有I/O操作,都会返回相应的结果(TRUE或FALSE),但是需要注意,必须要等待该时间才会有返回结果。这个时间段中是阻塞的。(在时间段内如果有I/O操作发生则直接返回)而如果我们设置时间参数为无限大,那么将会永远阻塞,直至有I/O操作发生则退出阻塞。返回值:成功则返回TRUE,否则返回FALSE。获取扩展的错误信息可以调用GetLastError函数。*/DWORD NumberOfBytesTransferred;void* lpCompletionKey = NULL;OVERLAPPED* lpOverlapped;BOOL bRet = GetQueuedCompletionStatus(_iocp, &NumberOfBytesTransferred, (PULONG_PTR)&lpCompletionKey, &lpOverlapped, 0);if (!bRet && NULL == lpOverlapped){//std::cout << "错误消息:" << GetLastError() << std::endl;return;}sEvent* event = (sEvent*)lpCompletionKey;switch (event->type){case U::sEvent::Type::E_TCPSERVER:event->tcpServer->OnAccept();break;default:break;}
}void U::EventLoop::UnInit()
{}void U::EventLoop::AddEvent(sEvent* event)
{}bool U::EventLoop::AssioIOCP(SOCKET sock, void* ptr)
{//第二步,将创建的服务端句柄跟I/O完成端口进行关联,同时绑定I/O完成密钥(使用文件描述符对应的sEvent对象进行绑定),//由于I/O完成端口不为NULL,此时并发线程数忽略。//文件描述符为有效,且I/O完成端口句柄有效,则返回值为I/O完成端口句柄本身,否则则返回NULL。return _iocp == CreateIoCompletionPort((HANDLE)sock, _iocp, (ULONG_PTR)ptr, 0);
}#endif // U_SELECT_NETCORE

IOCPTcpServer.h

#include "UNetCore.h"#ifdef U_IOCP_NETCORE
#include "IOCPEventLoop.h"
#ifndef TCPSERVER_H_
#define TCPSERVER_H_namespace U
{class TcpServer :public ITcpServer{private:FTcpServerCB _cb;//客户端连接回调事件SOCKET _sock;//服务端socketsEvent _event;EventLoop* _loop;//客户端句柄(文件描述符)及输出缓冲区SOCKET _clientSock;char _buffer[1024];DWORD _recvLen;//重叠结构体,供IOCP内部机制使用OVERLAPPED _overLapped;public:TcpServer();~TcpServer();bool Init(IEventLoop* loop, FTcpServerCB cb) override;bool Listen(const char* ip, int port) override;public://处理客户端连接void OnAccept();private:bool PostAccept();};
}#endif // !TCPSERVER_H_
#endif // U_SELECT_NETCORE

IOCPTcpServer.cpp

GetAcceptExSockaddrs

作用:

  解析AcceptEx函数获取的数据,将输出缓冲区和接收字节大小传入至函数,最终将本地和远端地址传递给sockaddr结构中。

VOID PASCAL FAR GetAcceptExSockaddrs (_In_reads_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer,输出缓冲区_In_ DWORD dwReceiveDataLength,接收的额外数据长度_In_ DWORD dwLocalAddressLength,本地数据长度_In_ DWORD dwRemoteAddressLength,远端数据长度_Outptr_result_bytebuffer_(*LocalSockaddrLength) struct sockaddr **LocalSockaddr,本地Sockaddr_Out_ LPINT LocalSockaddrLength,本地Sockaddr长度_Outptr_result_bytebuffer_(*RemoteSockaddrLength) struct sockaddr **RemoteSockaddr,远端Sockaddr_Out_ LPINT RemoteSockaddrLength 远端Sockaddr长度
);

参数:

  • lpOutputBuffer,一个指向输出缓冲区的指针,该指针只接收由AcceptEx产生的连接发送的第一个数据块。必须是传递给AcceptEx函数的lpOutputBuffer参数。
  • dwReceiveDataLength,输出缓冲区中用于接收第一个数据的字节数。该值必须等于传递给AcceptEx函数的接收数据长度参数。
  • dwLocalAddressLength,为本地地址信息保留的字节数。该值必须等于传递给AcceptEx函数的dwLocalAddressLength参数。
  • dwRemoteAddressLength,为远端地址信息保留的字节数。该值必须等于传递给AcceptEx函数的dwRemoteAddressLength参数。
  • LocalSockaddr,接收连接本地地址的SockAddr结构的指针(与getsockname函数返回的相同信息)。必须指定此参数。
  • LocalSockaddrLength,本地地址的字节大小。必须指定此参数。
  • RemoteSockaddr,接收连接远端地址的SockAddr结构的指针(与getpeername函数返回的相同信息)。必须指定此参数。
  • RemoteSockaddrLength,远端地址的字节大小。必须指定此参数。

返回值

  无。

WSASocket

SOCKET WSAAPI WSASocketW(_In_ int af,_In_ int type,_In_ int protocol,_In_opt_ LPWSAPROTOCOL_INFOW lpProtocolInfo,_In_ GROUP g,_In_ DWORD dwFlags
);

作用:

  WSASocket函数:创建一个套接字(文件描述符,文件句柄)。

参数:

  • 第一个参数表示采用ipv4族。
  • 第二个参数表示采用TCP协议。
  • 第三个参数表示可能的协议类型为TCP协议。
  • 第四个参数如果不为空,则会让创建的套接字与LPWSAPROTOCOL_INFOW指针指向的结构体绑定。
  • 第五个参数为0表示没有执行组相关的操作。
  • 第六个参数表示WSA_FLAG_OVERLAPPED表示创建一个支持重叠I/O(I/O完成端口句柄)的socket。

返回值:

  文件描述符(文件句柄,socket)。

AcceptEx

作用:
  AcceptEx函数接受一个新的连接,返回本地和远端地址,并接收第一个客户端应用程序发送的第一个数据块。

BOOL PASCAL FAR AcceptEx (_In_ SOCKET sListenSocket, 服务端文件描述符_In_ SOCKET sAcceptSocket, 客户端文件描述符_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, 输出缓冲区_In_ DWORD dwReceiveDataLength, 额外接收数据长度,除客户端地址和服务端地址之外。_In_ DWORD dwLocalAddressLength, 本地地址长度(服务端地址长度)_In_ DWORD dwRemoteAddressLength, 远端地址长度(客户端地址长度)_Out_ LPDWORD lpdwBytesReceived, 收到客户端的字节大小_Inout_ LPOVERLAPPED lpOverlapped 一个重叠的结构体
);

参数:

  • sListenSocket,服务端socket(文件描述符,文件句柄)
  • sAcceptSocket,客户端socket(文件描述符,文件句柄)
  • lpOutputBuffer,一个指向缓冲区的指针,该指针接收到新连接 连接上来后发送的第一个数据块,服务器的本地地址和客户端的远端地址。接收数据以偏移零开始写入缓冲区的第一部分,而地址则写入缓冲区的后半部分。注意,该参数必须被指定(必须填写)。
  • dwReceiveDataLength,lpOutputBuffer缓冲区中的字节数将在缓冲区开始时用于实际接收数据。这个大小不应该包括服务器本地地址,也不应该包括客户端的远程地址。它们附加到输出缓冲区。如果dwReceiveDataLength长度为零,则接受一个连接不会导致一个接收数据的操作。取而代之的是,AcceptEx将会立即完成,而无需等待任何数据(即仅仅接收客户端地址和服务端地址)。
  • dwLocalAddressLength,为本地地址信息保留的字节数。该值必须至少比使用的传输协议的最大地址长度高16个字节。这16个字节是I/O完成端口用于存放内存隐藏结构体进行处理交互使用的。
  • dwRemoteAddressLength,为远端地址信息保留的字节数。该值必须至少比使用的传输协议的最大地址长度高16个字节(原因如上)。注意,该值不能为零。
  • lpdwBytesReceived,DWORD类型的指针,用于接收客户端传入的字节数。仅当操作同步完成时才设置此参数。如果它返回ERROR_IO_PENDING并且稍后完成(即该socket文件描述符不支持重叠I/O(I/O完成端口,创建时需指定WSA_FLAG_OVERLAPPED)),则DWORD对象永远不会被设置并且从完成通知机制中获取读取的字节数(即无法通过AcceptEx拿到字节数)。
  • lpOverlapped,一个用来处理请求的重叠结构。该参数必须被指定,它不能为NULL。

返回值:

  如果没有发生错误,接收函数成功完成,并返回TRUE。否则返回FALSE。可以调用WSAGetLastError函数来返回扩展错误信息。

源码:

#include "IOCPTcpServer.h"
#ifdef U_IOCP_NETCORE
//IOCP框架头文件
#include <ws2tcpip.h>
#include <mswsock.h>namespace U
{ITcpServer* CreateTcpServer(){return new TcpServer;}
}U::TcpServer::TcpServer()
{std::cout << "初始化 TCPServer" << std::endl;_cb = nullptr;_sock = INVALID_SOCKET;_event.tcpServer = this;_event.type = sEvent::Type::E_TCPSERVER;_event.sock = INVALID_SOCKET;
}U::TcpServer::~TcpServer()
{_cb = nullptr;if (INVALID_SOCKET != _sock)closesocket(_sock);_sock = INVALID_SOCKET;
}bool U::TcpServer::Init(IEventLoop* loop, FTcpServerCB cb)
{_cb = cb;_loop = dynamic_cast<EventLoop*>(loop);return true;
}bool U::TcpServer::Listen(const char* ip, int port)
{std::cout << "Listen IP:" << ip << " port:" << port << std::endl;//TODO 待完善if (_sock != INVALID_SOCKET){closesocket(_sock);return false;}_sock = socket(AF_INET, SOCK_STREAM, 0);SOCKADDR_IN addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);if (SOCKET_ERROR == bind(_sock, (sockaddr*)&addr, sizeof(addr)))return false;if (SOCKET_ERROR == listen(_sock, 5))return false;_event.sock = _sock;if (_loop->AssioIOCP(_sock, (void*)&_event))std::cout << "服务端句柄关联IOCP成功" << __FUNCTION__ << std::endl;elsestd::cout << "服务端句柄关联IOCP失败" << __FUNCTION__ << std::endl;//投递IOCP accept,预处理PostAccept();return true;
}void U::TcpServer::OnAccept()
{//处理客户端消息//第六步,GetAcceptExSockaddrs解析AcceptEx函数获取的数据,将输出缓冲区和接收字节大小传入至函数,最终将本地和远端地址传递给sockaddr结构中/*GetAcceptExSockaddrs函数作用:解析AcceptEx函数获取的数据,将输出缓冲区和接收字节大小传入至函数,最终将本地和远端地址传递给sockaddr结构中。VOID PASCAL FAR GetAcceptExSockaddrs (_In_reads_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer,输出缓冲区_In_ DWORD dwReceiveDataLength,接收的额外数据长度_In_ DWORD dwLocalAddressLength,本地数据长度_In_ DWORD dwRemoteAddressLength,远端数据长度_Outptr_result_bytebuffer_(*LocalSockaddrLength) struct sockaddr **LocalSockaddr,本地Sockaddr_Out_ LPINT LocalSockaddrLength,本地Sockaddr长度_Outptr_result_bytebuffer_(*RemoteSockaddrLength) struct sockaddr **RemoteSockaddr,远端Sockaddr_Out_ LPINT RemoteSockaddrLength 远端Sockaddr长度);参数:lpOutputBuffer,一个指向输出缓冲区的指针,该指针只接收由AcceptEx产生的连接发送的第一个数据块。必须是传递给AcceptEx函数的lpOutputBuffer参数。dwReceiveDataLength,输出缓冲区中用于接收第一个数据的字节数。该值必须等于传递给AcceptEx函数的接收数据长度参数。dwLocalAddressLength,为本地地址信息保留的字节数。该值必须等于传递给AcceptEx函数的dwLocalAddressLength参数。dwRemoteAddressLength,为远端地址信息保留的字节数。该值必须等于传递给AcceptEx函数的dwRemoteAddressLength参数。LocalSockaddr,接收连接本地地址的SockAddr结构的指针(与getsockname函数返回的相同信息)。必须指定此参数。LocalSockaddrLength,本地地址的字节大小。必须指定此参数。RemoteSockaddr,接收连接远端地址的SockAddr结构的指针(与getpeername函数返回的相同信息)。必须指定此参数。RemoteSockaddrLength,远端地址的字节大小。必须指定此参数。返回值:无*/sockaddr* serverAddr = NULL;sockaddr* clientAddr = NULL;int serverAddrLen;int clientAddrLen;GetAcceptExSockaddrs(_buffer, _recvLen, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16,&serverAddr, &serverAddrLen, &clientAddr, &clientAddrLen);//连接成功后调用回调函数_cb(nullptr);//投递IOCP AcceptPostAccept();
}bool U::TcpServer::PostAccept()
{//第三步,创建支持重叠I/O的客户端socket//WSASocket函数:创建一个套接字(文件描述符,文件句柄)//第一个参数表示采用ipv4族,第二个参数表示采用TCP协议,第三个参数表示可能的协议类型为TCP协议//第四个参数如果不为空,则会让创建的套接字与LPWSAPROTOCOL_INFOW指针指向的结构体绑定//第五个参数为0表示没有执行组相关的操作//第六个参数表示WSA_FLAG_OVERLAPPED表示创建一个支持重叠I/O(I/O完成端口句柄)的socket_clientSock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);//等价于调用socket函数,内置默认支持重叠I/O(WSA_FLAG_OVERLAPPED)if (_clientSock == INVALID_SOCKET){std::cout << "创建客户端socket失败" << std::endl;return false;}//第四步,处理第一次客户端连接的数据块_recvLen = 0;//每次都需要重置一下当前用于接收客户端数据大小的长度/*AcceptEx函数的作用:AcceptEx函数接受一个新的连接,返回本地和远端地址,并接收第一个客户端应用程序发送的第一个数据块。BOOL PASCAL FAR AcceptEx (_In_ SOCKET sListenSocket, 服务端文件描述符_In_ SOCKET sAcceptSocket, 客户端文件描述符_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, 输出缓冲区_In_ DWORD dwReceiveDataLength, 额外接收数据长度,除客户端地址和服务端地址之外。_In_ DWORD dwLocalAddressLength, 本地地址长度(服务端地址长度)_In_ DWORD dwRemoteAddressLength, 远端地址长度(客户端地址长度)_Out_ LPDWORD lpdwBytesReceived, 收到客户端的字节大小_Inout_ LPOVERLAPPED lpOverlapped 一个重叠的结构体);参数:sListenSocket,服务端socket(文件描述符,文件句柄)sAcceptSocket,客户端socket(文件描述符,文件句柄)lpOutputBuffer,一个指向缓冲区的指针,该指针接收到新连接 连接上来后发送的第一个数据块,服务器的本地地址和客户端的远端地址。接收数据以偏移零开始写入缓冲区的第一部分,而地址则写入缓冲区的后半部分。注意,该参数必须被指定(必须填写)。dwReceiveDataLength,lpOutputBuffer缓冲区中的字节数将在缓冲区开始时用于实际接收数据。这个大小不应该包括服务器本地地址,也不应该包括客户端的远程地址。它们附加到输出缓冲区。如果dwReceiveDataLength长度为零,则接受一个连接不会导致一个接收数据的操作。取而代之的是,AcceptEx将会立即完成,而无需等待任何数据(即仅仅接收客户端地址和服务端地址)。dwLocalAddressLength,为本地地址信息保留的字节数。该值必须至少比使用的传输协议的最大地址长度高16个字节。这16个字节是I/O完成端口用于存放内存隐藏结构体进行处理交互使用的。dwRemoteAddressLength,为远端地址信息保留的字节数。该值必须至少比使用的传输协议的最大地址长度高16个字节(原因如上)。注意,该值不能为零。lpdwBytesReceived,DWORD类型的指针,用于接收客户端传入的字节数。仅当操作同步完成时才设置此参数。如果它返回ERROR_IO_PENDING并且稍后完成(即该socket文件描述符不支持重叠I/O(I/O完成端口,创建时需指定WSA_FLAG_OVERLAPPED)),则DWORD对象永远不会被设置并且从完成通知机制中获取读取的字节数(即无法通过AcceptEx拿到字节数)。lpOverlapped,一个用来处理请求的重叠结构。该参数必须被指定,它不能为NULL。返回值:如果没有发生错误,接收函数成功完成,并返回TRUE。否则返回FALSE。可以调用WSAGetLastError函数来返回扩展错误信息。*///在处理客户端连接的过程中虽然我们不处理第一次连接过程中额外的数据,第四个参数为0,但是客户端第一次连接服务端的时候//还是可能会携带一些数据发送至服务端,那么这时我们使用_recvBuf进行存储的数据不仅仅是服务端和客户端的地址信息,在其头部//可能存放一些额外的数据,这时我们使用_recvLen进行接收。_recvBuf存储数据的顺序是先存放额外的数据,然后在存放客户端和//服务端地址信息,在使用GetAcceptExSockaddrs进行解析客户端和服务端地址信息时,需要将头部额外的数据信息进行偏移掉,//从而正确获取客户端和服务端的地址偏移信息。if (FALSE == AcceptEx(_sock, _clientSock, _buffer, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &_recvLen, &_overLapped)){if (WSAGetLastError() != ERROR_IO_PENDING){std::cout << "AcceptEx ERROR" << std::endl;return false;}}std::cout << "AcceptEx OK" << std::endl;return true;}
#endif // U_SELECT_NETCORE

UServer.cpp

#include "UNetCore.h"
#include <iostream>int main()
{//加载Windows网络库U::InitNetCore();//调用EventLoop循环机制U::IEventLoop* loop = U::CreateEventLoop();loop->Init();//初始化I/O完成端口//初始化服务端U::ITcpServer* server = U::CreateTcpServer();//将服务端与事件循环机制loop关联server->Init(loop, [](U::ITcpSocket* sock) {std::cout << "客户端1连接" << std::endl;});server->Listen("0.0.0.0", 7890);//启动循环机制监听消息while (true)loop->LoopOnce();//卸载Windows网络库U::UnNetCore();return 0;
}

服务端事件处理顺序

IOCP模型C++入门级服务端搭建相关推荐

  1. windows 下frp服务启动_内网穿透frp linux服务端搭建和windows客户端使用

    一.Linux 服务端搭建 1.下载安装 wget --no-check-certificate https://raw.githubusercontent.com/clangcn/onekey-in ...

  2. GIT Windows服务端搭建笔记

    GIT Windows服务端搭建笔记 所需软件: GIT服务端: Bonobo Git Server,下载最新版 https://bonobogitserver.com/ 一:配置服务端(基于Wind ...

  3. apereo cas开发_Apereo CAS Server服务端搭建教程

    不说废话了,直接看搭建过程吧. 这个是下载后解压的目录,可以直接通过CMD执行mvnw.bat clean package 来构建,但是他会去找系统配置的M2_HOME,如果找不到会报错,这个时候可以 ...

  4. zabbix服务端搭建

    zabbix 是一个基于 WEB 界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案. zabbix 能监视各种网络参数,保证服务器系统的安全运营:并提供灵活的通知机制以让系统管理员快速定 ...

  5. Centos7下SVN服务端搭建以及hook应用

    介绍 SVN是subversion的缩写,是一个开放源代码的版本控制系统,特点是集中式管理,即一个远程主干分支,多个本地分支.同一时刻只能有一个用户commit,适用于中小型项目,方便快捷. 一.SV ...

  6. 天书奇谈3D服务端搭建架设教程Centos

    天书奇谈3D服务端搭建架设教程Centos 大家好,我是艾西,今天给大家分享一款回合制MMORPG手游的搭建教程.也算是G 内回合制手游的第一梯队吧,回合制手游总会有那么一帮热爱的玩家我们话不多说直接 ...

  7. 奇迹mu服务端搭建外网联机教程方案

    奇迹mu服务端搭建外网联机教程方案 我是艾西今天跟大家分享下奇迹服务端架设方法 1,安装MS-SQL2000(微软的数据库服务器) 2,还原奇迹服务端中的数据库文件 3,设置服务端中IP及其数据源 4 ...

  8. PVE 天龙八部TLBB服务端搭建(二)--服务端配置运行

    继上一篇<PVE 天龙八部TLBB服务端搭建(一)--linux环境搭建>环境搭建好之后,开始服务端的运行. 服务端运行环境分为linux和windows,我这里从某宝花1块2买了一个一键 ...

  9. cas 5.3.x 服务端搭建(一)

    前期准备: 服务端官⽅下载:https://github.com/apereo/cas-overlay-template 客户端官⽅下载:https://githu b.com/cas-project ...

最新文章

  1. Java Timer定时器 使用
  2. linux 实验 广技师 进程管理与系统监视,Linux系统管理之进程管理
  3. mongo 3t 处理时间
  4. Touch UI:高质量的移动端UI框架介绍
  5. Python机器视觉编程常用数据结构与示例
  6. Dojo-API介绍
  7. 深层神经网络难以训练的原因
  8. jquery 一些特效使用
  9. Kerberos 基本命令 - 持续更新
  10. apache启服务命令_linux系统下apache服务的启动、停止、重启命令
  11. 老男孩python第一天笔记
  12. Oracle database 11g release2发布
  13. 数学建模之层次分析法(AHP)
  14. android手机无法root成功,为什么有的安卓手机不能Root?
  15. java设置隐式事务_隐式事务 - -Timothy- - 博客园
  16. office2007 ppt制作与应用母板
  17. DVD转VCD,MPG文件参考
  18. mysql-query()expects_mysql_query() expects parameter 2 to be resource, string given in [duplicate]
  19. gamemaker 更新 runtime 快一点
  20. PostgreSQL使用PgAdmin导入数据

热门文章

  1. C语言—题目—猜数字游戏
  2. 代金券制作小程序秒代金券_小程序开发制作的秒杀活动担心亏本?引流到店怎么进行?...
  3. AWVS12 安装详解 -- For Windows 10
  4. 聊聊SWIM Protocol
  5. 效率源linux,效率源FAS7900现场勘验取证系统,多项产品功能惊喜来袭!
  6. 多维度统计报表的多维度查询 (高阶聚合函数)
  7. 微信jssdk多图上传
  8. wcg2012世界结果
  9. 【力扣-206】反转链表
  10. Java中float的表示范围