项目3.3中实现的通信程序仍然有很多不足,最明显的不足是,通信双方不能自动接收对方消息,需要单击“接收”按钮才能接收。
另外的问题是,在服务器端单击“创建服务器”按钮后,如果没有客户端连接,则该程序处于一种“失去响应”的状态,点击任何按钮都没反应,因为这时服务器端一直在等待连接。
当服务器与客户端连接成功后,若其中一方还没有发送消息,用户就去单击另一方的“接收”按钮,则程序也将进入“失去响应”状态,因为这时程序一直在等待接收消息。
上述问题都是因为WinSock程序默认处于“阻塞”状态造成的,要解决这些问题,必须先理解阻塞与非阻塞模式,同步与异步这些概念。
阻塞是指一个线程在调用一个函数时,该函数由于某种原因不能立即完成,导致线程处于等待的状态。C语言中许多I/O函数都能引起阻塞,例如,scanf()、getc()、gets()等。
在WinSock编程中,许多套接字函数也会引起阻塞,比如程序调用accept()函数时,如果没有客户端连接,则accept()函数会处于阻塞等待。此时,程序将停止执行,一直等待accept()函数执行成功后返回,才会继续执行下面的代码,因此,程序这段时间会处于“假死”的状态。
在WinSock中,能引起阻塞的套接字函数有以下这些:
1)accept() :如果套接字的缓冲队列中没有已到达的连接请求,则阻塞,当有连接请求到达时则恢复。
2)connect():连接请求发送出去后便阻塞,直到TCP/IP的三次握手过程成功时结束,返回对客户端连接请求的确认。
3)recv()、recvfrom():套接字的接收缓冲区没有数据可读,则阻塞,直到有数据可读。
4)send()、sendto():如果套接字缓冲区中仍有以前的数据未发送出去,并且该发送缓冲区的空间不能容纳要发送的数据,则阻塞,直到套接字发送缓冲区有足够的空间。
提示:listen()函数是不会引起阻塞的。
在默认情况下,套接字是工作在阻塞模式下的,即在调用引起阻塞的套接字函数时,如果要求的操作不能立刻完成,函数将阻塞等待,直到操作完成才会返回。如果不想让这种情况发生,可将套接字设置为非阻塞模式。
在非阻塞模式下,不管套接字函数执行是否成功,都将立即返回,程序继续执行。可见,非阻塞模式能克服阻塞模式的缺点,当一个I/O操作不能及时完成时,应用程序不再阻塞,而是继续做其他事情。另外,在有多个套接字的情况下,就可以通过循环来轮询各个套接字的I/O操作,从而提高工作效率。
WinSock提供了5种套接字的I/O模型:select模型、WSAAsyncSelect模型(异步选择模型)、WSAEventSelect模型(事件选择模型)、重叠I/O(Overlapped I/O)模型和完成端口(Completion port)模型,都可以将套接字设置为非阻塞模式。
尽管非阻塞模式不需要等待套接字函数执行成功后再返回,而是立即返回,但它不知道套接字函数是否执行成功了,于是它不得不每隔一段时间,去询问一下(如检测函数的返回值,以判断是否执行成功),显然这会带来资源的不必要消耗。
为此,WinSock 2引入了异步I/O模型,在这种模型中,当套接字函数执行完毕时,它将主动“通知”应用程序,应用程序只有在收到事件通知时才调用相应的套接字函数进行I/O操作。
下面举个例子来说明阻塞和非阻塞、同步和异步的区别。
老张要用水壶烧开水,则他有以下4种做法:
① 老张把水壶放到火上,然后站在旁边等待,期间他不做任何事,一直等到水开(同步阻塞);
② 老张把水壶放到火上,然后去客厅看电视,再时不时去厨房看看水开了没有(同步非阻塞);
③ 老张把一个会鸣笛的水壶放到火上,然后站在旁边等待,一直等到鸣笛水开(异步阻塞);
④ 老张把会鸣笛的水壶放到火上,然后去看电视,在水壶不鸣笛之前不再去看它,等听到鸣笛后再去拿壶(异步非阻塞)。
可见,同步和异步是针对水壶而言的,普通水壶:同步;响水壶:异步。而阻塞和非阻塞是针对老张而言的,站在旁边等的老张:阻塞;看电视的老张:非阻塞。
在以上4种做法中,显然第③种,异步阻塞没有多少实用价值,因此,WinSock中讨论的异步模式,指的都是异步非阻塞模式。也就是说,非阻塞模式不一定能做到是异步的,但异步模式一定是非阻塞的。
在WinSock2中,提供了WSAAsyncSelect和WSAEventSelect两种异步I/O模型,二者的差别在于系统通知应用程序的方法不同。
WSAAsyncSelect模型是基于Windows的消息机制实现的,当网络事件发生时,Windows系统将发送一条事件消息给应用程序,应用程序可根据事件类型编写相应的处理程序。
1. WSAAsyncSelect()函数
WSAAsyncSelect模型的核心是WSAAsyncSelect()函数,该函数的主要功能是为指定的套接字向系统注册一个或多个应用程序需关注的网络事件,注册网络事件时,需要指定事件发生时需要发送的消息,以及处理该消息的窗口的句柄。程序运行时,一旦被注册的事件发生,系统将向指定的窗口(或对话框)发送指定的消息。
WSAAsyncSelect()函数的语法如下:
int WSAAsyncSelect( SOCKET s, HWND hWnd,unsigned int wMsg, long lEvent )
该函数的4个参数含义如下:
如果网络事件注册成功,该函数将返回0;如果注册失败,则返回SOCKET_ERROR。
表4-1 WSAAsyncSelect()函数常用的网络事件
事件名 |
含义 |
FD_ACCEPT |
当有连接请求到达时触发 |
FD_CONNECT |
当连接建立完成时 |
FD_READ |
接收缓冲区有数据可读时 |
FD_WRITE |
发送缓冲区空出了空间,可发送数据时 |
FD_CLOSE |
当套接字被关闭时 |
FD_OOB |
接收缓冲区收到带外数据时 |
提示:
1)在程序中调用WSAAsyncSelect()函数后,会自动将套接字设置为非阻塞模式,因此WSAAsyncSelect模型是一种异步非阻塞模型。
2)WSAAsyncSelect模型只能用于Windows窗口(或对话框)程序中,无法用在控制台程序中。
2. WSAAsyncSelect模型编程步骤
在传统阻塞模式的Windows通信程序基础上,例如在3.3节程序的基础上,按照如下几步对程序进行扩充,即可实现基于WSAAsyncSelect模型的异步通信程序。
1)第1步:自定义socket事件消息
WSAAsyncSelect模型是基于Windows消息机制的,其WSAAsyncSelect()函数要求它的第3个参数wWsg是一个自定义消息,该自定义消息一般命名为WM_SOCKET,并且,为了保证自定义消息不会和Windows预定义消息发生冲突,该消息的值一般设置为比WM_USER预定义消息的值大一些,下面是定义WM_SOCKET消息的示例代码:
#define WM_SOCKET WM_USER+0x10
2)第2步:用WSAAsyncSelect()函数将套接字设置为异步方式
WSAAsyncSelect()函数一般紧跟在socket函数或者bind函数后面,这样,就能将传统WinSock中的套接字设置为异步非阻塞模式。示例代码如下:
sockSer=socket(AF_INET,SOCK_STREAM,0);
WSAAsyncSelect(sockSer, hDlg,WM_SOCKET, FD_ACCEPT |FD_READ | FD_CLOSE);
上述代码将套接字sockSer设置为了异步非阻塞模式,当网络事件FD_ACCEPT、FD_READ或FD_CLOSE发生时,将会把消息WM_SOCKET投递给对话框hDlg。对话框此时可以对WM_SOCKET消息进行处理。
本例中,注册了3个网络事件:FD_ACCEPT、FD_READ和FD_CLOSE,应用程序要注册哪些网络事件,完全取决于实际需要。但是,如果要注册多个事件,必须将多个事件名用按位或运算符(|)连接起来,再将它们作为整体赋值给lEvent参数,而绝不能写成如下形式:
WSAAsyncSelect(sockSer, hDlg,WM_SOCKET, FD_ACCEPT);
WSAAsyncSelect(sockSer, hDlg,WM_SOCKET, FD_READ);
WSAAsyncSelect(sockSer, hDlg,WM_SOCKET, FD_CLOSE);
因为任何一个套接字如果多次调用WSAAsyncSelect()函数,那么后一次的函数调用将取消前面注册的网络事件。因此,只有最后一条语句会起作用。
3)第3步:添加网络事件消息处理程序
当套接字上注册的网络事件发生时,系统将向指定的对话框发送指定的用户自定义消息,进而触发对该消息的消息处理函数的执行,消息处理函数必须能判断是发生了哪种网络事件,这需要用到WinSock提供的一个宏:WSAGETSELECTEVENT。示例代码如下:
case WM_SOCKET: //自定义消息
switch (WSAGETSELECTEVENT(lParam)) {
case FD_ACCEPT: { //接收请求事件
…… }
case FD_READ: { //可读事件
…… }
break; }
我们知道任何消息都有两个参数,对于WM_SOCKET消息来说,它的lParam参数的低16位存放的是发生的网络事件,高16位则存放了网络事件发生错误时的错误码,而wParam参数存放了发生网络事件的套接字的句柄。
下面按照WSAAsyncSelect模型编程的步骤将项目3.3改写为TCP异步通信版的程序,该程序的运行效果如图4-1所示,可见,该程序与3.3节的程序相比,最大的区别就是没有了“接收”按钮,因为它能自动接收消息。另外,它能实现客户端的“二次连接”,即关闭客户端后,再重新启动客户端,仍然能够成功连接上服务器并与之通信。
图4-1 异步通信版的TCP通信程序(左图为服务器端,右图为客户端)
该程序因为具有异步特性,因此当发生网络事件时(如有数据可读、接受连接成功),Windows系统将发送事件消息通知对话框,对话框此时可在消息处理程序中用recv()函数接收数据,从而实现了自动接收消息。另外,在服务器端单击“创建服务器”按钮,即使没有客户端连接,程序也不会处于假死状态,因为它具有非阻塞特性。
将项目3.3节中的程序改造成本节实例的步骤是:
① 在创建套接字之后用WSAAsyncSelect()函数将套接字设置为异步模式;
② 将“接收”按钮函数中的代码写到FD_READ事件中。
1)新建工程,选择“Win32 Application”,输入工程名(如TCPAsync_Server),然后选择“一个典型的Hello World程序”。
2)在左侧选择FileView选项卡,找到对应的源文件(如TCPAsync_Server.cpp),将WinMain函数中DialogBox一行(148行)和return 0; 保留,其他代码全部删除,再将DialogBox中第3个参数HWnd改为NULL。
3)切换到ResourceView选项卡,找到Dialog下的“IDD_ABOUTBOX”,将对话框的界面及各个控件的ID值设置为如图4-2所示。
图4-2 对话框的界面及各控件ID值
4)打开TCPAsync_Server.cpp文件,编写如下代码:
#include "stdafx.h"
#include "resource.h"
#include "stdio.h"
#include "WINSOCK2.h"
#include
#pragma comment(lib,"ws2_32.lib")
#define WM_SOCKET WM_USER+0x10 //自定义socket消息
HINSTANCE hInst;
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
SOCKET sockSer,sockConn;
SOCKADDR_IN addrSer, addrCli;
int len =sizeof(SOCKADDR);
char sendbuf[256], recvbuf[256];
char clibuf[999]="客户端: >", serbuf[999]="服务器: >";
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow){
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){
char port[5],ip[16];
time_t tt = time(NULL); //这句返回的只是一个时间cuo
tm* t= localtime(&tt);
char szTime[20]; //用来保存时间
switch (message) {
case WM_INITDIALOG:
SetDlgItemText(hDlg,IDC_IP,"127.0.0.1");
SetDlgItemText(hDlg,IDC_PORT,"5566");
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2), &wsaData)) {
MessageBox(hDlg,"Winsock加载失败","警告",0);
WSACleanup();
return -1; }
sockSer=socket(AF_INET,SOCK_STREAM,0);
//设置异步方式
WSAAsyncSelect(sockSer, hDlg,WM_SOCKET,FD_ACCEPT |FD_READ | FD_CLOSE);
return TRUE;
case WM_SOCKET: //自定义消息
switch (WSAGETSELECTEVENT(lParam)) { //选择要处理的事件
case FD_ACCEPT:{ //接收请求事件
sockConn=accept(sockSer,(SOCKADDR*) &addrCli,&len); }
break;
case FD_READ: { //可读事件
recv(sockConn,recvbuf,256,0); //接收消息
strcat(clibuf,recvbuf);
//将接收到的消息添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
strcpy(clibuf, "客户端: >"); } //重新给字符串赋值
break;
case FD_CLOSE: { //关闭连接事件
MessageBoxA(NULL, "正常关闭连接", "tip", 0); }
break; }
break;
case WM_COMMAND: {
switch(LOWORD(wParam) )
{ case IDC_QUIT: //单击了退出按钮
EndDialog(hDlg, LOWORD(wParam));
closesocket(sockSer);
WSACleanup();
return TRUE;
case IDC_CREATE: //单击了创建服务器按钮
GetDlgItemText(hDlg,IDC_IP,ip,16);
GetDlgItemText(hDlg,IDC_PORT,port,5);
EnableWindow(GetDlgItem(hDlg,IDC_CREATE),FALSE); //使该按钮失效
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(atoi(port));
addrSer.sin_addr.S_un.S_addr=inet_addr(ip);
bind(sockSer,(SOCKADDR*) &addrSer,len);
listen(sockSer,5);
break;
case IDC_SEND: //单击了发送按钮
sprintf(szTime," %02d:%02d:%02d", t->tm_hour, t->tm_min, t->tm_sec);
GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256);
strcat(sendbuf,szTime);
send(sockConn,sendbuf,strlen(sendbuf)+1,0); //发送带时间的消息
SetDlgItemText(hDlg,IDC_SENDBUF,NULL);
strcat(serbuf,sendbuf);
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);
strcpy(serbuf, "服务器: >"); //重新给字符串赋值
break; } }
break; }
return FALSE; }
按照制作服务器端程序的方法新建工程,工程名为TCPAsync_Client,程序的界面及各控件的ID属性值如图4-3所示。
图4-3 客户端界面及各控件的ID
打开TCPAsync_Client.cpp文件,编写如下代码:
#include "stdafx.h"
#include "resource.h"
#include "stdio.h"
#include "WINSOCK2.h"
#pragma comment(lib,"ws2_32.lib")
#define WM_SOCKET WM_USER+0x10 //自定义socket消息
HINSTANCE hInst;
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
SOCKET sockCli; //客户端套接字
SOCKADDR_IN addrSer, addrCli;
int len =sizeof(SOCKADDR);
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow){
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, NULL, (DLGPROC)About);
return 0;
}
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam,
LPARAM lParam) {
char sendbuf[256],recvbuf[256];
char clibuf[999]="客户端: >", serbuf[999]="服务器: >";
char ip[16],port[5]; int res;
switch (message) {
case WM_INITDIALOG: //对话框初始化时
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData); //初始化协议栈
SetDlgItemText(hDlg,IDC_IP,"127.0.0.1");
SetDlgItemText(hDlg,IDC_PORT,"5566");
sockCli=socket(AF_INET,SOCK_STREAM,0);
//设置异步方式
WSAAsyncSelect(sockCli, hDlg,WM_SOCKET, FD_READ |FD_CLOSE);
return TRUE;
case WM_SOCKET: //自定义消息
switch (WSAGETSELECTEVENT(lParam)){
case FD_READ: { //可读事件
recv(sockCli,recvbuf,256,0); //接收信息
strcat(serbuf,recvbuf);
//将接收到的数据添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);
strcpy(serbuf, "服务器: >"); } //重新给字符串赋值
break;
case FD_CLOSE: { //关闭连接事件
MessageBoxA(NULL, "正常关闭连接", "tip", 0); }
break; }
break;
case WM_COMMAND: {
switch(LOWORD(wParam) )
{ case IDC_QUIT:
EndDialog(hDlg, LOWORD(wParam));
closesocket(sockCli); //关闭套接字
WSACleanup(); //清空协议栈
return TRUE;
case IDC_CONN: //单击了连接服务器按钮
GetDlgItemText(hDlg,IDC_IP,ip,16); //获取ip
GetDlgItemText(hDlg,IDC_PORT,port,5);
EnableWindow(GetDlgItem(hDlg,IDC_CONN),FALSE);
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(atoi(port));
addrSer.sin_addr.S_un.S_addr=inet_addr(ip);
res=connect(sockCli,(SOCKADDR*)&addrSer,sizeof(SOCKADDR));
if(res==0){
MessageBox(NULL,"客户端连接服务器失败","警告",0);}
else MessageBox(NULL,"客户端连接服务器成功","通知",0);
break;
case IDC_SEND: //单击了发送按钮
GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256);
send(sockCli,sendbuf,strlen(sendbuf)+1,0); //发送消息
SetDlgItemText(hDlg,IDC_SENDBUF,NULL); //清空发送框
strcat(clibuf,sendbuf); SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
strcpy(clibuf, "客户端: >"); //重新给字符串赋值
break; } }
break; }
return FALSE; }
1. 在异步通信模式中,接收网络数据的程序应写在下列哪个网络事件中( )
A. FD_READ B. FD_WRITE
C. FD_ACCEPT D. FD_CONNECT
2. 网络事件的类型一般保存在 宏的lParam低字位中。( )
A. WSAGETSELECTEVENT B. WSANETWORKEVENTS
C. WSAEVENTSELECT D. WSAEVENT
3. 关于WinSock套接字编程,下列说法中错误的是: ( )
A. WSAAsyncSelect()函数不能用在控制台程序中
B. 客户端可以不使用bind()函数
C. 即使不使用WSAStartup()进行初始化,程序编译也不会出错
D. htons()函数的参数是一个表示端口号的字符数组
4. . 如果要在程序中注册多个网络事件,应该把多个网络事件写在 条WSAAsyncSelect()语句中(填1或多)。
5.异步通信使用的是WinSock的 模式(填阻塞或非阻塞)。
6. 在Windows应用程序中,怎样实现不需要接收按钮自动接收网络消息?
7.阅读下面的程序段,并解释程序中①-⑤处每条语句的作用,包括函数的功能和函数中各个参数的含义。