WSAAsyncSelect模型.ppt
WSASsyncSelect模型,前面学习的Windows Sockets select模型。在应用程序中调用select()函数时,会发生阻塞现象。开发人员可以通过select()函数的timeout参数,设置函数调用的阻塞时间,在设定时间内,线程保持等待,直到其中的一个或者多个套接字满足可读或者可写的条件,该函数才返回。,WSAAsyncSelect模型是非阻塞的。如图所示,Windows Sockets 应用程序在调用recv()函数接收数据前,调用WSAAsyncselect()函数注册网络事件。WSAAsyncselect()函数立即返回,线程继续运行。当系统中数据准备好时,向应用程序发送消息。应用程序接收到这个消息后,调用recv()函数接收数据。,与select模型比较,WSAAsyncSelect模型与Select模型的相同点:都可以对Windows套接字应用程序所使用的多个套接字进行有效的管理。WSAAsyncSeelect模型与Select模型相比存在以下不同:WSAAsyncSelet模型是异步的。在应用程序中调用WSAAsyncSelect()函数,通知系统感兴趣的网络事件,该函数立即返回,应用程序继续运行。,发生网络事件时,应用程序得到通知的方式不同。Select()函数返回时,说明某个或者某些套接字满足可读可写的条件,应用程序需要使用FD_ISSET宏,判断套接字是否存在于可读可写集合中。而对于WSAAsyncSelect模型来说,当网络事件发生时,系统向应用程序发送消息。,WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口。而Slelect模型广泛应用在Unix系统和Windows系统,使用模型不需要创建窗口。应用程序中调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式。而应用程序中调用select()函数后,并不能改变该套接字的工作方式。,套接字WSAAsyncSelect模型实现,WSAAsyncSelect模型核心是WSAAsyncSelect()函数,该函数使得Windows应用程序能够接收网络事件消息。在应用程序窗口例程中对接收到的网络事件进行处理。由于WSAAsyncSelect模型应用在基于消息的Windows应用程序中,所以本节还将讲解窗口例程和如何创建窗口等内容。,WSAAsynSelect()函数,int WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent),s:需要事件通知的套接字。hWnd:当网络事件发生时接收消息的窗口句柄。wWsg:当网络事件发生时窗口收到的消息。lEvent:应用程序感兴趣的网络事件集合。应当用程序中调用该函数后,自动将套接字设置为非阻塞模式。通常,应用程序声明的消息要比Windows的WM_USER值大,以避免该消息与Windows预定消息发生混淆。,注册哪种网络事件,取决于实际的需要。如果应用程序同时对多个网络事件感兴趣。需要对网络事件类型执行按位OR(或)运算。然后将它们分配给lEvent参数。例如,应用程序希望在套接字上接收有关连接完成、数据可读和套接字关闭的网络事件。那么在应用程序中,调用WSAAsyncSelect()函数如下所示:WSAAsyncSelect(s,hWnd,WM_SOCKET,FD_CONNECT|FD_READ|FD_CLOSE);,当该套接字连接完成、有数据可读或者套接字关闭的网络事件事件发生时,就会有WM_SOCKET消息发送给窗口句柄为hWnd窗口。,窗口例程,当调用WSAAsyncselect()函数后,应用程序会在hWnd窗口例程,以消息形式接收网络事件通知。窗口例程是回调函数,当成功创建窗口后由系统调用。窗口例程声明如下:LRESULT CALLBACK WindowProc(HWND hWnd,/窗口句柄UINT uMsg,/消息WPARAM wParam,/消息参数 LPARAM lParam,/消息参数);,hWnd:窗口句柄。uMsg消息。对Windows Sockets应用程序来说感兴趣的是在 WSAAsyncSelect()函数中,由应用程序定义的消息。wParam:消息参数。在 Windows Sockets应用程序中,该参数指明发生网络事件的套接字。lParam:消息参数。在 Windows sockets 应用程序中,该参数低字节指明已经发生的网络事件。高字节包含可能出现的错误代码。,在Windows sockets 应用程序中,当WindowProc()函数接收到网络事件消息时,在该函数内执行下面步骤:(1)读取lParam参数高字节,判断是否发生了一个网络错误事件。可以使用 WSAGETSELECTERROR宏。(2)如果应用程序发现套接字上没有发生任何错误,则读取Iparam低字节,检查到底是发生了什么网络事件。可以使用WSAGETSELECTEVENT宏。,WSAGETSELECTERROR和WSAGETSELECTEVENT宏声明如下:#define WSAGETSELECTEVENT(lParam)LOWORD(lParam)#define WSAGETSELECTERROR(lParam)HIWORD(lParam),WSAAsyncSelect模型示例程序,下面讲解一个服务器程序。该程序是Win32 Application。在该程序中使用WSAAsyncSelect模型管理接受的客户端套接字。该程序是示例程序,忽略主许多细节。程序设计如图所示,按照下面步骤编码。,1.声明自定义消息。在程序中声明自定义消息WM_SOCKET。2.声明窗口例程。3.调用MyRegisterClass()函数注册窗口类。4.调用InitInstance()函数创建并显示窗口。因为WSAAsyncSelect()函数的第一个参数是窗口句柄,所以要在调用该函数之前创建窗口。5.初始化套接字动态库,创建套接字。6.调用WSAAsyncSelect()函数注册感兴趣网络事件。该示例程序中,服务器监听套接字,感兴趣的网络事件有FD_ACCEPT和FD_CLOSE.7.绑定套接字,开始监听。8.消息循环。9.释放套接字和申请的其他资源。,1.声明自定义消息,在应用程序中,通常要声明一个比WM_USER值要大的自定义消息,以免与Windows定义的消息冲突。除了声明自定义消息外,在示例程序中还要声明最大字符串长度、服务器端口、数据缓冲区长度。#define MAX_LOADSTRING100/最大字符串长度#define WM_SOCKETWM_USER+1/套接字消息#define PORT5150/服务器端口#define MAX_SIZE_BUF 1024/数据缓冲区长度,2.声明窗口例程,窗口例程是由Windows系统调用的函数,通常将该函数的定义放在主函数之后,将声明放在主函数之前。在示例程序中为了使主程序结构清晰,将注册窗口类、创建和显示窗口的过程都设计为函数,并提前声明。声明HandleSocketMsg()函数用于对Windows网络事件消息进行处理。ATOM MyRegisterClass(HINSTANCE hInstance);/注册窗口BOOL InitInstance(HINSTANCE,int);/初始化实例/窗口例程LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);/处理WM_SOCKET消息void HandleSocketMsg(WPARM wParam,LPARAM lParam);,3.注册窗口类,调用MyRegisterClass()函数注册窗口类。,4.创建和显示窗口,调用InitInstance()函数创建、显示窗口。此时,窗口例程开始接收Windows消息。,5.创建套接字,调用WSAStartup()函数初始化套接字动态库,调用socket()函数创建套接字。,6.注册感兴趣的网络事件,以窗口名柄hWnd和WM_SOCKET为第2、第3个参数调用WSAAsyncSelect()函数。同时注册FD_ACCRPT和FD_CLOSE网络事件。请求系统当FD_ACCEPT和FD_CLOSE网络事件发生时,给hWnd窗口发送WM_SOCKET消息。,WSAAsyncSelect(sListen,hWnd,WM_SOCKET,FD_READ|FD_CLOSE);,7.绑定套接字,调用bind()函数绑定套接字。,8.开始监听,调用listen()函数套接字开始监听。,9.消息循环,在while循环语句中,GetMessage()函数不断从线程消息队列中取出消息。当FD_ACCEPT或者FD_CLOSE网络事件发生时,WM_SOCKET消息被投递到线程消息队列中,GetMessage()函数负责将该消息从线程消息队列中取出,DispatchMessage()函数再将消息发送到窗口例程。,10.程序退出,当GetMessage()函数接收到WM_QUIT消息时,while循环结束,释放资源,程序退出。,11.窗口例程,当创建窗口成功后WndProc()窗口例程便开始接收Windows消息。在该函数中需要处理许多消息。例如,当关闭窗口时发送WM_DESTROY消息,在窗口例程中调用PostQuitMessage()函数向线程消息队列投递WM_QUITI消息,GetMessage()函数接收到该消息后,程序退出。,应用程序不感兴趣的消息交给DefWindowProc()函数处理。当FD_ACCEPT或者FD_CLOSE网络事件发生时,窗口例程接收到WM_SOCKET消息。在窗口例程中调用HandleSocketMsg()函数对触发WM_SOCKET消息的网络事件进行处理。,/窗口例程LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)switch(message)case WM_SOCKET:/网络事件发生时发送给该窗口的消息 HandleSocketMsg(wParam,lParam);/处理该消息 break;case WM_PAINT:/画客户区/break;case WM_DESTROY:PostQuitMessage(0);break;/消息处理default:return DefWindowProc(hWnd,message,wParam,lParam);return 0;,12.CClient类,在程序中声明Cclient类管理服务器接受客户端的新建套接字。该类构造函数的参数为服务器接受客户端的新建套接字。在析构函数中将套接字关闭。在该类中声明RecvData()函数接收数据,SendData()函数发送数据,GetSocket()函数返回套接字。,#define MAX_SIZE_BUF1024/数据缓冲区长度class CClient public:CClient(SOCKET s);/构造函数 virtual CClient();/析构函数 public:void RecvData(void);/接收数据 void SendData(void);/发送数据 SOCKET GetSocket(void);/获取套接字private:SOCKET m_s;/套接字 char m_recvBufMAX_SIZE_BUF;/接收数据缓冲区 char m_sendBufMAX_SIZE_BUF;/发送数据缓冲区;,13.管理客户端套接字的链表,声明_socktnode结构体。该结构体pClient字段为CClient类指针。pNext变量为指向下一个节点指针。typedef struct _socktnodeCClient*pClient;/CClient类指针_socktnode*pNext;/指向下一个节点SOCKETNODE,*PSOCKETNODE;,当服务器接受一个客户端连接请求后,创建一个CClient实例,新建一个SOCKETNODE节点。将实例指针赋值给SOCKETNODE结构体的pClient变量。为了对链表进行操作,声明如下函数。AddNode()函数,添加节点。DeleteNode()函数:删除节点。GetClient()函数:获得Cclient类指针。DeleteAllNode()删除所有节点。,14.网络事件消息处理函数,在HandleSocketMsg()函数中调用 WSAGETSELECTERROR宏检查是否有网络错误事件发生。如果有网络错误事件发生则调用 DeleteNode()函数将该套接字从链表中删除。在前面的讲解中,已经知道 wParam 参数为发生网络事件的套接字。所以,以wParam为参数调用 DeleteNode函数。如果没有网络错误事件发生,则调用WSAGETSELECTEVENT宏,检查发生了什么网络事件。,如果网络事件为FD_ACCEPT,那么说明此时客户端等待服务器接受连接请求。发生这个网络事件的套接字一定是服务器监听套接字。调用accept()函数接受客户端连接请求,将该套接字加入链表中,然后以该新建套接字作为参数调用WSAAsyncSelect()函数,为该套接字请求FD_READ、FD_WRITE和FD_CLOSE网络事件。,当HandleSocketMsg()函数接收到FD_READ网络事件时,说明此时在服务器接受的客户端套接字中,某个套接字上存在可读的数据。这个套接字就是wParam参数值。调用GetClient()函数得到保存该套接字的CClient类指针。调用该类的ecvData()函数接收客户端数据。HandleSocketMsg()函数接收到FD_WRITE网络事件时的处理方法,同收到FD_READ网络事件时的处理方法相似。当该函数接收到FD_CLOSE网络事件时,说明此时客户端关闭了套接字,可以调用DeleteNode()函数删除该客户端节点。,/WM_SOCKET消息处理void HandleSocketMsg(WPARAM wParam,LPARAM lParam)if(WASGETSELECTERROR(lParam)/检查网络错误 DeleteNode(wParam);/删除节点,关闭套接字 else/检查网络事件 switch(WASGETSELECTEVENT(lParam)case FD_ACCEPT:/接受客户端连接请求,SOCKET sAccept;if(sAccept=accept(wParam,NULL,NULL)=INVALID_SOCKET)break;AddNode(sAccept);/将套接字加入链表中/FD_READ、FD_WRITE、FD_CLOSE事件发生/时,发送WM_SOCKET消息 WSAAsyncSelect(sAccept,hWnd,WM_SOCKET,FD_READ|FD_WRITE|FD_CLOSE);break;,case FD_READ:/接收数据 CClient*pClient=GetClient(wParam);/根据套接字,/获取客户端节点pClient-RecvData();/接收数据break;case FD_WRITE:CClient*pClient=GetClient(wParam);/根据套接字,/获取客户端节点pClient-SendData();/发送数据break;,case FD_CLOSE:/对方关闭了套接字连接 DeleteNode(wParam);/删除节点,break;return;,调用WSAAsyncSelect()函数注意问题,接收不到网络事件 第一种情况是由于在同一个套接字同一个自定义消息上,多次调用WSAAsyncSelect()函数注册不同的网络事件,最后一次函数调用取消了前面注册的网络事件。例如,在应用程序中,第一次调用WSAAsyncSelect()函数注册FD_READ网络事件,然后又调用该函数注册F_WRITE网络事件,那么此时应用程序,就只能接收到FD_WRITE网络事件。,如果要取消所有请求的网络事件通知,告知indows Sockets实现不再为该套接字发送任何网络事件相关的消息,要以参数IEvent值为0调用WSAAsyncSelect()函数。WSAAsyncSelect(s,hWnd,0,0);需要注意尽管应用程序调用上述函数取消了网络事件通知,但是在应用程序消息队列中,可能还有网络消息在排队。所以在调用WSAAsyncSelect()函数取消网络事件消息后,应用程序还应该继续准备接收网络事件。,第二种情况是在同一个套接字上,多次调用WSAAsyncSelect()函数,为不同的网络事件定义了不同的消息,最后一次该函数调用将取消前面注册的网络事件。下面的代码中,第二次函数调用将会取消第一次函数调用的作用。只有FD_WRITE网络事件能过wMsg2消息通知到窗口。WSAAsyncSelect(s,hWnd,wMsg1,FD_READ);WSAAsyncSelect(s,hWnd,wMsg2,FD_WRITE);,关于accept()函数,因为调用accept()函数接受的套接字和监听套接字具有同样的属性。所以,任何为监听套接字设置的网络事件对接受的套接字同样起作用。如果一个监听套接字请求FD_ACCEPT、FD_READ和FD_WRITE网络事件,则在该监听套接字上接受的任何套接字也会请求FD_ACCEPT、FD_READ和FD_WRITE网络事件,以及发送同样的消息。,若需要不同的消息和网络事件,应用程序应该调用WSAAsyncSelect()函数,为该套接字请求不同的网络事件和消息。,关于FD_READ网络事件,为一个FD_READ网络事件不要多次调用recv()函数。如果应用程序为一个FD_READ网络事件,调用了多个recv()函数,会使得该应用程序接收到多个_READ网络事件。如果在一次接收FD_READ网络事件时需要调用多次recv()函数,应用程序应该在调用recv()函数之前关闭FD_READ 消息。应用程序不必在收到FD_READ消息时,读进所有可读的数据。每接收到一次FD_READ网络事件,应用程序调用一次recv()函数是恰当的。,如何判断套接字已经关闭,要使用FD_CLOSE网络事件来判断套接字是否已经关闭。接收FD_CLOSE网络事件时,错误代码指示出套接字是从容关闭还是硬关闭。如果错误代码0,则为从容关闭;若错误代码为WSAECONNRESET,则套接字是硬关闭。如果套接字从容关闭,数据已经都全部接收,应用程序只会收到FD_CLOSE消息来指出虚电路关闭,它不会收到FD_READ消息来表明这种状况。调用closesocket()函数后不会投递FD_CLOSE网络事件。,发送数据失败,一个应用程序当接收到第一个FD_WRITE网络事件后,便认为在该套接字上可以发送数据。当调用输出函数发送数据时,会收到WSAEWOULDBLOCKE错误。经过这样的失败后,要在下一次接收到FD_WRITE网络事件后,再次发送数据,才能够将数据发送出去。,发生网络事件的条件,调用WSAAsyncSelect()函数只是请求系统在网络事件发生时,给窗口发送消息.开发人员需要了解在什么情况下会发生网络事件下。下面对发生网络事件条件进行小结。,FD_READ网络事件,在下面情况下,发生FD_READ网络事件。当调用WSAAsyncSelect()函数时,如果当前有可读数据时。当数据到达并且没有发送FD_READ网络事件时。调用recv()或者recvfrom()函数后,如果仍然有可读数据时。,FD_WRITE事件,在下面情况下,发生FD_WRITE网络事件。当调用WSAAsyncSelect()函数时,如果调用能够发送数据时。调用connect()或者accept()函数后,当连接已经建立时。调用send()或者sendto()函数,返回WSAEWOULDBLOCKE错误后,再次调用send()或者sendto()函数可能成功时。,FD_ACCEPT事件,在下面情况下,发生FD_ACCEPT网络事件。当调用WSAAsyncSelect()函数时,如果当前有连接请求需要接受时。当连接请求到达,还没有发送FD_ACCEPT网络事件时。调用accept()函数后,如果还有另外连接请求需要接受时。,FD_CONNECT事件,在下面情况下,发生FD_CONNECT网络事件。当调用WSAAsyncSelect()函数,如果当前一个连接已经建立时。当调用connect()函数后,建立连接完成时。当调用WSAJoinLeaf()函数后,加入操作完成时。在面向连接的非阻塞套接字上,调用connect()、WSAConnect()或者WSAJoinLeaf()函数后,尝试连接完成时。此时应用程序应该检查错误代码,确定连接是否成功。,FD_CLOSE事件,FD_CLOSE事件仅对面向连接套接字有效,在下面情况下发送FD_CLOSE事件。当调用WSAAsyncSelect()函数时,套接字连接关闭时。对方执行了从容关闭后,没有数据可读时。如果数据已经到达并等待读取,FD_CLOSE事件不会被发送,直到所有的数据都被接收。调用shutdown()函数执行从容关闭,对方应答FIN后,此时如果没有数据可读时。当对方结束了连接,并且IParam包含WSAECONNRESET错误时。,WSAAsyncSelect模型优势和不足,1.优势该模型的使用方便了基于消息的Windows环境下开发套接字应用程序。开发人员可以像处理其他消息一样对网络事件消息进行处理。该模型为确保接收所有数据提供很好的机制。通过注册FD_CLOSE网络事件,从容关闭服务器与客户端的连接保证了数据全部接收。,2.不足该模型局限在于,它基于Windows的消息机制,必须在应用程序中创建窗口。当然,在开发中可以根据具体情况确定是否显示该窗口,MFC的CSockeWnd类就是用来创建一个不显示的窗口,并在该类中声明接收网络事件消息处理数。,由于调用WSAAsnycSelect()函数后,自动将套接字设置为非阻塞状态。当应用程序为接收到网络事件调用相应的函数时,未必能够成功返回。这无疑增加了开发人员使用该模型的难度。对于这一点可以从MFC CSocket类的Accpet()、Receive()和Send()函数的实现中得到验证。,局域网简易聊天程序,需求分析该聊天程序分为服务器和客户端两个部分。客户端实现以下功能。向在线成员发送消息并接收消息。更新成员状态。当其他成员上线或者下线时,客户端及时更新该成员状态。更新成员列表。当新成员加入或者成员被删除时,客户端及时更新成员列表。当对方不在线时,可以向该成员发送离线消息。当用户上线时,接收其他用户发送的离线消息保存聊天记录。,服务器实现以下功能维护聊天室成员列表。包括接受新成员,删除已有成员。通知客户端更新成员状态,当用户上线或者下线时,通知客户端更新成员状态。通知客户端更新成员列表。当有新用户加入或者成员被删除时,通知客户端列新成员列表为离线客户端保存聊天消息。验证密码。当用户登录时,验证密码。禁止已经登录用户,再次登录。保存用户信息。为用户保存离线消息。,客户端启动时登录服务器,如果用户为非注册用户则服务器为该用户注册信息。注册信息包括该用户的名称和密码等。以后该用户需要使用注册名称密码登录服务器。服务器以用户名称作为每个用户的唯一标识。用户注册后,服务器向该用户发送用户列表。注册用户登录服务器时,如果输入密码正确,则服务器请向该用户发送用户列表。如果服务器验证该用户输入密码错误,则通知该用户重新登录。,当某个成员上线或者下线时,服务器通知在线成员更新该用户状态。当某个成员希望与另外一成员交流时,如果另外那个成员在线则可以其送消息并接收应答。当某个成员希望与另外一成员交流时,如果另外那个成员不在线则可以向发送离线消息。当对方上线时将自动接收到离线消息。,