目录
select/poll模型
select
原理:
select和poll模型是一种轮询实现
优点:
使用 select 的好处是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。添加到 fd_set 结构的套接字数量是有限制的,默认情况下,最大值是 FD_SETSIZE,它在 winsock2.h 文件中定义为 64。为了增加套接字数量,应用程序可以将 FD_SETSIZE 定义为更大的值(这个定义必须在包含 winsock2.h 之前出现)。不过,自定义的值也不能超过 Winsock 下层提供者的限制(通常是 1024)。
缺点:
如果FD_SETSIZE 值太大的话,服务器性能就会受到影响。例如有 1000 个套接字,那么在调用 select 之前就不得不设置这 1000 个套接字, select 返回之后,又必须检查这 1000 个套接字。
为了方便使用CInitSock类负责初始化
//
// initsock.h文件
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 链接到WS2_32.lib
class CInitSock
{
public:
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if(::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
~CInitSock()
{
::WSACleanup();
}
};
测试TCPClient.h:
//
// TCPClient.cpp文件
#include "../common/InitSock.h"
#include <stdio.h>
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套节字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(s == INVALID_SOCKET)
{
printf(" Failed socket() \n");
return 0;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
// 如果你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
printf(" Failed connect() \n");
return 0;
}
Sleep(1000);
char szText[] = "qweqwe";
//send中最后一个参数:0、MSG_OOB、MSG_PEEK、MSG_DONTROUTE
// 一般使用默认0
// MSG_OOB 优先数据 在linux中可以触发一个消息 无论发多长 优先数据只会是发的数据最后一个字节 OOB例子
// MSG_PEEK 标志会将套接字接收队列中的可读的数据拷贝到缓冲区,但不会使套接子接收队列中的数据减少,
// 常见的是:例如调用recv或read后,导致套接字接收队列中的数据被读取后而减少,而指定了MSG_PEEK标志,可通过返回值获得可读数据长度,并且不会减少套接字接收缓冲区中的数据,
// 所以可以供程序的其他部分继续读取。
// MSG_DONTROUTE 这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.这个标志一般用网络诊断和路由程序里面.
// 因为网络连接是现在主机找hosts再找arp缓存再在路由器上寻找 绕过这个流程直接发送
::send(s,szText,sizeof(szText),0 );
// 接收数据
//char buff[256];
//int nRecv = ::recv(s, buff, sizeof(buff)-1, 0);//sizeof(buff)-1否则buff[nRecv]会越界
//if(nRecv > 0)
//{
// buff[nRecv] = '\0';
// printf(" 接收到数据:%s", buff);
//}
// 关闭套节字
::closesocket(s);
return 0;
}
测试TCPServer:
//
// select.cpp文件
#include "../common/initsock.h"
#include <stdio.h>
CInitSock theSock; // 初始化Winsock库
int main()
{
USHORT nPort = 4567; // 此服务器监听的端口号
// 创建监听套节字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(nPort);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定套节字到本地机器
if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" Failed bind() \n");
return -1;
}
// 进入监听模式
::listen(sListen, 5);
// select模型处理过程
// 1)初始化一个套节字集合fdSocket,添加监听套节字句柄到这个集合
fd_set fdSocket; // 所有可用套节字集合
FD_ZERO(&fdSocket);
FD_SET(sListen, &fdSocket);
while(TRUE)
{
// 2)将fdSocket集合的一个拷贝fdRead传递给select函数,
// 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套节字句柄,然后返回。
//实现原理还是每隔一段时间对套接字数组检查 会导致效率问题
fd_set fdRead = fdSocket;
int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
if(nRet > 0)
{
// 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,
// 确定都有哪些套节字有未决I/O,并进一步处理这些I/O。
for(int i=0; i<(int)fdSocket.fd_count; i++)
{
if(FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
if(fdSocket.fd_array[i] == sListen) // (1)监听套节字接收到新连接
{
if(fdSocket.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(sNew, &fdSocket);
printf("接收到连接(%s)\n", ::inet_ntoa(addrRemote.sin_addr));
}
else
{
printf(" Too much connections! \n");
continue;
}
}
else
{
char szText[256];
//recv 第四个参数
// MSG_PEEK 标志会将套接字接收队列中的可读的数据拷贝到缓冲区,但不会使套接子接收队列中的数据减少,
// 常见的是:例如调用recv或read后,导致套接字接收队列中的数据被读取后而减少,而指定了MSG_PEEK标志,可通过返回值获得可读数据长度,并且不会减少套接字接收缓冲区中的数据,
// 所以可以供程序的其他部分继续读取。
// MSG_DONTWAIT 仅本操作非阻塞
// MSG_OOB 优先数据 在linux中可以触发一个消息 无论发多长 优先数据只会是发的数据最后一个字节
// MSG_WAITALL 一直等待直到数据接收完毕
int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
if(nRecv > 0) // (2)可读
{
szText[nRecv] = '\0';
printf("接收到数据:%s \n", szText);
}
else // (3)连接关闭、重启或者中断
{
::closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
}
}
else
{
printf(" Failed select() \n");
break;
}
}
return 0;
}
poll:
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <sys/poll.h>
main()
{
struct pollfd fds[1];
int r;
char buf[100];
//0: linux下运行程序在proc下默认会有0、1、2 3个文件
// 其中0代表 标准输入
// 1: 标准输出
// 2:错误输出
fds[0].fd=0;
fds[0].events=POLLIN;
while(1)
{
r=poll(fds,1,-1);
if(fds[0].revents & POLLIN)
{
printf("有数据输入!\n");
r=read(0,buf,99);
}
}
}
epoll/kqueue
libevent
boost::asio/ACE/muduo
以下为Windows特有
WSAAsyncSelect/WSAEventSelect
WSAAsyncSelect
原理:
WSAAsyncSelect 模型允许应用程序以 Windows 消息的形式接收网络事件通知。这个模型是为了适应 Windows 的消息驱动环境而设置的, 现在许多对性能要求不高的网络应用程序都采用 WSAAsyncSelect 模型, MFC(Microsoft Foundation Class, Microsoft 基础类库)中的CSocket 类也使用了它。
优点:
WSAAsyncSelect 模型最突出的特点是与 Windows 的消息驱动机制融在了一起,这使得开发带 GUI 界面的网络程序变得很简单。
缺点:
但是如果连接增加,单个 Windows 函数处理上千个客户请求时,服务器性能势必会受到影响。
///
// WSAAsyncSelect.cpp文件
#include "../common/initsock.h"
#include <stdio.h>
#define WM_SOCKET WM_USER + 101 // 自定义消息
CInitSock theSock;
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int main()
{
char szClassName[] = "MainWClass";
WNDCLASSEX wndclass;
// 用描述主窗口的参数填充WNDCLASSEX结构
wndclass.cbSize = sizeof(wndclass);
wndclass.style = CS_HREDRAW|CS_VREDRAW;
wndclass.lpfnWndProc = WindowProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = NULL;
wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szClassName ;
wndclass.hIconSm = NULL;
::RegisterClassEx(&wndclass);
// 创建主窗口
HWND hWnd = ::CreateWindowEx(
0,
szClassName,
"",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
NULL,
NULL);
if(hWnd == NULL)
{
::MessageBox(NULL, "创建窗口出错!", "error", MB_OK);
return -1;
}
USHORT nPort = 4567; // 此服务器监听的端口号
// 创建监听套节字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(nPort);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 绑定套节字到本地机器
if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" Failed bind() \n");
return -1;
}
// 将套接字设为窗口通知消息类型。
::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);
// 进入监听模式
::listen(sListen, 5);
// 从消息队列中取出消息
MSG msg;
while(::GetMessage(&msg, NULL, 0, 0))
{
// 转化键盘消息
::TranslateMessage(&msg);
// 将消息发送到相应的窗口函数
::DispatchMessage(&msg);
}
// 当GetMessage返回0时程序结束
return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_SOCKET:
{
// 取得有事件发生的套节字句柄
SOCKET s = wParam;
// 查看是否出错
if(WSAGETSELECTERROR(lParam))
{
::closesocket(s);
return 0;
}
// 处理发生的事件
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT: // 监听中的套接字检测到有连接进入
{
SOCKET client = ::accept(s, NULL, NULL);
::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
}
break;
case FD_WRITE:
{
}
break;
case FD_READ:
{
char szText[1024] = { 0 };
if(::recv(s, szText, 1024, 0) == -1)
::closesocket(s);
else
printf("接收数据:%s", szText);
}
break;
case FD_CLOSE:
{
::closesocket(s);
}
break;
}
}
return 0;
case WM_DESTROY:
::PostQuitMessage(0) ;
return 0 ;
}
// 将我们不处理的消息交给系统做默认处理
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
WSAEventSelect
原理:
Winsock 提供了另一种有用的异步事件通知 I/O 模型——WSAEventSelect 模型。这个模型与 WSAAsyncSelect 模型类似,允许应用程序在一个或者多个套接字上接收基于事件的网络通知。它与 WSAAsyncSelect 模型类似是因为它也接收 FD_XXX 类型的网络事件,不过并
不是依靠 Windows 的消息驱动机制,而是经由事件对象句柄通知。
优点:
WSAEventSelect 模型简单易用,也不需要窗口环境。
缺点:
该模型惟一的缺点是有最多等待 64个事件对象的限制,当套接字连接数量增加时,就必须创建多个线程来处理 I/O,也就是使
用所谓的线程池。
int WSAEventSelect(
SOCKET s, // 套接字句柄
WSAEVENT hEventObject, // 事件对象句柄
long lNetworkEvents // 感兴趣的 FD_XXX 网络事件的组合
);
WSAWaitForMultipleEvents 最 多 支 持 WSA_MAXIMUM_WAIT_EVENTS 个 对 象 ,WSA_MAXIMUM_WAIT_EVENTS 被定义为 64。因此,这个 I/O 模型在一个线程中同一时间最多能支持 64 个套接字,如果需要使用这个模型管理更多套接字,就需要创建额外的工作
线程了。
注意,将fWaitAll参数设为FALSE以后,如果同 时有几个事件对象受信,WSAWaitForMultipleEvents 函数的返回值也仅能指明一个,就是句柄数组中最前面的那个。如果指明的这个事件对象总有网络时间发生,那么后面其他事件对象所关联的网络事件就得不到处理了。解决办法是, WSAWaitForMultipleEvents 函数返回后,对每个事件都再次调用WSAWaitForMultipleEvents 函数,以便确定其状态。
DWORD WSAWaitForMultipleEvents(
DWORD cEvents, // 指定下面 lphEvents 所指的数组中事件对象句柄的个数
const WSAEVENT* lphEvents, // 指向一个事件对象句柄数组
BOOL fWaitAll, // 指定是否等待所有事件对象都变成受信状态
DWORD dwTimeout, // 指定要等待的时间, WSA_INFINITE 为无穷大
BOOL fAlertable // 在使用 WSAEventSelect 模型时可以忽略,应设为 FALSE
);
一旦事件对象受信,那么找到与之对应的套接字,然后调用 WSAEnumNetworkEvents 函数即可查看发生了什么网络事件。
nt WSAEnumNetworkEvents(
SOCKET s, // 套接字句柄
WSAEVENT hEventObject, // 对应的事件对象句柄。 如果提供了此参数, 本函数会重置这个事件对象的状态
LPWSANETWORKEVENTS lpNetworkEvents // 指向一个 WSANETWORKEVENTS 结构
);
出错代码
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents; // 指定已发生的网络事件(如 FD_ACCEPT、 FD_READ 等)
int iErrorCode[FD_MAX_EVENTS]; // 与 lNetworkEvents 相关的出错代码
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
iErrorCode 参数是一个数组,数组的每个成员对应着一个网络事件的出错代码。可以用
预定义标识 FD_READ_BIT、 FD_WRITE_BIT 等来索引 FD_READ、 FD_WRITE 等事件发生
时的出错代码。如下面代码片段所示
if(event.lNetworkEvents & FD_READ) // 处理 FD_READ 通知消息
{ if(event.iErrorCode[FD_READ_BIT] != 0)
{
…… // FD_READ 出错,错误代码为 event.iErrorCode[FD_READ_BIT]
}
}
例子:
//
// WSAEventSelect文件
#include "initsock.h"
#include <stdio.h>
#include <iostream>
#include <windows.h>
// 初始化Winsock库
CInitSock theSock;
int main()
{
// 事件句柄和套节字句柄表
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS];
int nEventTotal = 0;
USHORT nPort = 4567; // 此服务器监听的端口号
// 创建监听套节字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(nPort);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" Failed bind() \n");
return -1;
}
::listen(sListen, 5);
// 创建事件对象,并关联到新的套节字
WSAEVENT event = ::WSACreateEvent();
::WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE);
// 添加到表中
eventArray[nEventTotal] = event;
sockArray[nEventTotal] = sListen;
nEventTotal++;
// 处理网络事件
while(TRUE)
{
// 在所有事件对象上等待
int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
// 对每个事件调用WSAWaitForMultipleEvents函数,以便确定它的状态
nIndex = nIndex - WSA_WAIT_EVENT_0;
for(int i=nIndex; i<nEventTotal; i++)
{
nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);
if(nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
{
continue;
}
else
{
// 获取到来的通知消息,WSAEnumNetworkEvents函数会自动重置受信事件
WSANETWORKEVENTS event;
::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event);
if(event.lNetworkEvents & FD_ACCEPT) // 处理FD_ACCEPT通知消息
{
if(event.iErrorCode[FD_ACCEPT_BIT] == 0)
{
if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
{
printf(" Too many connections! \n");
continue;
}
SOCKET sNew = ::accept(sockArray[i], NULL, NULL);
WSAEVENT event = ::WSACreateEvent();
::WSAEventSelect(sNew, event, FD_READ|FD_CLOSE|FD_WRITE);
// 添加到表中
eventArray[nEventTotal] = event;
sockArray[nEventTotal] = sNew;
nEventTotal++;
}
}
else if(event.lNetworkEvents & FD_READ) // 处理FD_READ通知消息
{
if(event.iErrorCode[FD_READ_BIT] == 0)
{
char szText[256];
int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0);
if(nRecv > 0)
{
szText[nRecv] = '\0';
printf("接收到数据:%s \n", szText);
}
}
}
else if(event.lNetworkEvents & FD_CLOSE) // 处理FD_CLOSE通知消息
{
if(event.iErrorCode[FD_CLOSE_BIT] == 0)
{
::closesocket(sockArray[i]);
for(int j=i; j<nEventTotal-1; j++)
{
sockArray[j] = sockArray[j+1];
eventArray[j] = eventArray[j+1];
}
nEventTotal--;
}
}
else if(event.lNetworkEvents & FD_WRITE) // 处理FD_WRITE通知消息
{
}
}
}
}
return 0;
}
重叠(Overlapped)I/O模型
原理:
这个模型的基本设计思想是允许应用程序使用重叠数据结构一次投递一个或者多个异步 I/O 请求(即所谓的重叠I/O)。提交的 I/O 请求完成之后,与之关联的重叠数据结构中的事件对象受信,应用程序便可使用 WSAGetOverlappedResult 函数获取重叠操作结果。这和使用重叠结构调用 ReadFile和 WriteFile 函数操作文件类似。
优点:
与介绍过的其他模型相比,重叠 I/O 模型提供了更好的系统性能。
编程模型:
1.创建套接字
要使用重叠 I/O 模型,在创建套接字时必须使用 WSASocket 函数,设置重叠标志。
SOCKET WSASocket(int af, int type, int protocol, // 前 3 个参数与 socket 函数相同
LPWSAPROTOCOL_INFO lpProtocolInfo, // 指定下层服务提供者,可以是 NULL
GROUP g, // 保留
DWORD dwFlags // 指定套接字属性。 要使用重叠 I/O 模型, 必须指定 WSA_FLAG_OVERLAPPED
);
2.传输数据
在重叠 I/O 模型中,传输数据的函数是 WSASend、 WSARecv(TCP)和 WSASendTo、WSARecvFrom 等。下面是 WSASend 函数的定义,其他函数与之类似。
int WSASend(
SOCKET s, // 套接字句柄
LPWSABUF lpBuffers, // WSABUF 结构的数组
// 每个 WSABUF 结构包含一个缓冲区指针和对应缓冲区的长度
DWORD dwBufferCount, // 上面 WSABUF 数组的大小
LPDWORD lpNumberOfBytesSent, // 如果 I/O 操作立即完成的话,此参数取得实际传输数据的字节数
DWORD dwFlags, // 标志
LPWSAOVERLAPPED lpOverlapped, // 与此 I/O 操作关联的 WSAOVERLAPPED 结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 指定一个完成例程
);
这些函数与 Winsock1 中的 send、 recv 等函数相比,都多了如下两个参数。
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
I/O 操作函数都接收一个 WSAOVERLAPPED 结构类型的参数。这些函数被调用之后会立即返回,它们依靠应用程序传递的 WSAOVERLAPPED 结构管理 I/O 请求的完成。应用程序有两种方法可以接收到重叠 I/O 请求操作完成的通知:
(1)在与 WSAOVERLAPPED 结构关联的事件对象上等待, I/O 操作完成后,此事件对象受信,这是最经常使用的方法。
(2)使用 lpCompletionRoutine 指向的完成例程。完成例程是一个自定义的函数, I/O 操作完成后, Winsock 便去调用它。这种方法很少使用,将 lpCompletionRoutine 设为 NULL 即可。
3.接受连接
可以异步接受连接请求的函数是 AcceptEx。这是一个 Microsoft 扩展函数,它接受一个新的连接,返回本地和远程地址,取得客户程序发送的第一块数据。
BOOL AcceptEx(
SOCKET sListenSocket, // 监听套接字句柄
SOCKET sAcceptSocket, // 指定一个未被使用的套接字,在这个套接字上接受新的连接
PVOID lpOutputBuffer, // 指定一个缓冲区,用来取得在新连接上接收到的第一块数据,服务器
// 的本地地址和客户端地址
DWORD dwReceiveDataLength, // 上面 lpOutputBuffer 所指缓冲区的大小
DWORD dwLocalAddressLength, // 缓冲区中,为本地地址预留的长度。必须比最大地址长度多 16
DWORD dwRemoteAddressLength, // 缓冲区中,为远程地址预留的长度。必须比最大地址长度多 16
LPDWORD lpdwBytesReceived, // 用来取得接收到数据的长度
LPOVERLAPPED lpOverlapped // 指定用来处理本请求的 OVERLAPPED 结构,不能为 NULL
); // 声明在 Mswsock.h 中,需要添加到 Mswsock.lib 库的链接
模型没理解透需要重新学习 by 2018.8.24
///
// OverlappedServer.cpp文件
#include "../common/initsock.h"
#include <Mswsock.h>
#include <stdio.h>
#include <windows.h>
CInitSock theSock;
#define BUFFER_SIZE 1024
typedef struct _SOCKET_OBJ
{
SOCKET s; // 套节字句柄
int nOutstandingOps; // 记录此套节字上的重叠I/O数量
LPFN_ACCEPTEX lpfnAcceptEx; // 扩展函数AcceptEx的指针(仅对监听套节字而言)
} SOCKET_OBJ, *PSOCKET_OBJ;
typedef struct _BUFFER_OBJ
{
OVERLAPPED ol; // 重叠结构
char *buff; // send/recv/AcceptEx所使用的缓冲区
int nLen; // buff的长度
PSOCKET_OBJ pSocket; // 此I/O所属的套节字对象
int nOperation; // 提交的操作类型
#define OP_ACCEPT 1
#define OP_READ 2
#define OP_WRITE 3
SOCKET sAccept; // 用来保存AcceptEx接受的客户套节字(仅对监听套节字而言)
_BUFFER_OBJ *pNext;
} BUFFER_OBJ, *PBUFFER_OBJ;
HANDLE g_events[WSA_MAXIMUM_WAIT_EVENTS]; // I/O事件句柄数组
int g_nBufferCount; // 上数组中有效句柄数量
PBUFFER_OBJ g_pBufferHead, g_pBufferTail; // 记录缓冲区对象组成的表的地址
// 申请套节字对象和释放套节字对象的函数
PSOCKET_OBJ GetSocketObj(SOCKET s)
{
PSOCKET_OBJ pSocket = (PSOCKET_OBJ)::GlobalAlloc(GPTR, sizeof(SOCKET_OBJ));
if(pSocket != NULL)
{
pSocket->s = s;
}
return pSocket;
}
void FreeSocketObj(PSOCKET_OBJ pSocket)
{
if(pSocket->s != INVALID_SOCKET)
::closesocket(pSocket->s);
::GlobalFree(pSocket);
}
PBUFFER_OBJ GetBufferObj(PSOCKET_OBJ pSocket, ULONG nLen)
{
if(g_nBufferCount > WSA_MAXIMUM_WAIT_EVENTS - 1)
return NULL;
PBUFFER_OBJ pBuffer = (PBUFFER_OBJ)::GlobalAlloc(GPTR, sizeof(BUFFER_OBJ));
if(pBuffer != NULL)
{
pBuffer->buff = (char*)::GlobalAlloc(GPTR, nLen);
pBuffer->ol.hEvent = ::WSACreateEvent();
pBuffer->pSocket = pSocket;
pBuffer->sAccept = INVALID_SOCKET;
// 将新的BUFFER_OBJ添加到列表中
if(g_pBufferHead == NULL)
{
g_pBufferHead = g_pBufferTail = pBuffer;
}
else
{
g_pBufferTail->pNext = pBuffer;
g_pBufferTail = pBuffer;
}
g_events[++ g_nBufferCount] = pBuffer->ol.hEvent;
}
return pBuffer;
}
void FreeBufferObj(PBUFFER_OBJ pBuffer)
{
// 从列表中移除BUFFER_OBJ对象
PBUFFER_OBJ pTest = g_pBufferHead;
BOOL bFind = FALSE;
if(pTest == pBuffer)
{
g_pBufferHead = g_pBufferTail = NULL;
bFind = TRUE;
}
else
{
while(pTest != NULL && pTest->pNext != pBuffer)
pTest = pTest->pNext;
if(pTest != NULL)
{
pTest->pNext = pBuffer->pNext;
if(pTest->pNext == NULL)
g_pBufferTail = pTest;
bFind = TRUE;
}
}
// 释放它占用的内存空间
if(bFind)
{
g_nBufferCount --;
::CloseHandle(pBuffer->ol.hEvent);
::GlobalFree(pBuffer->buff);
::GlobalFree(pBuffer);
}
}
PBUFFER_OBJ FindBufferObj(HANDLE hEvent)
{
PBUFFER_OBJ pBuffer = g_pBufferHead;
while(pBuffer != NULL)
{
if(pBuffer->ol.hEvent == hEvent)
break;
pBuffer = pBuffer->pNext;
}
return pBuffer;
}
void RebuildArray()
{
PBUFFER_OBJ pBuffer = g_pBufferHead;
int i = 1;
while(pBuffer != NULL)
{
g_events[i++] = pBuffer->ol.hEvent;
pBuffer = pBuffer->pNext;
}
}
BOOL PostAccept(PBUFFER_OBJ pBuffer)
{
PSOCKET_OBJ pSocket = pBuffer->pSocket;
if(pSocket->lpfnAcceptEx != NULL)
{
// 设置I/O类型,增加套节字上的重叠I/O计数
pBuffer->nOperation = OP_ACCEPT;
pSocket->nOutstandingOps ++;
// 投递此重叠I/O
DWORD dwBytes;
pBuffer->sAccept =
::WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
BOOL b = pSocket->lpfnAcceptEx(pSocket->s,
pBuffer->sAccept,
pBuffer->buff,
BUFFER_SIZE - ((sizeof(sockaddr_in) + 16) * 2),
sizeof(sockaddr_in) + 16,
sizeof(sockaddr_in) + 16,
&dwBytes,
&pBuffer->ol);
if(!b)
{
if(::WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
}
return TRUE;
}
return FALSE;
};
BOOL PostRecv(PBUFFER_OBJ pBuffer)
{
// 设置I/O类型,增加套节字上的重叠I/O计数
pBuffer->nOperation = OP_READ;
pBuffer->pSocket->nOutstandingOps ++;
// 投递此重叠I/O
DWORD dwBytes;
DWORD dwFlags = 0;
WSABUF buf;
buf.buf = pBuffer->buff;
buf.len = pBuffer->nLen;
if(::WSARecv(pBuffer->pSocket->s, &buf, 1, &dwBytes, &dwFlags, &pBuffer->ol, NULL) != NO_ERROR)
{
if(::WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
}
return TRUE;
}
BOOL PostSend(PBUFFER_OBJ pBuffer)
{
// 设置I/O类型,增加套节字上的重叠I/O计数
pBuffer->nOperation = OP_WRITE;
pBuffer->pSocket->nOutstandingOps ++;
// 投递此重叠I/O
DWORD dwBytes;
DWORD dwFlags = 0;
WSABUF buf;
buf.buf = pBuffer->buff;
buf.len = pBuffer->nLen;
if(::WSASend(pBuffer->pSocket->s,
&buf, 1, &dwBytes, dwFlags, &pBuffer->ol, NULL) != NO_ERROR)
{
if(::WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
}
return TRUE;
}
BOOL HandleIO(PBUFFER_OBJ pBuffer)
{
PSOCKET_OBJ pSocket = pBuffer->pSocket; // 从BUFFER_OBJ对象中提取SOCKET_OBJ对象指针,为的是方便引用
pSocket->nOutstandingOps --;
// 获取重叠操作结果
DWORD dwTrans;
DWORD dwFlags;
BOOL bRet = ::WSAGetOverlappedResult(pSocket->s, &pBuffer->ol, &dwTrans, FALSE, &dwFlags);
if(!bRet)
{
// 在此套节字上有错误发生,因此,关闭套节字,移除此缓冲区对象。
// 如果没有其它抛出的I/O请求了,释放此缓冲区对象,否则,等待此套节字上的其它I/O也完成
if(pSocket->s != INVALID_SOCKET)
{
::closesocket(pSocket->s);
pSocket->s = INVALID_SOCKET;
}
if(pSocket->nOutstandingOps == 0)
FreeSocketObj(pSocket);
FreeBufferObj(pBuffer);
return FALSE;
}
// 没有错误发生,处理已完成的I/O
switch(pBuffer->nOperation)
{
case OP_ACCEPT: // 接收到一个新的连接,并接收到了对方发来的第一个封包
{
// 为新客户创建一个SOCKET_OBJ对象
PSOCKET_OBJ pClient = GetSocketObj(pBuffer->sAccept);
// 为发送数据创建一个BUFFER_OBJ对象,这个对象会在套节字出错或者关闭时释放
PBUFFER_OBJ pSend = GetBufferObj(pClient, BUFFER_SIZE);
if(pSend == NULL)
{
printf(" Too much connections! \n");
FreeSocketObj(pClient);
return FALSE;
}
RebuildArray();
// 将数据复制到发送缓冲区
pSend->nLen = dwTrans;
memcpy(pSend->buff, pBuffer->buff, dwTrans);
// 投递此发送I/O(将数据回显给客户)
if(!PostSend(pSend))
{
// 万一出错的话,释放上面刚申请的两个对象
FreeSocketObj(pSocket);
FreeBufferObj(pSend);
return FALSE;
}
// 继续投递接受I/O
PostAccept(pBuffer);
}
break;
case OP_READ: // 接收数据完成
{
if(dwTrans > 0)
{
// 创建一个缓冲区,以发送数据。这里就使用原来的缓冲区
PBUFFER_OBJ pSend = pBuffer;
pSend->nLen = dwTrans;
// 投递发送I/O(将数据回显给客户)
PostSend(pSend);
}
else // 套节字关闭
{
// 必须先关闭套节字,以便在此套节字上投递的其它I/O也返回
if(pSocket->s != INVALID_SOCKET)
{
::closesocket(pSocket->s);
pSocket->s = INVALID_SOCKET;
}
if(pSocket->nOutstandingOps == 0)
FreeSocketObj(pSocket);
FreeBufferObj(pBuffer);
return FALSE;
}
}
break;
case OP_WRITE: // 发送数据完成
{
if(dwTrans > 0)
{
// 继续使用这个缓冲区投递接收数据的请求
pBuffer->nLen = BUFFER_SIZE;
PostRecv(pBuffer);
}
else // 套节字关闭
{
// 同样,要先关闭套节字
if(pSocket->s != INVALID_SOCKET)
{
::closesocket(pSocket->s);
pSocket->s = INVALID_SOCKET;
}
if(pSocket->nOutstandingOps == 0)
FreeSocketObj(pSocket);
FreeBufferObj(pBuffer);
return FALSE;
}
}
break;
}
return TRUE;
}
void main()
{
// 创建监听套节字,绑定到本地端口,进入监听模式
int nPort = 4567;
SOCKET sListen =
::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = ::ntohs(nPort);
si.sin_addr.S_un.S_addr = INADDR_ANY;
::bind(sListen, (sockaddr*)&si, sizeof(si));
::listen(sListen, 200);
// 为监听套节字创建一个SOCKET_OBJ对象
PSOCKET_OBJ pListen = GetSocketObj(sListen);
// 加载扩展函数AcceptEx
GUID GuidAcceptEx = WSAID_ACCEPTEX;
DWORD dwBytes;
WSAIoctl(pListen->s,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&GuidAcceptEx,
sizeof(GuidAcceptEx),
&pListen->lpfnAcceptEx,
sizeof(pListen->lpfnAcceptEx),
&dwBytes,
NULL,
NULL);
// 创建用来重新建立g_events数组的事件对象
g_events[0] = ::WSACreateEvent();
// 在此可以投递多个接受I/O请求
for(int i=0; i<5; i++)
{
PostAccept(GetBufferObj(pListen, BUFFER_SIZE));
}
::WSASetEvent(g_events[0]);
while(TRUE)
{
int nIndex =
::WSAWaitForMultipleEvents(g_nBufferCount + 1, g_events, FALSE, WSA_INFINITE, FALSE);
if(nIndex == WSA_WAIT_FAILED)
{
printf("WSAWaitForMultipleEvents() failed \n");
break;
}
nIndex = nIndex - WSA_WAIT_EVENT_0;
for(int i=0; i<=nIndex; i++)
{
int nRet = ::WSAWaitForMultipleEvents(1, &g_events[i], TRUE, 0, FALSE);
if(nRet == WSA_WAIT_TIMEOUT)
continue;
else
{
::WSAResetEvent(g_events[i]);
// 重新建立g_events数组
if(i == 0)
{
RebuildArray();
continue;
}
// 处理这个I/O
PBUFFER_OBJ pBuffer = FindBufferObj(g_events[i]);
if(pBuffer != NULL)
{
if(!HandleIO(pBuffer))
RebuildArray();
}
}
}
}
}
IOCP
IOCP(I/O completion port, I/O 完成端口)是伸缩性最好的一种 I/O 模型。
完成端口 I/O 模型
原理:
I/O 完成端口最初的设计是应用程序发出一些异步 I/O 请求,当这些请求完成时,设备驱动将把这些工作项目排序到完成端口,这样,在完成端口上等待的线程池便可以处理这些完成 I/O。 完成端口实际上是一个 Windows I/O 结构, 它可以接收多种对象的句柄, 如文件对象、套接字对象等。
优点:
处理多个并发异步 I/O请求时,使用 I/O 完成端口比在 I/O 请求时创建线程更快更有效。当应用程序必须一次管理多个套接字时,完成端口模型提供了最好的系统性能。这个模型也提供了最好的伸缩性,它非常适合用来处理上百、上千个套接字。 IOCP 技术广泛应用于各种类型的高性能服务器,如 Apache 等。
参考资料:https://www.cnblogs.com/lancidie/archive/2011/12/19/2293773.html
参考资料: