windows提供了六种网络通信模型:阻塞模型、选择模型、异步选择模型、事件选择模型、重叠I/O模型、IOCP模型。
异步通信
同步通信
选择模型
选择模型:通过一个fd_set集合管理套接字,在满足套接字需求后,通知套接字。让套接字进行工作。
选择模型的核心是FD_SET集合和select函数。通过该函数,我们可以们判断套接字上是否存在数据,或者能否向一个套接字写入数据。
用途:如果我们想接受多个SOCKET的数据,该怎么处理呢?
由于当前socket是阻塞的,直接处理是一定完成不了要求的
a.我们会想到多线程,的确可以解决线程的阻塞问题,但开辟大量的线程并不是什么好的选择;
b我们可以想到用ioctlsocket()函数把socket设置成非阻塞的,然后用循环逐个socket查看当前套接字是否有数据,轮询进行。
这种是可以解决问题的,但是会导致频繁切换状态到内核去查看是否有数据到达,浪费时间。
c.于是想办法用只切换一次状态就知道所有socket的接受缓冲区是否有数据,于是有了select模型,select是阻塞的,Select的好处是可以同时处理若干个Socket,
select函数
int select(
int nfds,//忽略,只是为了保持与早期的Berkeley套接字应用程序的兼容
fd_set FAR* readfds,//可读性检查(有数据可读入,连接关闭,重设,终止),为空则不检查可读性
fd_set FAR* writefds,//可写性检查(有数据可发出),为空则不检查可写性
fd_set FAR* exceptfds,//带外数据检查(带外数据),为空则不检查
const struct timeval FAR* timeout//超时
);
三个 fd_set参数:一个用于检查可读性(readfds),一个用于检查可写性(writefds),另一个用于例外数据( excepfds)。
从根本上说,fdset数据类型代表着一系列特定套接字的集合。其中,
readfds集合包括符合下述任何一个条件的套接字:
■ 有数据可以读入。
■ 连接已经关闭、重设或中止。
■ 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
writefds集合包括符合下述任何一个条件的套接字:
■ 有数据可以发出。
■ 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
最后,exceptfds集合包括符合下述任何一个条件的套接字:
■ 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
■ 有带外(out-of-band,OOB)数据可供读取。
最后一个参数timeout:
对应的是一个指针,它指向一个timeval结构,用于决定select最多等待 I / O操作完成多久的时间。
如 timeout是一个空指针,那么select调用会无限期地“锁定”或停顿下去,直到至少有一个描述符符合指定的条件后结束。
对timeval结构的定义如下:
struct timeval {
long tv_sec;
long tv_usec;
} ;
若将超时值设置为(0,0),表明select会立即返回,允许应用程序对 select操作进行“轮询”。出于对性能方面的考虑,应避免这样的设置。
select成功完成后,会在 fd_set结构中,返回刚好有未完成的I/O操作的所有套接字句柄的总量。
若超过timeval设定的时间,便会返回0。
fd_set结构是一个结构体
typedef struct fd_set
{
u_int fd_count;
socket fd_array[FD_SETSIZE];
}fd_set;
fd_cout表示该集合套接字数量。最大为64.
fd_array套接字数组。
为了方便使用,windows sockets提供了下列宏,用来对fd_set进行一系列操作。使用以下宏可以使编程工作简化。
FD_CLR(s,*set);从set集合中删除s套接字。
FD_ISSET(s,*set);检查s是否为set集合的成员。
FD_SET(s,*set);将套接字加入到set集合中。
FD_ZERO(*set);将set集合初始化为空集合。
在开发Windows sockets应用程序时,通过下面的步骤,可以完成对套接字的可读写判断:
1:使用FD_ZERO初始化套接字集合。如FD_ZERO(&readfds);
2:使用FD_SET将某套接字放到readfds内。如: FD_SET(s,&readfds);
3:以readfds为第二个参数调用select函数。select在返回时会返回所有fd_set集合中套接字的总个数,并对每个集合进行相应的更新。将满足条件的套接字放在相应的集合中。
4:使用FD_ISSET判断s是否还在某个集合中。如: FD_ISSET(s,&readfds);
5:调用相应的Windows socket api 对某套接字进行操作。
select优缺点:
优点:可实现单线程处理多个任务
缺点:
a.等待数据到达的过程以及将数据从内核拷贝到用户的过程总也存在一定阻塞
b.管理的set数组有一定上限,最多是64个(可通过重置fd_setsize将上限扩大到1024)
c.select低效是因为每次它都需要轮询。
测试代码
服务端
// SelectTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define PORT 8899
#define MSGSIZE 255
#define SRV_IP "127.0.0.1"
int g_nSockConn = 0;//请求连接的数目
struct ClientInfo
{
SOCKET sockClient;
SOCKADDR_IN addrClient;
};
//客户端socket列表
//FD_SETSIZE是在winsocket2.h头文件里定义的,这里windows默认最大为64
//在包含winsocket2.h头文件前使用宏定义可以修改这个值
ClientInfo g_Client[FD_SETSIZE];
DWORD WINAPI WorkThread(LPVOID lpParameter);
int main(int argc, char* argv[])
{
//初始化socket
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建socket
SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//定义服务端socket地址
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr(SRV_IP);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(PORT);
//绑定端口
bind(sockListen, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
//监听端口
listen(sockListen, 64);
DWORD dwThreadIDRecv = 0;
DWORD dwThreadIDWrite = 0;
//创建工作线程
HANDLE hand = CreateThread(NULL, 0, WorkThread, NULL, 0, &dwThreadIDRecv);
if (hand == NULL)
{
cout << "Create work thread failed\n";
return 1;
}
SOCKET sockClient;
SOCKADDR_IN addrClient;
while (true)
{
//接受客户端连接
int nLenAddrClient = sizeof(SOCKADDR);
sockClient = accept(sockListen, (SOCKADDR*)&addrClient, &nLenAddrClient);//第三个参数一定要按照addrClient大小初始化
if (sockClient != INVALID_SOCKET)
{
cout << inet_ntoa(addrClient.sin_addr) << ":" << ntohs(addrClient.sin_port) << " has connect !" << endl;
g_Client[g_nSockConn].addrClient = addrClient;//保存连接端地址信息
g_Client[g_nSockConn].sockClient = sockClient;//加入连接者队列
g_nSockConn++;
}
}
closesocket(sockListen);
WSACleanup();
return 0;
}
DWORD WINAPI WorkThread(LPVOID lpParameter)
{
int nRet = 0;
TIMEVAL tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
char buf[MSGSIZE] = "";
while (true)
{
//socket监视集合
FD_SET fdRead;
FD_ZERO(&fdRead);
for (int i = 0; i < g_nSockConn; i++)
{
FD_SET(g_Client[i].sockClient, &fdRead); //将所有socket加入到监视的socket集合中
}
//只处理read事件 新的客户端连接也是read
nRet = select(0, &fdRead, NULL, NULL, &tv);
if (nRet == 0)
{ //没有连接或者没有读事件
continue;
}
for (int i = 0; i < g_nSockConn; i++)
{
if (FD_ISSET(g_Client[i].sockClient, &fdRead)) //socket 是否在可读集合中
{
//如果在集合中,向下进行相应的IO操作
nRet = recv(g_Client[i].sockClient, buf, sizeof(buf), 0);//看是否能正常接收到数据
if (nRet == 0 || (nRet == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
cout << "Client " << inet_ntoa(g_Client[i].addrClient.sin_addr) << "closed" << endl;
closesocket(g_Client[i].sockClient);
if (i < g_nSockConn - 1)
{
//用最后一个socket去替换失效的socket
g_Client[i--].sockClient = g_Client[--g_nSockConn].sockClient;
}
else
{
g_nSockConn--;
}
}
else
{
cout << inet_ntoa(g_Client[i].addrClient.sin_addr) << " recv:" << buf << endl;
strcpy(buf, "recv success");
nRet = send(g_Client[i].sockClient, buf, strlen(buf) + 1, 0);
}
}
}
}
return 0;
}
客户端
// ClientTest.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
SOCKET g_socketClient = NULL;
DWORD WINAPI ThreadFunction(LPVOID pThis)
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(8899);
server.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
g_socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int nFlage = connect(g_socketClient, (sockaddr*)&server, sizeof(server));
if (nFlage < 0)
{
cout << "\nConnect Fail" << endl;
return 1;
}
while (true)
{
char szBuffer[1024] = { 0 };
int nRet = recv(g_socketClient, szBuffer, 1024, 0);
if (nRet > 0)
{
cout << endl << szBuffer << endl;
cout << "please input send content:";
}
}
closesocket(g_socketClient);
WSACleanup();
return 0;
}
int main()
{
DWORD dwThreadId = 0;
CreateThread(NULL, 0, ThreadFunction, NULL, 0, &dwThreadId);
while (true)
{
cout << "please input send content:";
string str;
cin >> str;
if (str == "q")
{
return 0;
}
send(g_socketClient, str.c_str(), str.length() + 1, 0);
}
return 0;
}
异步选择(WSAAsyncSelect)模型
异步选择(WSAAsyncSelect)模型是一个有用的异步I/O 模型。其核心函数是WSAAsyncSelect,该函数是非阻塞的
它可以用来在一个socket上接收以windows消息为基础的网络事件。它提供了读写数据的异步通知功能,但不提供异步数据传送。WSAAsyncSelect模型的优势在于只需要一个主线程即可。缺点是必须要绑定窗口句柄。即要先调用createwindow创建一个窗口。这也就是为什么这个模型只适用于windows操作系统而不能跨平台的原因。
WSAAsyncSelect 函数:
int WSAAsyncSelect(
__in SOCKET s,
__in HWND hWnd,
__in unsigned int wMsg,
__in long lEvent
);
s 参数指定的是我们感兴趣的那个套接字。
hWnd 参数指定一个窗口句柄,它对应于网络事件发生之后,想要收到通知消息的那个窗口。
wMsg 参数指定在发生网络事件时,打算接收的消息。该消息会投递到由hWnd窗口句柄指定的那个窗口。
(通常,应用程序需要将这个消息设为比Windows的WM_USER大的一个值,避免网络窗口消息与系统预定义的标准窗口消息发生混淆与冲突)
(即自定义消息)
lEvent 参数指定一个位掩码,对应于一系列网络事件的组合,大多数应用程序通常感兴趣的网络事件类型包括:
FD_READ、FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE。当然,到底使用FD_ACCEPT,还是使用FD_CONNECT类型
要取决于应用程序的身份是客户端,还是服务器。如应用程序同时对多个网络事件有兴趣,只需对各种类型执行一次简单的按位OR(或)运算,然后将它们分配给lEvent就可以了,例如:
WSAAsyncSeltct(s, hwnd,WM_SOCKET, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE);
解释说明:我们的应用程序以后便可在套接字s上,接收到有关连接、发送、接收以及套接字关闭这一系列网络事件的通知。
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接完成的通知
FD_CLOSE 应用程序想接收与套接字关闭的通知
注意1
多个事件务必在套接字上一次注册!
另外还要注意的是,一旦在某个套接字上允许了事件通知,那么以后除非明确调用closesocket命令,
或者由应用程序针对那个套接字调用了WSAAsyncSelect,从而更改了注册的网络事件类型,否则的话,
事件通知会永远有效!若将lEvent参数设为0,效果相当于停止在套接字上进行的所有网络事件通知。
注意2
若应用程序针对一个套接字调用了WSAAsyncSelect,那么套接字的模式会自动从“阻塞”变成“非阻塞”。
这样一来,如果调用了像WSARecv这样的Winsock函数,但当时却并没有数据可用,那么必然会造成调用的失败,并返回WSAEWOULDBLOCK错误。
为防止这一点,应用程序应依赖于由WSAAsyncSelect的uMsg参数指定的用户自定义窗口消息,来判断网络事件类型何时在套接字上发生;而不应盲目地进行调用。
应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口类中,以Windows消息的形式,接收网络事件通知。
窗口例程通常定义如下:
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
● hWnd 参数指定一个窗口的句柄,对窗口例程的调用正是由那个窗口发出的。
● uMsg 参数指定需要对哪些消息进行处理。这里我们感兴趣的是WSAAsyncSelect调用中定义的消息。
● wParam 参数指定发生网络事件的socket。假若同时为这个窗口例程分配了多个套接字,这个参数的重要性便显示出来了。
● lParam参数中,包含了两方面重要的信息。其中,lParam的低字(低位字)指定了已经发生的网络事件,而lParam的高字(高位字)包含了可能出现的任何错误代码。
测试代码
// WSAAsyncSelectTest.cpp : 定义应用程序的入口点。
//
#include <WinSock2.h>
#include "framework.h"
#include "WSAAsyncSelectTest.h"
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define MAX_LOADSTRING 100
#define WM_SOCKET_MSG 0x04004
// 全局变量:
HINSTANCE hInst; // 当前实例
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void initSocekt();
HWND g_hWnd = NULL;
SOCKET g_sockListen;
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_WSAASYNCSELECTTEST, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
initSocekt();
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WSAASYNCSELECTTEST));
MSG msg;
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
WSACleanup();
return (int) msg.wParam;
}
//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WSAASYNCSELECTTEST));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WSAASYNCSELECTTEST);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassExW(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中
g_hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!g_hWnd)
{
return FALSE;
}
ShowWindow(g_hWnd, nCmdShow);
UpdateWindow(g_hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_SOCKET_MSG:
if (WSAGETASYNCERROR(lParam)) //WSAGENSELECTERROR宏获得是否有错误 这里用HIWORD(lParam)也是可以的
{
MessageBoxA(g_hWnd, "标题", "网络错误", MB_OK);
closesocket(wParam);
return 0;
}
switch (WSAGETSELECTEVENT(lParam)) //WSAGETSELECTEVENT获取网络事件 这里也可以用LODORD
{
case FD_ACCEPT: //case里定义变量时要加入{}
{
SOCKADDR_IN cliAdd;
int len = sizeof(SOCKADDR);
SOCKET cliSock = accept(wParam, (SOCKADDR*)&cliAdd, &len);
WSAAsyncSelect(cliSock, g_hWnd, WM_SOCKET_MSG, FD_READ | FD_WRITE | FD_CLOSE);//该套接字也要用WSAAsyncSelect处理
if (cliSock == INVALID_SOCKET)
{
MessageBoxA(g_hWnd, "标题", "接收连接出错", MB_OK);
return 0;
}
break;
}
case FD_READ:
{
char bufData[1024] = { 0 };
int flag;
flag = recv(wParam, (char*)bufData, 1024, 0);
if (flag == 0)
{
MessageBoxA(g_hWnd, "标题", "连接已经断开", MB_OK);
}
else
{
MessageBoxA(g_hWnd, "标题", bufData, MB_OK);
}
break;
}
case FD_WRITE:
{
wParam = wParam;//不做处理
break;
}
case FD_CLOSE:
{
closesocket(wParam);
break;
}
default:
break;
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
void initSocekt()
{
//初始化socket
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//创建socket
g_sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//定义服务端socket地址
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8899);
//绑定端口
bind(g_sockListen, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
//监听端口
listen(g_sockListen, SOMAXCONN);
//当有感兴趣的事件发生时用Windows消息通知
WSAAsyncSelect(g_sockListen, g_hWnd, WM_SOCKET_MSG, FD_ACCEPT | FD_CLOSE);
}
客户端代码与上一个例子相同
WSAEventSelect模型
WSAEventSelect模型和WSAAsyncSelec模型类似,都是用调用WSAXXXXXSelec函数将socket和事件关联并注册到系统,并将socket设置成非阻塞模式。二者不同之处在于socket事件的通知方法:WSAAsyncSelec模型利用窗口句柄和消息映射函数通知网络事件,而WSAEventSelect模型利用WSAEVENT通知网络事件。
相关函数
WSAEventSelect注册网络事件
int WSAEventSelect( //成功返回0
SOCKET s, //要注册的socket
WSAEVENT hEventObject,//网络事件对象
long lNetworkEvents //网络事件,FD_开头的
);
WSAEVENT、WSACreateEvent、WSACloseEvent、WSASetEvent、WSAResetEvent
和windows的event对象一样,WSAEVENT实际类型是HANDLE。WSACreateEvent和WSACloseEvent分别用了创建WSAEVENT和销毁WSAEVENT。函数定义如:
WSACreateEvent
WSAEVENT WSACreateEvent (void);
WSACloseEvent
BOOL WSACloseEvent(__in WSAEVENT hEvent);
WSASetEvent
BOOL WSASetEvent(__in WSAEVENT hEvent);
WSAResetEvent
BOOL WSAResetEvent(__in WSAEVENT hEvent);
WSAWaitForMultipleEvents
WSAWaitForMultipleEvents函数在一个或所有指定的事件对象处于信号状态、超时间隔到期或 I/O 完成例程已执行时返回
WSAWaitForMultipleEvents 返回由以下几种情况造成:1,一个或多个event编程授信状态 ;2,时间到;3,IO完成例程开始执行
DWORD WSAWaitForMultipleEvents(
__in DWORD cEvents, //phEvents指向的数组中事件对象句柄的数量
__in const WSAEVENT* lphEvents, //l指向事件对象句柄数组的指针
__in BOOL fWaitAll, //指定等待类型的值。如果为 TRUE,则当lphEvents数组中的所有对象的状态发出信号时,该函数返回。如果为 FALSE,则该函数在任何事件对象发出信号时返回
__in DWORD dwTimeout, //超时间隔,以毫秒为单位。如果超时间隔到期,WSAWaitForMultipleEvents将返回
__in BOOL fAlertable //先置为FALSE
);
这个函数会返回一个index,表示是第几个网络事件对象,根据这个index也可以索引到socket,然后下面调用WSAEnumNetworkEvents来判断该socket上发生了什么事件
测试代码
服务端:
#pragma once
#include <WinSock2.h>
#include <Windows.h>
class CEventSelect
{
public:
CEventSelect(void);
~CEventSelect(void);
private:
void run();
WSAData m_wsa;
bool m_bRes;
SOCKET m_listensocket;
private:
WSAEVENT m_events[WSA_MAXIMUM_WAIT_EVENTS];
SOCKET m_socks[WSA_MAXIMUM_WAIT_EVENTS];
int m_eventsNum;
};
#include "EventSelect.h"
#include <thread>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
CEventSelect::CEventSelect(void)
{
m_bRes = true;
//初始化socket
WSAStartup(MAKEWORD(2, 3), &m_wsa);
//创建socket
m_listensocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_listensocket == INVALID_SOCKET)
{
m_bRes = false;
}
//绑定socket
sockaddr_in m_server;
m_server.sin_family = AF_INET;
m_server.sin_port = htons(8899);
m_server.sin_addr.s_addr = inet_addr("127.0.0.1");
if (m_bRes && (bind(m_listensocket, (sockaddr*)&m_server, sizeof(sockaddr_in)) == SOCKET_ERROR))
{
DWORD dw = WSAGetLastError();
m_bRes = false;
}
//监听socket
if (m_bRes && (listen(m_listensocket, SOMAXCONN) == SOCKET_ERROR))
{
m_bRes = false;
}
//创建网络事件对象
WSAEVENT e = WSACreateEvent();
//注册网络事件
if (m_bRes && WSAEventSelect(m_listensocket, e, FD_ACCEPT | FD_CLOSE))
{
m_bRes = false;
}
m_eventsNum = 0;
m_events[m_eventsNum] = e;
m_socks[m_eventsNum] = m_listensocket;
m_eventsNum++;
std::thread(&CEventSelect::run, this).detach();
}
CEventSelect::~CEventSelect(void)
{
for (int i = 0; i < m_eventsNum; i++)
{
//关闭event和socket,首先应当使用WSAEventSelect设置事件0,接触socket和event的关联
WSAEventSelect(m_socks[i], m_events[i], 0);
//关闭socket
closesocket(m_socks[i]);
//关机event
WSACloseEvent(m_events[i]);
}
}
void CEventSelect::run()
{
while (true)
{
DWORD dw = WSAWaitForMultipleEvents(m_eventsNum, m_events, FALSE, WSA_INFINITE, FALSE);
if (dw == WSA_WAIT_TIMEOUT)
{
continue;
}
//如果WSAWaitForMultipleEvents第三项是true,则返回值标识所有的事件都已授信
//如果WSAWaitForMultipleEvents第三项是false,则返回值标识已授信事件的最小索引
DWORD index = dw - WSA_WAIT_EVENT_0;
//获取授信事件的最小索引,后面所有事件的状态都不知道,所以要从最小索引开始,轮询一遍
//否则,下次进入svc调用WSAWaitForMultipleEvents又返回最小索引
for (int i = index; i < m_eventsNum; i++)
{
//循环调用WSAWaitForMultipleEvents
//注意:此次调用参数不同,
//1:需要查询的事件个数为1,第一个参数为1,第二个参数为待查询的事件地址
//2:由于事件个数只有一个,第3个参数可改成true
//3:不能设置等待时间为WSA_INFINITE,如果当前事件无限期等待,程序将阻塞在此
DWORD dw = WSAWaitForMultipleEvents(1, &m_events[i], TRUE, 1000, FALSE);
if (dw == WSA_WAIT_TIMEOUT || dw == WSA_WAIT_FAILED)
{
//超时,下一个事件查询
continue;
}
//使用WSAEnumNetworkEvents 获取授信事件
WSANETWORKEVENTS nwevents;
//获取socket[i]的事件集合,并将重置m_events[i]
WSAEnumNetworkEvents(m_socks[i], m_events[i], &nwevents);
//accept请求到达
if (nwevents.lNetworkEvents & FD_ACCEPT)
{
//验证当前网络状态,非0对应错误码
if (nwevents.iErrorCode[FD_ACCEPT_BIT] == 0)
{
sockaddr_in m_client;
int sz = sizeof(sockaddr_in);
SOCKET acp = accept(m_socks[i], (sockaddr*)&m_client, &sz);
if (acp == INVALID_SOCKET)
{
continue;
}
//为新连接的socket关联事件
WSAEVENT e = WSACreateEvent();
WSAEventSelect(acp, e, FD_READ | FD_WRITE | FD_CLOSE);
m_events[m_eventsNum] = e;
m_socks[m_eventsNum] = acp;
m_eventsNum++;
}
}
else if (nwevents.lNetworkEvents & FD_READ)
{
if (nwevents.iErrorCode[FD_READ_BIT] == 0)
{
char buf[1024];
int res = recv(m_socks[i], buf, 1024, 0);
if (res == 0)
{
closesocket(m_socks[i]);
break;
}
buf[res] = 0;
std::cout << buf << std::endl;
}
}
else if (nwevents.lNetworkEvents & FD_WRITE)
{
if (nwevents.iErrorCode[FD_WRITE_BIT] == 0)
{
std::string str = "send data from service";
int sz = send(m_socks[i], str.c_str(), str.length(), 0);
if (sz == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAEWOULDBLOCK)
{
continue;
}
}
}
}
else if (nwevents.lNetworkEvents & FD_CLOSE)
{
//此处不应再判断结束状态,非正常终止的连接将返回错误码10053或10054,而非0
//if (nwevents.iErrorCode[FD_CLOSE_BIT] == 0)
{
closesocket(m_socks[i]);
//从socket数组中移除当前socket
//此过程省略
}
}
}
}
}
重叠IO(overlapped)模型
比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。
因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。
而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。
重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。
需要注意的是,有两个方法可以用来管理重叠IO请求的完成情况(就是说接到重叠操作完成的通知):
- 事件对象通知(event object notification)
- 完成例程(completion routines) ,注意,这里并不是完成端口
IOCP模型
IOCP原理
IOCP包括三个部分:完成端口(存放重叠的I/O请求),客户端请求的处理,等待线程队列(一定数量的工作者线程,一般采用CPU*2个)
线程池中的工作线程的数量与CPU内核数量相同,用它来最小化线程切换代价。一个IOCP对象,在操作系统中可关联着多个socket和文件控制端。IOCP对象内部有一个先进先出(FIFO)队列,用于存放IOCP所关联的输入输出端的服务请求完成消息。请求输入输出服务的进程不接受IO服务完成通知,而是检查IOCP的消息队列以确定IOCP请求状态。(线程池中的)多个线程负责从IOCP消息队列中取走完成通知并执行数据处理,如果队列中没有消息,那么线程阻塞挂起在该队列中。这些线程从而实现负载均衡。
IOCP优势
- IOCP基于异步IO,决定了IOCP所实现服务器高吞吐量。
- 引入IOCP,减少Thread切换带来的额外开销,最小化的线程上下文切换,减少线程切换带来的巨大开销,让CPU把大量时间用于线程运行。当与该完成端口相关联的可运行线程的总数目达到该并发量,系统就会阻塞。
IOCP基本应用
- 创建和关联完成端口
- 与socket进行关联
- 获取队列完成状态
- 用于IOCP函数
- 投递一个队列完成状态
IOCP模型流程
IOCP常用函数介绍
- 创建和关联完成端口:CreateIoCompletionPort
- 与socket进行关联:CreateIoCompletionPort
- 获取队列完成状态:GetQueuedCompletionStatus
- 投递一个队列完成状态:PostQueuedCompletionStatus
服务端代码
// IOCP-DEMO.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <string>
#include <WinSock2.h>
#include <Windows.h>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
//#pragma comment(lib, "kernel32.lib")
HANDLE g_hIocp; //定义一个全局句柄
char cBuffer[1024];
enum IO_OPERATION { IO_READ, IO_WRITE }; //io操作状态
struct IO_DATA
{
OVERLAPPED Overlapped;
WSABUF wsabuf;
int nBytes;
IO_OPERATION opCode;
SOCKET client;
};
//工作线程
DWORD WINAPI WorkerThreadFunc(LPVOID workThreadContext)
{
int nRet = 0;
void* lpCompletionKey = nullptr;
IO_DATA* lpIOContext = nullptr;
DWORD nBytes = 0;
DWORD dwFlags = 0;
DWORD dwIOSize = 0;
LPOVERLAPPED lpOverlapped = nullptr;
while (true)
{
GetQueuedCompletionStatus(g_hIocp, &dwIOSize, (LPDWORD)&lpCompletionKey, (LPOVERLAPPED*)&lpOverlapped, INFINITE);
lpIOContext = (IO_DATA*)lpOverlapped;
if (dwIOSize == 0)
{
cout << "Client Disconnect." << endl;
closesocket(lpIOContext->client);
delete lpIOContext;
continue;
}
if (lpIOContext->opCode == IO_READ)
{
ZeroMemory(&lpIOContext->Overlapped, sizeof(lpIOContext->Overlapped));
lpIOContext->wsabuf.buf = cBuffer;
lpIOContext->wsabuf.len = strlen(cBuffer) + 1;
lpIOContext->opCode = IO_WRITE;
lpIOContext->nBytes = strlen(cBuffer) + 1;
dwFlags = 0;
nBytes = strlen(cBuffer) + 1;
nRet = WSASend(lpIOContext->client, &lpIOContext->wsabuf, 1, &nBytes, dwFlags, &(lpIOContext->Overlapped), nullptr);
if (nRet == SOCKET_ERROR && ERROR_IO_PENDING != WSAGetLastError())
{
cout << "WSASend Failed: Reason Code:" << WSAGetLastError() << endl;
closesocket(lpIOContext->client);
delete lpIOContext;
continue;
}
memset(cBuffer, 0, sizeof(cBuffer));
}
else if (lpIOContext->opCode == IO_WRITE)
{
lpIOContext->opCode = IO_READ;
nBytes = 1024;
dwFlags = 0;
lpIOContext->wsabuf.buf = cBuffer;
lpIOContext->wsabuf.len = nBytes;
lpIOContext->nBytes = nBytes;
ZeroMemory(&lpIOContext->Overlapped, sizeof(lpIOContext->Overlapped));
nRet = WSARecv(lpIOContext->client, &lpIOContext->wsabuf, 1, &nBytes, &dwFlags, &lpIOContext->Overlapped, nullptr);
if (nRet == SOCKET_ERROR && ERROR_IO_PENDING != WSAGetLastError())
{
cout << "WSARecv Failed: Reason Code:" << WSAGetLastError() << endl;
closesocket(lpIOContext->client);
delete lpIOContext;
continue;
}
cout << lpIOContext->wsabuf.buf << endl;
}
}
return 0;
}
int main()
{
WSADATA wsaDta;
WSAStartup(MAKEWORD(2, 2), &wsaDta);
SOCKET m_socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(8899);
server.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
bind(m_socket, (sockaddr*)&server, sizeof(server));
listen(m_socket, 10);
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
int g_ThreadCount = sysInfo.dwNumberOfProcessors * 2;
g_hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, g_ThreadCount);
for (int i = 0; i < g_ThreadCount; i++)
{
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, WorkerThreadFunc, 0, 0, &dwThreadId);
CloseHandle(hThread);
}
while (true)
{
SOCKET client = accept(m_socket, NULL, NULL);
cout << "Client Connected" << endl;
if (CreateIoCompletionPort((HANDLE)client, g_hIocp, 0, 0) == NULL)
{
cout << "Binding Client Socket to IO Completion Port Failed Reason Code:" << GetLastError() << endl;
closesocket(client);
}
else
{
IO_DATA* data = new IO_DATA;
memset(cBuffer, 0, 1024);
memset(&data->Overlapped, 0, sizeof(data->Overlapped));
data->opCode = IO_READ;
data->nBytes = 0;
data->wsabuf.buf = cBuffer;
data->wsabuf.len = sizeof(cBuffer);
data->client = client;
DWORD nBytes = 1024;
DWORD dwFlags = 0;
int nRet = WSARecv(client, &data->wsabuf, 1, &nBytes, &dwFlags, &data->Overlapped, NULL);
if (nRet == SOCKET_ERROR && ERROR_IO_PENDING != WSAGetLastError())
{
cout << "WSARecv Failed: Reason Code:" << WSAGetLastError() << endl;
closesocket(client);
delete data;
}
cout << data->wsabuf.buf << endl;
}
}
closesocket(m_socket);
WSACleanup();
return 0;
}
客户端代码
// ClientIocps.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <string>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaDta;
WSAStartup(MAKEWORD(2, 2), &wsaDta);
sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(8899);
server.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int nFlag = connect(client, (sockaddr*)&server, sizeof(server));
if (nFlag < 0)
{
cout << "Connect Fail" << endl;
return 1;
}
while (true)
{
cout << "Send Hello World" << endl;
char cBuffer[1024] = { 0 };
strncpy(cBuffer, "Hello Server", 1024);
send(client, cBuffer, 1024, 0);
memset(cBuffer, 0, sizeof(cBuffer));
cout << "Recv:" << endl;
int nRev = recv(client, cBuffer, 1024, 0);
if (nRev == 0)
{
cout << "Recv Nothing" << endl;
}
else
{
cout << cBuffer << endl;
}
Sleep(1000);
}
closesocket(client);
WSACleanup();
return 0;
}
比较windows下的5种IO模型
看到一个很有意思的解释:
老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
这和Socket模型非常类似。下面我就以老陈接收信件为例讲解SocketI/O模型。
select模型:
老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信,在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
select模型和老陈的这种情况非常相似:周而复始地去检查…如果有数据…接收/发送…
WSAAsyncSelect模型:
后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天…不是,微软…
微软提供的WSAAsyncSelect模型就是这个意思。
WSAAsyncSelect模型是Windows下最简单易用的一种Socket I/O模型。使用这种模型时,Windows会把网络事件以消息的形势通知应用程序。
WSAEventSelect模型:
后来,微软的信箱非常畅销,购买微软信箱的人以百万计数…以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使。微软改进 了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以睡觉了。
Overlapped I/O 事件通知模型:
后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!
Overlapped I/O 事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在“Overlapped”,Overlapped模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区~~~~~
Overlapped I/O 完成例程模型:
老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件…为了进一步减轻用户负担,微软又开发了 一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读/回复了!老陈终于过上了小资生活!
IOCP模型:
微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏…
微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!
“Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的 [没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多 CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。