1. Windows网络I/O模型
两种I/O模式:
(1) 阻塞模式:执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字默认为阻塞模式。可以通过多线程技术进行处理。
(2) 非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回WSAEWOULDBLOCK错误。但功能强大。例如,如何让recv从阻塞状态下变成非阻塞状态呢?
1. 设置Recv超时(setsockopt);
2. 调用ioctlsocket函数设置成非阻塞;
3. 使用一个工作线程来recv,这样不影响主线程;
4. 用Select、异步IO…
为了解决这个问题,提出了下面的进行I/O操作的一些I/O模型。
1.0 I/O模型客户端
如果你想在Windows平台上构建服务器应用,那么I/O模型是你必须考虑的。Windows操作系统提供了选择(Select)、异步选择(WSAAsyncSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped I/O)和完成端口(Completion Port)共五种I/O模型。每一种模型均适用于一种特定的应用场景。程序员应该对自己的应用需求非常明确,而且综合考虑到程序的扩展性和可移植性等因素,作出自己的选择。
我会以一个回应反射式服务器(与《Windows网络编程》第八章一样)来介绍这五种I/O模型。我们假设客户端的代码如下(为代码直观,省去所有错误检查,以下同):
// common.h
#ifndef __COMMON_H__
#define __COMMON_H__
#define PORT 8800
#define MSGSIZE 1024
#define SERVER_ADDRESS "127.0.0.1"
#include <winsock2.h>
#include <stdio.h>
#include <process.h>
#pragma comment(lib, "ws2_32.lib")
#endif
// Client.cpp
#include "stdafx.h"
#include "common.h"
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sClient;
SOCKADDR_IN server;
char szMessage[MSGSIZE];
int ret;
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
// Create client socket
sClient = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);
// Connect to server
memset(&server,0, sizeof(SOCKADDR_IN));
server.sin_family= AF_INET;
server.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS);
server.sin_port= htons(PORT);
connect(sClient,(struct sockaddr*)&server, sizeof(SOCKADDR_IN));
while (TRUE)
{
printf("Send:");
gets(szMessage);
// Send message
send(sClient, szMessage, strlen(szMessage), 0);
// Receive message
ret = recv(sClient, szMessage,MSGSIZE, 0);
szMessage[ret]= '\0';
printf("Received[%d bytes]: '%s'\n", ret, szMessage);
}
// Clean up
closesocket(sClient);
WSACleanup();
return 0;
}
客户端所做的事情相当简单,创建套接字,连接服务器,然后不停的发送和接收数据。
比较容易想到的一种服务器模型就是采用一个主线程,负责监听客户端的连接请求,当接收到某个客户端的连接请求后,创建一个专门用于和该客户端通信的套接字和一个辅助线程。以后该客户端和服务器的交互都在这个辅助线程内完成。这种方法比较直观,程序非常简单而且可移植性好,但是不能利用平台相关的特性。例如,如果连接数增多的时候(成千上万的连接),那么线程数成倍增长,操作系统忙于频繁的线程间切换,而且大部分线程在其生命周期内都是处于非活动状态的,这大大浪费了系统的资源。所以,如果你已经知道你的代码只会运行在Windows平台上,建议采用Winsock I/O模型。
1.1 Select(选择模型)
Select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“Select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理。最初设计该模型时,主要面向的是某些使用UNIX操作系统的计算机,它们采用的是Berkeley套接字方案。Select模型已集成到Winsock1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。由于Winsock 1.1向后兼容于Berkeley套接字实施方案,所以假如有一个Berkeley套接字应用使用了select函数,那么从理论角度讲,毋需对其进行任何修改,便可正常运行。(节选自《Windows网络编程》第八章) 下面的这段程序就是利用选择模型实现的Echo服务器的代码(已经不能再精简了):
#include "stdafx.h"
#include "common.h"
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[FD_SETSIZE];
unsigned WINAPI WorkerThread(VOID* lpParameter);
int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData,
LPQOS lpSQOS,LPQOS lpGQOS,
LPWSABUF lpCalleeId, LPWSABUFlpCalleeData,
GROUP FAR * g,DWORD dwCallbackData)
{
if (g_iTotalConn< 3)
return CF_ACCEPT;
else
return CF_REJECT;
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sListen,sClient;
SOCKADDR_IN local,client;
int iaddrSize = sizeof(SOCKADDR_IN);
unsigned dwThreadId;
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family= AF_INET;
local.sin_port= htons(PORT);
bind(sListen, (struct sockaddr*)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen,3);
// Create worker thread
_beginthreadex(NULL,0, WorkerThread, NULL,0, &dwThreadId);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client,&iaddrSize);
//sClient = WSAAccept(sListen, (struct sockaddr*)&client, &iaddrSize, ConditionFunc, NULL);
printf("Acceptedclient:%s:%d\n", inet_ntoa(client.sin_addr),ntohs(client.sin_port));
// Add socket to g_CliSocketArr
g_CliSocketArr[g_iTotalConn++]= sClient;
}
return 0;
}
unsigned WINAPI WorkerThread(VOID* lpParam)
{
int i;
fd_set fdread; // 读集
int ret;
struct timeval tv = {1, 0}; //Maximumtime for select to wait
char szMessage[MSGSIZE];
while (TRUE)
{
// 初始化读集
FD_ZERO(&fdread);
// 将当前所有的客户端套接字加入到读集fdread中
for (i = 0; i < g_iTotalConn;i++)
{
FD_SET(g_CliSocketArr[i],&fdread);
}
// We only care read event
// The select function returns the total number of sockethandles that
// are ready and contained in the fd_set structures, zeroif the time limit expired,
// or SOCKET_ERROR if an error occurred.
ret = select(0,&fdread, NULL,NULL, &tv);
if (ret == 0 || ret == SOCKET_ERROR)
{
// Time expired or an error occurred
continue;
}
for (i = 0; i < g_iTotalConn;i++)
{
if (FD_ISSET(g_CliSocketArr[i], &fdread))
{
// A read event happened on g_CliSocketArr[i]
SOCKET sockConn= g_CliSocketArr[i];
ret = recv(sockConn, szMessage,MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR&& WSAGetLastError() == WSAECONNRESET))
{
// Client socket closed
printf("Clientsocket %d closed.\n", sockConn);
closesocket(sockConn);
// g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
--g_iTotalConn;
for (int j=i; j<g_iTotalConn;j++)
{
g_CliSocketArr[j]= g_CliSocketArr[j+1];
}
}
else
{
// We received a message from client
szMessage[ret]= '\0';
send(sockConn, szMessage, strlen(szMessage), 0);
}
}
}
}
return 0;
}
服务器的几个主要动作如下:
1.创建监听套接字,绑定,监听;
2.创建工作者线程;
3.创建一个套接字数组,用来存放当前所有活动的客户端套接字,每accept一个连接就更新一次数组;
4.接受客户端的连接。这里有一点需要注意的,就是我没有重新定义FD_SETSIZE宏,所以服务器最多支持的并发连接数为64。而且,这里决不能无条件的accept,服务器应该根据当前的连接数来决定是否接受来自某个客户端的连接。一种比较好的实现方案就是采用WSAAccept函数,而且让WSAAccept回调自己实现的ConditionFunction。如下所示:
int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOSlpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,
GROUP FAR *g,DWORD dwCallbackData)
{
if (当前连接数 < FD_SETSIZE)
return CF_ACCEPT;
else
return CF_REJECT;
}
工作者线程里面是一个死循环,一次循环完成的动作是:
1.将当前所有的客户端套接字加入到读集fdread中;
2.调用select函数;
3.查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)。除了需要有条件接受客户端的连接外,还需要在连接数为0的情形下做特殊处理,因为如果读集中没有任何套接字,select函数会立刻返回,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%。
4.2 Select & MultiThread
// 开辟的最大线程数
const int MAX_THREADS_NUM= 2;
// 每个线程处理的客户端数
const int MAX_CLIENTS_NUM_PER_THREAD= 2;
// 实际总连接数
int g_iTotalConn = 0;
// Socket数组
SOCKET g_CliSocketArr[MAX_THREADS_NUM*MAX_CLIENTS_NUM_PER_THREAD];
// 工作线程函数
unsigned WINAPI WorkerThread(VOID* lpParameter);
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sListen,sClient;
SOCKADDR_IN local,client;
int iaddrSize = sizeof(SOCKADDR_IN);
unsigned dwThreadId;
// 线程数组
HANDLE threadArray[MAX_THREADS_NUM];
// Initialize Windows socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family= AF_INET;
local.sin_port= htons(PORT);
bind(sListen, (struct sockaddr*)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen,MAX_THREADS_NUM*MAX_CLIENTS_NUM_PER_THREAD);
// Create worker thread
for (int i=0; i<MAX_THREADS_NUM; i++)
{
threadArray[i]= (HANDLE) _beginthreadex(NULL, 0, WorkerThread,(VOID*)i,
CREATE_SUSPENDED, &dwThreadId);
}
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client,&iaddrSize);
printf("Acceptedclient:%s:%d\n", inet_ntoa(client.sin_addr),ntohs(client.sin_port));
// Add socket to g_CliSocketArr
if (g_iTotalConn>= MAX_THREADS_NUM*MAX_CLIENTS_NUM_PER_THREAD)
continue;
ResumeThread(threadArray[g_iTotalConn/MAX_CLIENTS_NUM_PER_THREAD]);
g_CliSocketArr[g_iTotalConn++]= sClient;
}
return 0;
}
unsigned WINAPI WorkerThread(VOID* lpParam)
{
int i;
fd_set fdread; // 读集
int ret;
struct timeval tv = {1, 0}; //Maximumtime for select to wait
char szMessage[MSGSIZE];
char szSendMessage[MSGSIZE] = {0};
//该工作线程处理的客户端起始序号
int start= ((int)lpParam)*MAX_CLIENTS_NUM_PER_THREAD;
while (TRUE)
{
//计算该工作线程处理的客户端终止序号
//由于g_iTotalConn会变化,所以放在循环中做
int end= (((int)lpParam)+1)*MAX_CLIENTS_NUM_PER_THREAD < g_iTotalConn
?(((int)lpParam)+1)*MAX_CLIENTS_NUM_PER_THREAD : g_iTotalConn;
// 初始化读集
FD_ZERO(&fdread);
// 将当前自己负责的客户端的套接字加入到读集fdread中
for (i = start; i < end; i++)
{
FD_SET(g_CliSocketArr[i], &fdread);
}
// We only care read event
// The select function returns the total number of sockethandles that
// are ready and contained in the fd_set structures, zeroif the time limit expired,
// or SOCKET_ERROR if an error occurred.
ret = select(0,&fdread, NULL,NULL, &tv);
if (ret == 0 || ret == SOCKET_ERROR)
{
// Time expired or an error occurred
continue;
}
for (i = start; i < end; i++)
{
if (FD_ISSET(g_CliSocketArr[i], &fdread))
{
// A read event happened on g_CliSocketArr[i]
SOCKET sockConn= g_CliSocketArr[i];
ret = recv(sockConn, szMessage, MSGSIZE,0);
if (ret == 0 || (ret == SOCKET_ERROR&& WSAGetLastError() == WSAECONNRESET))
{
// Client socket closed
printf("Clientsocket %d closed.\n", sockConn);
closesocket(sockConn);
// g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
--g_iTotalConn;
for (int j=i; j<g_iTotalConn;j++)
{
g_CliSocketArr[j]= g_CliSocketArr[j+1];
}
}
else
{
// We received a message from client
szMessage[ret]= '\0';
memset(szSendMessage,0, MSGSIZE);
sprintf(szSendMessage,"From thread: %d, %s", (int)lpParam, szMessage);
send(sockConn, szSendMessage, strlen(szSendMessage), 0);
}
}
}
}
return 0;
}
1.3 WSAAsyncSelect(异步选择模型)
/*
Winsock提供了一个有用的异步I/O模型。利用这个模型,
应用程序可在一个套接字上,接收以Windows消息
为基础的网络事件通知。具体的做法是在建好一个套接字后,
调用WSAAsyncSelect函数。该模型最早出现于Winsock的.1版本中,
用于帮助应用程序开发者面向一些早期的位Windows平台
(如Windows for Workgroups),适应其“落后”的多任务消息环境。
应用程序仍可从这种模型中得到好处,特别是它们用一个
标准的Windows例程(常称为"WndProc"),对窗口消息进行管理的时候。
该模型亦得到了Microsoft Foundation Class(微软基本类,MFC)对象
CSocket的采纳。(节选自《Windows网络编程》第八章)
*/
//之所以用Win32程序框架,是因为利用它的回调函数。
// 当然也可以使用MFC框架(见孙鑫网络聊天程序)
#include "stdafx.h"
#include "WSAAsyncSelect.h"
#include "common.h"
#define WM_SOCKWM_USER+1 //自定义消息
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING]; //主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
// 回调函数
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
// 主函数
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此放置代码。
MSG msg;
HACCEL hAccelTable;
// 初始化全局字符串
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_WSAASYNCSELECT,szWindowClass,MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WSAASYNCSELECT));
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 函数:MyRegisterClass()
//
// 目的: 注册窗口类。
//
// 注释:
//
// 仅当希望
// 此代码与添加到Windows95 中的“RegisterClassEx”
// 函数之前的Win32系统兼容时,才需要此函数及其用法。调用此函数十分重要,
// 这样应用程序就可以获得关联的
// “格式正确的”小图标。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX 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_WSAASYNCSELECT));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_WSAASYNCSELECT);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassEx(&wcex);
}
//
// 函数:InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // 将实例句柄存储在全局变量中
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(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)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
WSADATA wsd;
static SOCKET sListen;
SOCKET sClient;
SOCKADDR_IN local, client;
int ret, iAddrSize = sizeof(client);
char szMessage[MSGSIZE];
switch (message)
{
case WM_CREATE:
{
// Initialize Windows Socket library
WSAStartup(0x0202, &wsd);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(local));
// Listen
listen(sListen, 3);
// Associate listening socket with FD_ACCEPT event
WSAAsyncSelect(sListen, hWnd, WM_SOCK, FD_ACCEPT);
}
break;
case WM_SOCK:
{
if (WSAGETSELECTERROR(lParam))
{
closesocket(wParam);
break;
}
switch (WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
{
// Accept a connection from client
sClient = accept(wParam, (struct sockaddr *)&client, &iAddrSize);
// Associate client socket with FD_READ and FD_CLOSE event
WSAAsyncSelect(sClient, hWnd, WM_SOCK, FD_READ | FD_CLOSE);
// 打印
memset(szMessage, 0, MSGSIZE);
sprintf_s(szMessage, MSGSIZE, "Accepted client:%s:%d\n",
inet_ntoa(client.sin_addr), ntohs(client.sin_port));
}
break;
case FD_READ:
{
ret = recv(wParam, szMessage, MSGSIZE, 0);
if (ret == 0 || ret == SOCKET_ERROR &&WSAGetLastError()== WSAECONNRESET)
{
closesocket(wParam);
}
else
{
szMessage[ret] = '\0';
send(wParam, szMessage, strlen(szMessage), 0);
}
}
break;
case FD_CLOSE:
{
closesocket(wParam);
}
break;
default:
break;
}
}
break;
case WM_COMMAND:
{
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
// TODO: 在此添加任意绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
{
closesocket(sListen);
WSACleanup();
PostQuitMessage(0);
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
WSAAsyncSelect是最简单的一种Winsock I/O模型啦(之所以说它简单是因为一个主线程就搞定了)。使用Raw Windows API写过窗口类应用程序的人应该都能看得懂。这里,我们需要做的仅仅是:
(1). 在WM_CREATE消息处理函数中,初始化Windows Socketlibrary,创建监听套接字,绑定,监听,并且调用WSAAsyncSelect函数表示我们关心在监听套接字上发生的FD_ACCEPT事件;
(2). 自定义一个消息WM_SOCK,一旦在我们所关心的套接字(监听套接字和客户端套接字)上发生了某个事件,系统就会调用WndProc并且message参数被设置为WM_SOCK;
(3). 在WM_SOCK的消息处理函数中,分别对FD_ACCEPT、FD_READ和FD_CLOSE事件进行处理;
(4). 在窗口销毁消息(WM_DESTROY)的处理函数中,我们关闭监听套接字,清除Windows Socket library。
下面是用于WSAAsyncSelect函数的网络事件类型:
FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_OOB 应用程序想接收是否有带外(OOB)数据抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通知
FD_QOS 应用程序想接收套接字“服务质量”(QoS)发生更改的通知
FD_GROUP_QOS 应用程序想接收套接字组“服务质量”发生更改的通知(现在没什么用处,为未来套接字组的使用保留)
FD_ROUTING_INTERFACE_CHANGE 应用程序想接收在指定的方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE 应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知。
1.4 WSAEventSelect(事件选择模型)
/*
Winsock提供了另一个有用的异步I/O模型。
和WSAAsyncSelect模型类似的是,它也允许
应用程序在一个或多个套接字上,接收以事件
为基础的网络事件通知。对于由WSAAsyncSelect模型
采用的网络事件来说,它们均可原封不动地移植到新模型。
在用新模型开发的应用程序中,也能接收和处理所有那些事件。
该模型最主要的差别在于网络事件会投递至一个事件对象句柄,
而非投递至一个窗口例程。选自《Windows网络编程》。
*/
#include "stdafx.h"
#include "common.h"
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
DWORD WINAPI WorkerThread(LPVOID);
void Cleanup(int index);
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
DWORD dwThreadId;
int iaddrSize = sizeof(SOCKADDR_IN);
// Initialize Windows Socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen, 3);
// Create worker thread
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Associate socket with network event
g_CliSocketArr[g_iTotalConn] =sClient;
g_CliEventArr[g_iTotalConn] =WSACreateEvent();
WSAEventSelect(g_CliSocketArr[g_iTotalConn], g_CliEventArr[g_iTotalConn],
FD_READ | FD_CLOSE);
g_iTotalConn++;
}
return 0;
}
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int ret, index;
WSANETWORKEVENTS NetworkEvents;
char szMessage[MSGSIZE];
while (TRUE)
{
ret =WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
if (ret == WSA_WAIT_FAILED ||ret == WSA_WAIT_TIMEOUT)
{
continue;
}
index =ret - WSA_WAIT_EVENT_0;
WSAEnumNetworkEvents(g_CliSocketArr[index], g_CliEventArr[index], &NetworkEvents);
if (NetworkEvents.lNetworkEvents &FD_READ) //读事件
{
// Receive message from client
ret = recv(g_CliSocketArr[index], szMessage, MSGSIZE, 0);
if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
{
Cleanup(index);
}
else
{
szMessage[ret] = '\0';
send(g_CliSocketArr[index], szMessage, strlen(szMessage), 0);
}
}
if (NetworkEvents.lNetworkEvents &FD_CLOSE) //关闭事件
{
Cleanup(index);
printf("Client socket %d closed.\n", g_CliSocketArr[index]);
}
}
return 0;
}
void Cleanup(int index)
{
closesocket(g_CliSocketArr[index]);
WSACloseEvent(g_CliEventArr[index]);
if (index < g_iTotalConn - 1)
{
g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn - 1];
g_CliEventArr[index] = g_CliEventArr[g_iTotalConn - 1];
}
g_iTotalConn--;
}
事件选择模型也比较简单,实现起来也不是太复杂,它的基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网络事件。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。程序定义了两个全局数组,一个套接字数组,一个WSAEVENT对象数组,其大小都是MAXIMUM_WAIT_OBJECTS(64),两个数组中的元素一一对应。
同样的,这里的程序没有考虑两个问题,一是不能无条件的调用accept,因为我们支持的并发连接数有限。解决方法是将套接字按MAXIMUM_WAIT_OBJECTS分组,每MAXIMUM_WAIT_OBJECTS个套接字一组,每一组分配一个工作者线程;或者采用WSAAccept代替accept,并回调自己定义的Condition Function。第二个问题是没有对连接数为0的情形做特殊处理,程序在连接数为0的时候CPU占用率为100%。
1.5 Overlapped(重叠I/O模型)
1.5.1 使用事件通知方式实现
/* ! <
用事件通知方式实现的重叠I/O模型
这个模型与上述其他模型不同的是它使用Winsock2提供的异步I/O函数WSARecv。
在调用WSARecv时, 指定一个WSAOVERLAPPED结构,这个调用不是阻塞的,也就是说,它会立刻返回。
一旦有数据到达的时候,被指定的WSAOVERLAPPED结构中的hEvent被Signaled。
由于语句:g_CliEventArr[g_iTotalConn]= g_pPerIODataArr[g_iTotalConn]->overlap.hEvent;
使得与该套接字相关联的WSAEVENT对象也被Signaled,所以WSAWaitForMultipleEvents的调用操作成功返回。
我们现在应该做的就是用与调用WSARecv相同的WSAOVERLAPPED结构为参数调用WSAGetOverlappedResult,从而
得到本次I/O传送的字节数等相关信息。在取得接收的数据后,把数据原封不动的发送到客户端,然后重新
激活一个WSARecv异步操作。
*/
#include "stdafx.h"
#include "common.h"
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
int g_iTotalConn = 0;
SOCKET g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
LPPER_IO_OPERATION_DATA g_pPerIODataArr[MAXIMUM_WAIT_OBJECTS];
unsigned int WINAPI WorkerThread(void*);
void Cleanup(int);
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
DWORD dwThreadId;
int iaddrSize = sizeof(SOCKADDR_IN);
// Initialize Windows Socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen, 3);
// Create worker thread
_beginthreadex(NULL, 0, WorkerThread, NULL, 0, (unsigned*)&dwThreadId);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
g_CliSocketArr[g_iTotalConn] =sClient;
// Allocate a PER_IO_OPERATION_DATA structure
g_pPerIODataArr[g_iTotalConn] = (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
g_pPerIODataArr[g_iTotalConn]->Buffer.len = MSGSIZE;
g_pPerIODataArr[g_iTotalConn]->Buffer.buf = g_pPerIODataArr[g_iTotalConn]->szMessage;
g_CliEventArr[g_iTotalConn] =g_pPerIODataArr[g_iTotalConn]->overlap.hEvent = WSACreateEvent();
// Launch an asynchronous operation
WSARecv(
g_CliSocketArr[g_iTotalConn],
&g_pPerIODataArr[g_iTotalConn]->Buffer,
1,
&g_pPerIODataArr[g_iTotalConn]->NumberOfBytesRecvd,
&g_pPerIODataArr[g_iTotalConn]->Flags,
&g_pPerIODataArr[g_iTotalConn]->overlap,
NULL);
g_iTotalConn++;
}
closesocket(sListen);
WSACleanup();
return 0;
}
unsigned int WINAPI WorkerThread(void* lpParam)
{
int ret, index;
DWORD cbTransferred;
while (TRUE)
{
ret =WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
if (ret == WSA_WAIT_FAILED ||ret == WSA_WAIT_TIMEOUT)
{
continue;
}
index= ret - WSA_WAIT_EVENT_0;
WSAResetEvent(g_CliEventArr[index]);
WSAGetOverlappedResult(
g_CliSocketArr[index],
&g_pPerIODataArr[index]->overlap,
&cbTransferred,
TRUE,
&g_pPerIODataArr[g_iTotalConn]->Flags);
if (cbTransferred == 0)
{
// The connection was closed by client
Cleanup(index);
}
else
{
// g_pPerIODataArr[index]->szMessage contains thereceived data
g_pPerIODataArr[index]->szMessage[cbTransferred] = '\0';
send(g_CliSocketArr[index], g_pPerIODataArr[index]->szMessage, cbTransferred, 0);
// Launch another asynchronous operation
WSARecv(
g_CliSocketArr[index],
&g_pPerIODataArr[index]->Buffer,
1,
&g_pPerIODataArr[index]->NumberOfBytesRecvd,
&g_pPerIODataArr[index]->Flags,
&g_pPerIODataArr[index]->overlap,
NULL);
}
}
return 0;
}
void Cleanup(int index)
{
closesocket(g_CliSocketArr[index]);
WSACloseEvent(g_CliEventArr[index]);
HeapFree(GetProcessHeap(), 0, g_pPerIODataArr[index]);
if (index < g_iTotalConn - 1)
{
g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn - 1];
g_CliEventArr[index] = g_CliEventArr[g_iTotalConn - 1];
g_pPerIODataArr[index] = g_pPerIODataArr[g_iTotalConn - 1];
}
g_pPerIODataArr[--g_iTotalConn] = NULL;
}
1.5.2 使用完成例程方式实现
/* ! <
用完成例程方式实现的重叠I/O模型
用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中,主线程只用不停的接受连接即可;
辅助线程判断有没有新的客户端连接被建立,如果有,就为那个客户端套接字激活一个异步的WSARecv操作,
然后调用SleepEx使线程处于一种可警告的等待状态,以使得I/O完成后CompletionROUTINE可以被内核调用。
如果辅助线程不调用SleepEx,则内核在完成一次I/O操作后,无法调用完成例程(因为完成例程的运行应该
和当初激活WSARecv异步操作的代码在同一个线程之内)。完成例程内的实现代码比较简单,它取出接收到的数据,然后将数据原封不动的发送给客户端,最后重新
激活另一个WSARecv异步操作。
注意:在这里用到了“尾随数据”。我们在调用WSARecv的时候,参数lpOverlapped实际上指向一个比它大得多
的结构PER_IO_OPERATION_DATA,这个结构除了WSAOVERLAPPED以外,还被我们附加了缓冲区的结构信息,
另外还包括客户端套接字等重要的信息。这样,在完成例程中通过参数lpOverlapped拿到的不仅仅是
WSAOVERLAPPED结构,还有后边尾随的包含客户端套接字和接收数据缓冲区等重要信息。这在我后面介绍
完成端口的时候还会使用到。
*/
#include "stdafx.h"
#include "common.h"
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
SOCKET sClient;
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
DWORD WINAPI WorkerThread(LPVOID);
void CALLBACK CompletionROUTINE(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
SOCKET g_sNewClientConnection;
BOOL g_bNewConnectionArrived= FALSE;
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sListen;
SOCKADDR_IN local, client;
DWORD dwThreadId;
int iaddrSize = sizeof(SOCKADDR_IN);
// Initialize Windows Socket library
WSAStartup(0x0202, &wsaData);
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen, 3);
// Create worker thread
CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
while (TRUE)
{
// Accept a connection
g_sNewClientConnection =accept(sListen, (struct sockaddr *)&client, &iaddrSize);
g_bNewConnectionArrived= TRUE;
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
}
}
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
LPPER_IO_OPERATION_DATA lpPerIOData= NULL;
while (TRUE)
{
if (g_bNewConnectionArrived)
{
// Launch an asynchronous operation for new arrivedconnection
lpPerIOData= (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf =lpPerIOData->szMessage;
lpPerIOData->sClient =g_sNewClientConnection;
WSARecv(lpPerIOData->sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
CompletionROUTINE);
g_bNewConnectionArrived =FALSE;
}
SleepEx(1000, TRUE);
}
return 0;
}
void CALLBACK CompletionROUTINE(DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags)
{
LPPER_IO_OPERATION_DATA lpPerIOData= (LPPER_IO_OPERATION_DATA)lpOverlapped;
if (dwError != 0 || cbTransferred == 0)
{
// Connection was closed by client
closesocket(lpPerIOData->sClient);
HeapFree(GetProcessHeap(), 0, lpPerIOData);
}
else
{
lpPerIOData->szMessage[cbTransferred] = '\0';
send(lpPerIOData->sClient, lpPerIOData->szMessage, cbTransferred, 0);
// Launch another asynchronous operation
memset(&lpPerIOData->overlap, 0, sizeof(WSAOVERLAPPED));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
WSARecv(lpPerIOData->sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
CompletionROUTINE);
}
}
1.6 CompletionPort(完成端口模型)
/*!<
在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,就可考虑采用“完成端口”模型。
*/
#include "stdafx.h"
#include "common.h"
typedef enum
{
RECV_POSTED,
}OPERATION_TYPE;
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
OPERATION_TYPE OperationType;
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
DWORD WINAPI WorkerThread(LPVOID);
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
DWORD i, dwThreadId;
int iaddrSize= sizeof(SOCKADDR_IN);
HANDLE CompletionPort= INVALID_HANDLE_VALUE;
SYSTEM_INFO systeminfo;
LPPER_IO_OPERATION_DATA lpPerIOData= NULL;
// Initialize Windows Socket library
WSAStartup(0x0202, &wsaData);
// Create completion port
CompletionPort =CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// Create worker thread
GetSystemInfo(&systeminfo);
for (i = 0; i < systeminfo.dwNumberOfProcessors;i++)
{
CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &dwThreadId);
}
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen, 3);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Associate the newly arrived client socket withcompletion port
CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)sClient, 0);
// Launch an asynchronous operation for new arrivedconnection
lpPerIOData= (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len =MSGSIZE;
lpPerIOData->Buffer.buf =lpPerIOData->szMessage;
lpPerIOData->OperationType =RECV_POSTED;
WSARecv(sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
}
PostQueuedCompletionStatus(CompletionPort, 0xFFFFFFFF, 0, NULL);
CloseHandle(CompletionPort);
closesocket(sListen);
WSACleanup();
return 0;
}
DWORD WINAPI WorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort=(HANDLE)CompletionPortID;
DWORD dwBytesTransferred;
SOCKET sClient;
LPPER_IO_OPERATION_DATA lpPerIOData= NULL;
while (TRUE)
{
// 取得本次I/O的相关信息(例如套接字句柄、传送的字节数、单I/O数据结构的地址等等)
GetQueuedCompletionStatus(
CompletionPort,
&dwBytesTransferred,
(PULONG_PTR)&sClient,
(LPOVERLAPPED *)&lpPerIOData,
INFINITE);
if (dwBytesTransferred ==0xFFFFFFFF)
{
return 0;
}
if (lpPerIOData->OperationType == RECV_POSTED)
{
if (dwBytesTransferred ==0)
{
// Connection was closed by client
closesocket(sClient);
HeapFree(GetProcessHeap(), 0, lpPerIOData);
}
else
{
lpPerIOData->szMessage[dwBytesTransferred]= '\0';
send(sClient, lpPerIOData->szMessage, dwBytesTransferred,0);
// Launch another asynchronous operation for sClient
memset(lpPerIOData, 0, sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = RECV_POSTED;
WSARecv(sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
}
}
}
return 0;
}
1.7五种I/O模型的比较
*有无每线程64连接数限制
如果在选择模型中没有重新定义FD_SETSIZE宏,则每个fd_set默认可以装下64个SOCKET。同样的,受MAXIMUM_WAIT_OBJECTS宏的影响,事件选择、用事件通知实现的重叠I/O都有每线程最大64连接数限制。如果连接数成千上万,则必须对客户端套接字进行分组,这样,势必增加程序的复杂度。
相反,异步选择、用完成例程实现的重叠I/O和完成端口不受此限制。
*线程数
除了异步选择以外,其他模型至少需要2个线程。一个主线程和一个辅助线程。同样的,如果连接数大于64,则选择模型、事件选择和用事件通知实现的重叠I/O的线程数还要增加。
*实现的复杂度
我的个人看法是,在实现难度上,异步选择<选择<用完成例程实现的重叠I/O<事件选择<完成端口<用事件通知实现的重叠I/O
*性能
由于选择模型中每次都要重设读集,在select函数返回后还要针对所有套接字进行逐一测试,我的感觉是效率比较差;完成端口和用完成例程实现的重叠I/O基本上不涉及全局数据,效率应该是最高的,而且在多处理器情形下完成端口还要高一些;事件选择和用事件通知实现的重叠I/O在实现机制上都是采用WSAWaitForMultipleEvents,感觉效率差不多;至于异步选择,不好比较。所以我的结论是:选择<用事件通知实现的重叠I/O<事件选择<用完成例程实现的重叠I/O<完成端口