windows 常见模型
select模型
是什么?
对多个socket 进行管理 调用select()可以获取指定socket状态,即select 选择获得有响应的指定的socket
为什么?
解决基本C/S模型中,accept()、recv()、send()阻塞的问题
select模型与C/S模型的不同点
C/S模型中accept()会阻塞一直傻等socket来链接
select模型只解决accept()傻等的问题,不解决recv(),send()执行阻塞问题
怎么用?
服务端
1.一般创建非阻塞步骤(初始化,创建,绑定ip和端口,监听,ioctlsocket设置非阻塞)
2.装填socket数组 FD_SET(socketServer, &allSockets);
3.调用select() 对有响应的socket 做相应处理(accept,send,recv)
具体参照
《windows网络编程》第八章 8.3 基于select模型的socket编程
WSAAsyncSelect模型
是什么?
应用程序可以在一个socket上接收以windows消息为基础的网络事件通知,它实现了读写数据的异步通知功能,但不提供异步的数据传输。
为什么?
WSAAsyncSelect是select模型的异步版本。在应用程序使用select函数时会发生阻塞现象。可以通过select的timeout参数设置阻塞的时间。在设置的时间内,select函数等待,直到一个或多个套接字满足可读或可写的条件。
而WSAAsyncSelect是非阻塞的。Windows sockets程序在调用recv或send之前,调用WSAAsyncSelect注册网络事件。WSAAsyncSelect函数立即返回。当系统中数据准备好时,会向应用程序发送消息。此此消息的处理函数中可以调用recv或send进行接收或发送数据。
WSAAsyncSelect模型与select模型的相同点是它们都可以对多个套接字进行管理。但它们也有不小的区别。首先WSAAsyncSelect模型是异步的,且通知方式不同。更重要的一点是:WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口,而select模型可以广泛应用在Unix系统,使用该模型不需要创建窗口。最后一点区别:应用程序在调用WSAAsyncSelect函数后,套接字就被设置为非阻塞状态。而使用select函数不改变套接字的工作方式。
怎么用?
1.初始化socket环境,并创建win32自定义事件的socket事件 WM_SOCKET
2.调用WSAAsyncSelect()绑定事件到窗口消息队列
3.通过GetMessage()轮询消息,当消息事件发生时,实现对socket的处理(包括accept,send,recv)
具体参照
《windows网络编程》第八章 8.5 基于WSAAsyncSelect模型的服务器编程
WSAEventSelect模型
是什么?
允许在多个Socket上接收以事件为基础的网络事件通知,应用程序在创建Socket后,调用WSAEventSelect()函数将事件对象与网络事件集合相关联。当网络事件发生时,应用程序以事件的形式接收网络事件通知。
为什么?
WSAEventSelect与WSAAsyncSelect在网络事件发生时系统通知应用程序的形式不同。
Select 主动获取指定socket状态,
WSAEventSelect,WSAAsyncSelect 则会被动选择系统通知应用程序socket状态变化。
WSAEventSelect模型和WSAAsyncSelec模型类似,都是用调用WSAXXXXXSelec函数将socket和事件关联并注册到系统,并将socket设置成非阻塞模式。二者不同之处在于socket事件的通知方法:WSAAsyncSelec模型利用窗口句柄和消息映射函数通知网络事件,而WSAEventSelect模型利用WSAEVENT通知网络事件。
怎么用?
服务端:
1.初始化socket环境,并创建用于监听的socket
2.创建事件对象 WSACreateEvent()
3.将新建的事件对象与监听Socket相关联,并注册该Socket关注的网络事件集合,通常为FD_ACCEPT 和 FD_CLOSE
4.等待所有事件对象上发生注册的网络事件,并对网络事件进行处理。
WSWaitForMultipleEvents()
WSAEnumNetWorkEvents();
5.如果触发了FD_ACCEPT事件,则程序接收来自客户端的请求,得到与客户端通信的Socket,并为该Socket创建相关联的事件
对象,注册该Socket关注网络事件集合,通常为FD_READ,FD_CLOSE和FD_WRITE
6.如果触发了FD_CLOSE事件,则关闭Socket,释放其占用资源
7.如果触发了FD_READ事件,则调用recv函数接收来则客户端的请求
8.如果触发了FD_WRITE事件,则调用send()函数向客户端发送数据
具体参照
《windows网络编程》第八章 8.5 基于WSAEventSelect模型的服务器编程
重叠I/O模型
是什么?
重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。
重叠i/o是真正意义上的异步io模型 调用输入/输出函数后 立即返回(WSARecv,WSASend)。
为什么?
1.可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。
2.比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。
因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。不需要等待调用recv时才拷贝
而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。
3. 从《windows网络编程》中提供的试验结果中可以看到,在使用了P4 1.7G Xero处理器(CPU很强啊)以及768MB的回应服务器中,最大可以处理4万多个SOCKET连接,在处理1万2千个连接的时候CPU占用率才40% 左右 ―― 非常好的性能,已经直逼完成端口。
怎么用?
使用事件通知来管理重叠i/o操作
通过事件来通知应用程序I/O 操作已完成
在WSASend()或者WSARecv函数中,当重叠操作完成后,如果lpCompletionRoutine参数为NULL,则lpOverlapped的hEvent参数将被设为已授信状态。应用程序调用WSAWaitForMultipleEvents()函数或者WSAGetOverlappedResult函数等待或者轮询事件对象变为未授信状态。
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) //lpCompletionRoutine参数为NULL,使用事件通知
// OverlappedServer.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <stdlib.h> #include "winsock2.h" #pragma comment(lib,"ws2_32.lib") #define DATA_BUFSIZE 4096 int _tmain(int argc, _TCHAR* argv[]) { //----------------------------------------- // 声明和初始化变量 WSABUF DataBuf; // 发送和接收数据的缓冲区结构体 char buffer[DATA_BUFSIZE]; // 缓冲区结构体DataBuf中 DWORD EventTotal = 0, // 记录事件对象数组中的数据 RecvBytes = 0, // 接收的字节数 Flags = 0, // 标识位 BytesTransferred = 0; // 在读、写操作中实际传输的字节数 // 数组对象数组 WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; WSAOVERLAPPED AcceptOverlapped; // 重叠结构体 SOCKET ListenSocket, AcceptSocket; // 监听套接字和与客户端进行通信的套接字 //----------------------------------------- // 初始化Windows Sockets WSADATA wsaData; WSAStartup(MAKEWORD(2,2), &wsaData); //----------------------------------------- // 创建监听套接字,并将其绑定到本地IP地址和9990端口 ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); u_short port = 9990; char* ip; sockaddr_in service; service.sin_family = AF_INET; service.sin_port = htons(port); hostent* thisHost; thisHost = gethostbyname(""); ip = inet_ntoa (*(struct in_addr *)*thisHost->h_addr_list); service.sin_addr.s_addr = inet_addr(ip); bind(ListenSocket, (SOCKADDR *) &service, sizeof(SOCKADDR)); //----------------------------------------- // 开始监听 listen(ListenSocket, 1); printf("Listening...\n"); //----------------------------------------- // 接收连接请求 AcceptSocket = accept(ListenSocket, NULL, NULL); printf("Client Accepted...\n"); //----------------------------------------- // 创建事件对象,建立重叠结构 EventArray[EventTotal] = WSACreateEvent(); ZeroMemory(buffer, DATA_BUFSIZE); ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 初始化重叠结构 AcceptOverlapped.hEvent = EventArray[EventTotal]; // 设置重叠结构中的hEvent字段 DataBuf.len = DATA_BUFSIZE; // 设置缓冲区 DataBuf.buf = buffer; EventTotal++; // 事件对象总数加1 //----------------------------------------- // 处理在套接字上接收到数据 while (1) { DWORD Index; // 保存处于授信状态的事件对象句柄 //----------------------------------------- // 调用WSARecv()函数在AcceptSocket套接字上以重叠I/O方式接收数据,保存到DataBuf缓冲区中 if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) { if (WSAGetLastError() != WSA_IO_PENDING) printf("Error occured at WSARecv()\n"); } //----------------------------------------- // 等待完成的重叠I/O调用 Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE); //----------------------------------------- // 决定重叠事件的状态 WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped, &BytesTransferred, FALSE, &Flags); //----------------------------------------- // 如果连接已经关闭,则关闭AcceptSocket套接字 if (BytesTransferred == 0) { printf("Closing Socket %d\n", AcceptSocket); closesocket(AcceptSocket); WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); return -1; } //----------------------------------------- // 如果有数据到达,则将收到的数据则发送回客户端 if (WSASend(AcceptSocket, &DataBuf, 1, &RecvBytes, Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR) printf("WSASend() is busted\n"); //----------------------------------------- // 重置已授信的事件对象 WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]); //----------------------------------------- // 重置Flags变量和重叠结构 Flags = 0; ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); ZeroMemory(buffer, DATA_BUFSIZE); AcceptOverlapped.hEvent = EventArray[Index - WSA_WAIT_EVENT_0]; //----------------------------------------- // 重置缓冲区 DataBuf.len = DATA_BUFSIZE; DataBuf.buf = buffer; } system("pause"); return 0; }
使用完成例程来管理重叠i/o操作
完成例程则指定应用程序在完成i/o操作后调用一个事先定义的回调函数。
在WSASend()或者WSARecv函数中,如果lpCompletionRoutine参数不为NULL,则hEvent参数将被忽略,而是将上下文信息传送给完成例程函数,调用WSAGet-OverlapperResult函数查询重叠操作的结果。
完成例程函数原型如下:
void CALLBACK CompletionROUTINE( DWORD dwError, // 重叠操作的完成状态
DWORD cbTransferred, // 发送的字节数
LPWSAOVERLAPPED lpOverlapped, // 指定重叠操作的结构体
DWORD dwFlags) // 标识位
服务端
1.初始化windows socket环境
2.创建监听socket sListen 并将其绑定到本地地址,端口
3.在socket sListen上进行监听
4.创建工作线程,对客户端发送来的数据进行处理
5.循环处理来自客户端的连接请求,接收连接,并将得到的与客户端的socket保存到g_sNewClientConnectio中,将变量g_bNewConnectionArried设置为TRUE,表示存在新的客户端连接。
// CompletionRoutineServer.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <winsock2.h> #include <stdio.h> #define PORT 9990 // 监听的端口 #define MSGSIZE 1024 // 发送和接收消息的最大长度 # pragma comment( lib, "ws2_32.lib" ) // I/O操作的数据 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); // 在工作线程中调用WSARecv()函数接收数据时指定的完成例程函数 void CALLBACK CompletionROUTINE( DWORD, DWORD, LPWSAOVERLAPPED, DWORD); SOCKET g_sNewClientConnection; // 接收客户端连接请求后得到的 BOOL g_bNewConnectionArrived = FALSE ; // 标识是否存在未经WorkerThread()函数处理的新的连接 int _tmain(int argc, _TCHAR* argv[]) { WSADATA wsaData; // Windows Sockets对象 SOCKET sListen; // 与客户端进行通信的套接字 SOCKADDR_IN local, client; // 服务器本地地址和客户端地址 DWORD dwThreadId; // 工作线程的线程ID int iaddrSize = sizeof(SOCKADDR_IN) ; // 地址的大小 // 初始化Windows Socket环境 WSAStartup( 0x0202, & wsaData) ; // 创建监听套接字 sListen = socket ( AF_INET , SOCK_STREAM , IPPROTO_TCP ) ; // 绑定 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(sListen, 3) ; // 创建工作线程 CreateThread( NULL , 0, WorkerThread, NULL , 0, & dwThreadId) ; // 循环处理来自客户端的连接请求 while(TRUE) { // 接收连接,得到与客户端进行通信的套接字g_sNewClientConnection 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; // 保存I/O操作的数据 while (TRUE) { if ( g_bNewConnectionArrived) // 如果有新的连接请求 { // 为新的连接执行一个异步操作 // 为LPPER_IO_OPERATION_DATA结构体分配堆空间 lpPerIOData = (LPPER_IO_OPERATION_DATA) HeapAlloc( GetProcessHeap( ), HEAP_ZERO_MEMORY, sizeof (PER_IO_OPERATION_DATA)) ; // 初始化结构体lpPerIOData 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) ; // 休息1秒钟,然后继续 } return 0; } // 完成例程函数 void CALLBACK CompletionROUTINE( DWORD dwError, // 重叠操作的完成状态 DWORD cbTransferred, // 发送的字节数 LPWSAOVERLAPPED lpOverlapped, // 指定重叠操作的结构体 DWORD dwFlags) // 标识位 { // 将LPWSAOVERLAPPED类型的lpOverlapped转化成了LPPER_IO_OPERATION_DATA LPPER_IO_OPERATION_DATA lpPerIOData = ( LPPER_IO_OPERATION_DATA) lpOverlapped; // 如果发生错误或者没有数据传输,则关闭套接字,释放资源 if (dwError != 0 || cbTransferred == 0) { closesocket( lpPerIOData-> sClient) ; HeapFree( GetProcessHeap(), 0, lpPerIOData) ; } else { lpPerIOData->szMessage[cbTransferred] = '\0'; // 标识接收数据的结束 // 向客户端发送接收到的数据 send(lpPerIOData->sClient, lpPerIOData->szMessage, cbTransferred, 0) ; // 执行另一个异步操作,接收数据 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); } }
具体参照
《windows网络编程》第八章 8.6 基于重叠io模型的服务器编程