Windows Sockets程序设计
3.1 Windows Sockets简介
3.1.1 什么是Windows Sockets
Sockets最初只是UNIX系统中最流行的网络通信接口之一,它不支持微机DOS环境和Microsoft Windows环境。Sockets在UNIX中成功的应用产生了将其移植到DOS和Windows环境下的设想。1992年,制定出Windows Sockets规范1.0版;在此之后,一些公司根据1.0版规范设计开发了Windows Sockets实现,并在实现上开发了应用程序。在实际使用过程中,发现了一些问题,因此由Martin Hall领导的Windows Sockets委员会对1.0版作了一些修改,并与1993年推出1.1版规范。本书便是根据Windows Sockets规范1.1版编写的。
Windows Sockets是Microsoft Windows的网络程序设计接口,它是从Berkeley Sockets扩展而来的。Windows Sockets在继承了Berkeley Sockets主要特征的基础上,又对它进行了重要扩充。这些扩充主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。这些扩充有利于应用程序开发者编制符合Windows编程模式的软件,它使在Windows下开发高性能的网络程序成为可能。
3.1.2 Windows Sockets组成部分
Windows Sockets实现一般都由两部分组成;开发组件和运行组件。
开发组件是供程序员开发Windows Sockets应用程序使用的,它包括介绍Windows Sockets实现的文档、Windows Sockets应用程序接口(API)引入库和一些头文件。头文件WINSOCK.H是Windows Sockets最重要的头文件,它包括了Windows Sockets实现所定义的宏、常数值、数据结构和函数调用接口原型。其他头文件是为了兼容目的,它们没有实质内容,只是为了方便Berkeley Sockets下开发的网络程序的移植。因此,对于Windows Sockets应用程序的源文件来说,只要包括WINSOCK.H就足够了。
运行组件是Windows Sockets应用程序接口的动态连接库(DLL),文件名为 WINSOCK.DLL,应用程序在执行时通过装入它实现网络通信功能。
3.1.3 Windows Sockets对Berkeley Sockets的扩充
Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。基于这一策略,Windows Sockets在如下方面作了扩充:
(1)异步选择机制
UNIX下Sockets对于异步事件的选择是靠调用select()函数来查询的,这种方式对于Windows应用程序来说是难以接受的。Windows Sockets的异步选择函数提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。
(2)异步请求函数
在 标准Berkeley Sockets中,请求服务是阻塞的。Windows Sockets除了支持这一类函数外,还增加了相应的异步请求服务函数WSAASyncGetXByY()。这些异步请求函数允许应用程序采用异步方式获取请求信息,并且在请求的服务完成时给应用程序相应的窗口函数发送一个消息。
(3)阻塞处理方法
Windows Sockets为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃CPU让其它应用程序运行,它在调用处于阻塞时便进入一个叫“HOOK”的例程,此例程负责接收和分配Windows消息,这使得其它应用程序仍然能够接收到自己的消息并取得控制权。Windows Sockets还提供了两个函数(WSASetBlockingHook()和WSAUnhookBlockingHook())让用户设置和取消自己的阻塞处理例程,以支持要求复杂消息处理的应用程序(如多文档界面)。
(4)出错处理
Windows Sockets为了和以后多线程环境(如Windows/NT)兼容,它提供了两个出错处理函数WSAGetLastError()和WSASetLastError()来获取和设置当前线程的最近错误号,而不使用Berkeley Sockets中的全局变量errno和h_errno。
(5)启动与终止
对于所有在Windows Sockets上开发的应用程序,在它使用任何Windows Sockets API调用之前,必须先调用启动函数WSAStartup(),它完成Windows Sockets DLL的初始化;协商版本支持,分配必要的资源。在应用程序完成了对Windows Sockets的使用之后,它必须调用函数WSACleanup()来从Windows Sockets实现中注销自己,并允许实现释放为其分配的任何资源。
3.2 异步选择机制
使用Windows Sockets实现Windows网络程序设计的关键是它提供了对网络事件基于消息的异步存取。Windows Sockets提供了一个异步选择函数WSAAsyncSelect(),它用来注册应用程序感兴趣的网络事件,当这些网络事件发生时,应用程序相应的窗口函数将收到一个消息。
WSAAsyncSelect()函数的原型为:
int PASCAL FAR WSAAsyncSelect(SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);
它请求Windows Sockets DLL在检测到套接字s上发生的网络事件lEvent时,向窗口hWnd发送一个消息wMsg。它自动地设置套接字处于非阻塞工作方式,参数lEvent由下列事件的一个或多个组合而成:
值 含义
FD_READ 期望在套接字s收到数据(即读准备好)时接到通知。
FD_WRITE 期望在套接字s可发送数据(即写准备好)时接到通知。
FD_OOB 期望在套接字s上有带外数据到达时接到通知
FD_ACCEPT 期望在套接字s上有外来连接时接到通知
FD_CONNECT 期望在套接字s连接建立完成时接到通知
FD_CLOSE 期望在套接字s关闭时接到通知
例如,我们要在套接字s读准备好或写准备好时接到通知,可以使用下面的语句:
rc = WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_WRITE);
当套接字s上注册的网络事件(读准备好或写准备好)发生时,窗口hWnd将收到消息wMsg。消息变量wParam指示发生网络事件的套接字,变量lParam的低字描述发生的网络事件,高字包含错误码。
针对上面的语句,我们可以在窗口函数的消息循环中增加一个分支:
case wMsg:
switch (lParam) {
case FD_READ:
/* 从套接字wParam读取数据 */
case FD_WRITE:
/* 向套接字wParam发送数据 */
}
break;
需 要注意的是,为一个套接字发布的WSAAsyncSelect()函数调用将取消在它之前发布的WSAAsyncSelect()函数调用的作用。在上面的例子里,使用下面的代码不会做所期望的工作:第二次调用将取消第一次调用的作用,只有FD_WRITE事件发生时会用消息wMsg2来报告。
rc = WSAAsyncSelect(s, hWnd, wMsg1, FD_READ);
rc = WSAAsyncSelect(s, hWnd, wMsg2, FD_WRITE);
根据这一特性,我们为了注销对套接字s上发生的网络事件的消息发送,就可以将参数lEvent设为0来调用它:
rc = WSAAsyncSelect(s, hWnd,0,0);
Windows Sockets DLL在一个网络事件发生后,它只给相应的应用程序发送一个消息。但是当应用程序使用函数调用隐式地允许重新发送此网络事件的消息时,应用程序的窗口函数又可能再次收到相应的消息。比如说,Windows Sockets DLL在套接字s接口收到100个字节的数据,它发出一个FD_READ消息,应用程序在收到FD_READ消息后,调用 recv(s,buffptr,50,0)读取了50个字节的数据,由于数据没有读完,Windows Sockets DLL又会发出另一个FD_READ消息。因此,应用程序不必为响应一个FD_READ消息而读完所有接收到的数据,使用一个recv()调用来响应一个 FD_READ消息是合适的。如果一个应用程序执行多个recv()调用来响应一个FD_READ消息,它可能会收到多个FD_READ消息。这样的应用程序可以在调用recv()执行前使用函数WSAAsyncSelect()来注销对FD_READ事件的登记。和FD_READ事件类似的还有 FD_OOB事件和FD_ACCEPT事件。对于FD_CONNECT和FD_CLOSE事件来说,因为它们只是一种状态(连接建立成功和连接关闭),因此不会产生多个消息。事件FD_WRITE和上述事件的处理稍有不同:当一个套接字首次用connect()或accept()建立连接时,Windows Sockets DLL将发出一个FD_WRITE消息;然后只有在调用send()或sendto()因为可能阻塞(错误代码WSAEWOULDBLOCK)而失败返回,并且发送缓冲区空间重新变为可用时才有FD_WRITE消息发出。因此,应用程序可以假设从接到FD_WRITE消息开始直到发送因可能阻塞失败返回为止的这段时间都是可以发送的,在这样一次失败后,应用程序将在再次可以发送时接收到一个FD_WRITE消息。如果应用程序在收到FD_WRITE消息之后使用send()或sendto()调用发送数据均成功返回,那么它在以后需要再次发送数据时可以直接发送;但如果它希望再次获得一个 FD_WRITE消息,则必须使用函数WSAAsyncSelect()来重新注册FD_WRITE事件。
在上一章中,已经讲到,使用accept()函数接收套接字上的连接时,它创建一个新的套接字用于连接,原来的套接字仍处于监听状态,新的套接字和用于监听的套接字具有相同的属性。如果使用WSAAsyncSelect()函数为监听套接字设置了事件选择,那么此事件选择将同样作用于新的接收套接字。例如,如果监听套接字发生了WSAAsyncSelect()选择的事件FD_ACCEPT,FD_READ和FD_WRITE,那么任何在此监听套接字上接收的套接字也会有FD_ACCEPT,FD_READ和FD_WRITE事件,并且它们拥有相同的消息wMsg值。如果应用程序对于不同的接收套接字要求不同的消息或事件,那么它应该使用WSAAsyncSelect()函数来给接收套接字设置不同的事件选择。
3.3 阻塞处理方法
Microsoft Windows是非抢先的多任务环境,如果一个应用程序不主动放弃其控制权,别的应用程序就不能够执行。因此,在Windows Sockets网络程序设计中,尽管它支持阻塞操作,但是它是反对使用阻塞操作的。而在Berkeley Sockets程序中,套接字的默认操作模式却是阻塞的,对于从Berkeley Sockets环境中移植来的应用程序来说,阻塞处理问题是不可回避的。
在Windows Sockets实现中,对于不能立即完成的阻塞操作做如下处理:DLL初始化操作,然后进入一个循环。在循环中,它发送任何Windows消息,并检查这个Windows Sockets调用是否完成,在必要时,它可以放弃CPU让其它应用程序执行。如果此阻塞操作完成,或者应用程序调用了WSACancelBlockingCall() 函数取消了此阻塞操作,那么DLL将结束此阻塞函数调用,并返回相应结果。对这一阻塞处理机制,可用下面的伪代码表示:
for (;;) {
/* flush messages for good user response */
while (BlockingHook())
;
/* check for WSACancelBlockingCall() */
if (operation_Cancelled())
break;
/* check to see if operation completed */
if (operation_complete())
break; /* normal completion */
}
在Windows Sockets中,有一个默认的阻塞处理例程BlockingHook(),它等价于下面的代码:
BOOL DefaultBlockingHook(void) {
MSG msg;
BOOL ret;
/* get the next message if any */
ret = (BOOL)PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
/* if we got one, process it */
if (ret) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
/* TRUE if we got a message */
return ret;
}
此例程只是简单地获取并发送Windows消息,对于要求更复杂消息处理的应用程序(如使用了多文档界面的应用程序),Windows Sockets提供了用户安装自己的阻塞处理例程的能力。函数WSAbetBlockingHook()用来安装用户自己定义的阻塞处理例程,在操作处于阻塞时,此阻塞处理例程将取代原来的阻塞处理例程。
与 WSASetBlockingHook()函数相对应的是WSAUnhookBlockingHook()函数,它删除先前安装的任何阻塞处理例程,并重新安装默认的阻塞处理例程。考虑到应用程序函数的嵌套调用,应用程序最好在使用WSASetBlockingHook()函数时保存其返回原来的阻塞例程,并在必要时重新安装原来的阻塞处理例程,而不只是简单地调用WSAUnhookBlockingHook()函数。
用户在设计自己的阻塞处理例程时,除了函数WSACancelBlockingCall()外,它不能使用其它的Windows Sockets API函数。因为此时已经有了一个正处于阻塞的Windows Sockets API函数在处理,此时调用任何其它Windows Sockets函数将以WSAEINPROGESS错误码而失败返回。在阻塞处理例程中调用WSACancleBlockingHook()函数将取消处于阻塞的操作,它将结束阻塞循环。
3.4 Windows Sockets网络程序设计
在Microsoft Windows下开发Windows Sockets网络程序与在UNIX环境下开发Berkeley Sockets网络程序有一定的差别,这主要时因为Windows是非抢先多任务环境,各任务之间的切换是通过消息驱动的。因此,在Windows下开发 Sockets网络程序要尽量避开阻塞工作方式,而使用Windows Sockets提供的基于消息机制的网络事件异步存取接口。
3.4.1 启动与终止
在Windows Sockets提供的所有函数中,只有启动函数WSAStartup()和终止函数WSACleanup()是必须使用的。
启动函数WSAStartup()必须是Windows Sockets应用程序第一个调用的Windows Sockets函数。它允许应用程序指定Windows Sockets API要求的版本,并获取特定的Windows Sockets实现的一些技术细节。应用程序只有在一次成功的WSAStartup()启动之后才能执行其它Windows Sockets API函数。
Windows Sockets为了支持将来升级版本,在WSAStartup()函数中安排了一次协商。WSAStartup()调用者和Windows Sockets DLL都给对方指出它所能支持的最高版本号,并且都确认对方的最高版本是否是可接受的。在WSAStartup()入口,Windows Sockets DLL检查应用程序要求的版本号,如果此版本高于DLL能够支持的最低版本,则调用成功,DLL在wHighVersion中返回它支持的最高版本号,在 wVersion中返回它的最高版本号和wVersionRequested的较小者。Windows Sockets DLL就假设应用程序将使用版本wVersion,如果结构WSADATA的wVersion元素不能为调用者所接受,那么它应该调用 WSACleanup()终止使用Windows Sockets DLL,并寻求另外一个Windows Sockets DLL或认为初始化失败。
一个只支持Windows Sockets版本1.1的DLL在WSAStartup()中的版本协商可以用下面的代码段表示:
/* Make sure that the version requested is >=1.1 */
/* The low byte is the major version and the high */
/* byte is the minor version. */
if (LOBYTE(wVersionRequested)<1 || (LOBYTE(wVersionRequested)==1 &&
HIBYTE(WVersionRequested<1))) {
return WSAVERNOTSUPPORTED;
}
/* since we only support 1.1, set both Wversion and */
/* wHigh Version to 1.1 */
lpWsaData->wVersion = MAKEWORD(1,1);
lpWsaData->wHighVersion = MAKEWORD(1,1);
了解了Windows Sockets DLL在WSAStartup()函数中所作的版本协商,我们就可以在应用程序中设计出合适的版本协商程序。下面的代码段演示了只支持Windows Sockets版本1.1的应用程序是如何进行WSAStartup()调用的:
WORD wVersionRequested;
WSADATA wsaDate;
int err;
wVersionRequested = MAKEWORD(1,1);
err = WSAStartup(wVersionRequested, &wsaData);
if (err!=0) {
/* Tell the user that we couldn‘t find a usable winsock.dll */
return;
}
/* Confirm that the Windows Sockets DLL supports 1.1, note that */
/* if the DLL support versions greater than 1.1 in addition to 1.1, */
/* it will still return 1.1 in wVersion since that is the version */
/* we requested.*/
if (LOBYTE(wsaData.wVersion)!=1 || HIBYTE(wsaData.wVersion)!=1) {
/* tell the user that we conldn‘t find a useable winsock.dll */
WSACleanup();
return;
}
/* the Windows Sockets DLL is acceptable proceed */
这种协商允许Windows Sockets DLL和Windows Sockets应用程序支持的Windows Sockets版本都可有一定的范围,如果两者的版本范围有任何重叠,应用程序就可以成功地使用Windows Sockets DLL。表3.1给出了WSAStartup()如何结合不同的应用程序和Windows Sockets DLL得到适当版本的例子:
表3.1 Windows Sockets DLL版本协商结果表
应用程序版本 | DLL版本 | wVersionRequested | wVersion | wHighVersion | 最终结果 |
1.1 | 1.1 | 1.1 | 1.1 | 1.1 | use 1.1 |
1.0 1.1 | 1.0 | 1.1 | 1.0 | 1.0 | use 1.0 |
1.0 | 1.0 1.1 | 1.0 | 1.0 | 1.1 | use 1.0 |
1.1 | 1.0 1.1 | 1.1 | 1.1 | 1.1 | use 1.1 |
1.1 | 1.0 | 1.1 | 1.0 | 1.0 | Application fails |
1.0 | 1.1 | 1.0 | --- | --- | WSAVERNOTSUPPORTED |
1.0 1.1 | 1.0 1.1 | 1.1 | 1.1 | 1.1 | use 1.1 |
1.1 2.0 | 1.1 | 2.0 | 1.1 | 1.1 | use 1.1 |
一 旦应用程序做了一次成功的WSAStartup()调用,它就可以进行所需要的其它Windows Sockets API的调用了。当应用程序使用完Windows Sockets DLL的服务后,它必须调用WSACleanup()函数来从Windows Sockets DLL中注销自己,并让Windows Sockets DLL释放为此应用程序分配的任何资源。当WSACleanup()调用时,任何打开并已连接的SOCK_STREAM套接字被复位,但那些已由 closesocket()函数关闭但仍有未发送数据的套接字不受影响,未发送的数据仍将被发送。
需 要注意的是,如果应用程序需要多次获取WSAData结构信息,它可以调用WSAStartup()函数多次。然而,wVersionRequested 参数是假设为所有WSAStartup()调用都相同的;也就是说,应用程序初次调用WSAStartup()之后就不能改变它所期望的Windows Sockets版本。如果一个应用程序调用了多次WSAStartup()函数,那它必须调用相同次数的WSACleanup()函数,前面的调用只是简单地减少一个内部参考计数器,最后一次调用才真正做所必须的资源释放工作。设置内部参考计数器的目的是为了使Windows Sockets DLL的使用对应用程序透明,应用程序在使用Windows Sockets DLL时就不必关心其它应用程序是否也在使用相同的Windows Sockets DLL。
3.4.2 异步请求服务
在Berkeley Sockets中,请求服务是同步的,应用程序等待服务完成后获得请求的消息。例如,我们可以调用使用下面的语句获取主机信息:
struct hostent *hostaddr;
char hostname = “rs6000”
hostaddr = gethostbyname(hostname);
当调用结束后,如果是成功返回(hostaddr不为NULL),则hostaddr指向的hostent结构中包含了请求的信息。
Windows Sockets在支持上述同步请求服务的同时,还增加了一类异步请求服务函数WSAAsyncGetXByY()。
函 数WSAAsyncGetXByY()是阻塞请求函数getXbyY()的异步版本。应用程序调用它时,由Windows Sockets DLL初始化这一操作并立即返回调用者,此函数返回一个异步句柄,用来标识这个异步操作。当操作完成时,结果存储在调用者提供的缓冲区,并且发送一个消息到应用程序相应窗口。WSAAsyncGetXByY()类函数一般具有下面的原型:
HANDLE PASCAL FAR WSAAsyncGetXByY(HWND hWnd, unsigned int wMsg, ......, char FAR * buf, int buflen);
函数的返回值说明了异步操作是否成功初始化,它并不指示操作本身是成功还是失败。如果异步操作被成功初始化,其返回值是一个HANDLE 类型的非0值,它是此请求的异步任务句柄,应用程序可以使用它通过调用WSACancelAsyncRequest()函数来取消此异步操作,或者在操作完成时用于识别和匹配相应的异步操作。如果异步操作不能初始化,则返回值为NULL。当异步操作完成时,应用程序窗口hWnd将收到消息wMsg。消息变量wParam包含了原始函数返回的异步任务句柄,消息变量lParam的高16位包含了错误码。如果错误码为0则指示异步操作成功完成,此时原始函数提供的缓冲区中包含了请求的信息。如果错误码为WSAENOBUFS,它指示原始调用中指定的缓冲区大小buflen太小,不能包容所有的结果信息,此时,消息变量lParam的低16位包含了支持所有必要的信息所要求的缓冲区大小,应用程序可以用一个足够大(例如,不少于变量lParam的低 16位值)能够接受所有要求的信息的缓冲区来重新调用此异步函数。
对于前面请求主机信息的程序段,在Windows Sockets网络程序中可以改写为:
HANDLE taskHnd;
char hostname = “rs6000”;
taskHnd = WSAAsyncGetHostByName(hWnd, wMsg, hostname, buf, buflen);
此语句不需等待立即返回。另外,在消息循环中我们可以增加如下分支语句:
case wMsg:
if (HIWORD(lParam)==0) {
if (wParam==taskHnd) {
/* 从buf中提取所需要的信息 */
}
}
break;
需要注意的是,由于Windows的内存对象是可以设置为可移动或可丢弃的,因此在异步操作使用了内存对象(缓冲区buf)指针作参数时,应用程序必须保证在整个异步操作期间,Windows Sockets DLL对此内存对象是可用的。
另 外,Windows Sockets还提供了一个函数WSACancelAsyncRequest()来取消一个未完成的异步操作。当应用程序不需要一个异步操作时,它可以用此操作的异步任务句柄作参数调用WSACancelAsyncRequest()函数来终止此异步操作。
3.4.3 异步数据传输
Windows Sockets提供的传输接口和Berkeley Sockets一致,也是使用send()或sendto()函数发送数据,使用recv()或recvfrom()函数接收数据。但是,Windows Sockets不鼓励用户使用阻塞方式传输数据,这样的调用可能阻塞整个Windows环境。Windows Sockets推荐使用它的基于消息的网络事件异步选择机制,用消息来驱动数据的发送和接收。
下面我们用一个实例来说明Windows Sockets消息驱动的异步数据传输。
假设套接字s在连接建立后,已经使用函数WSAAsyncSelect()在其上注册了网络事件FD_READ,和FD_WRITE,并且wMsg值为UM_SOCK,那么我们可以在Windows消息循环中增加如下的分支语句:
case UM_SOCK:
switch (lParam) {
case FD_READ:
len=recv(wParam, lpBuffer, length, 0);
break;
case FD_WRITE:
while (send(wParam, lpBuffer, len, 0) != SOCKET_ERROR);
break;
}
break;
这是一个最简单的异步数据传送,应用程序在收到网络事件发生的消息时,判别它是读准备好还是写准备好,如果是读准备好,则调用recv()函数读取一定量的数据(如果数据未读完,应用程序窗口将再次收到此消息);如果是写准备好,则重复发送缓冲区的数据,一般情况下它会因可能阻塞而出错返回,这样可再次可以发送(写准备好)时,应用程序窗口将再次收到FD_WRITE消息。
异步数据传输对缓冲区内存对象的要求与异步请求相同,应用程序必须保证在整个调用期间Windows Sockets对内存对象是可用的。
3.4.4 出错处理
在Berkeley Sockets中,错误码是通过全局变量errno和h_errno取得的。但在Windows Sockets中,为了和以后的多线程版本Windows兼容,它提供了一个函数WSAGetLastError(),此函数用来获取最近的错误码。
前面在设计程序时,我们都没有涉及到错误处理,考虑到错误处理,推荐的编程风格为:
len = send(s, lpBuffer, len, 0);
if ((len==socket_ERROR) && (WSAGetLastError()==WSAEWOULDBLOCK))
{......}
注意到推荐的风格中包括使用了WSAGetLastError()函数和Windows Sockets定义的错误代码的宏。
与WSAGetLastError()函数相对应,Windows Sockets还提供了一个函数WSASetLastError(),它为应用程序设置当前线索的错误码,此代码可为随后的WSAGetLastError()函数调用获得。
3.4.5 宏的使用
Windows Sockets为了增强应用程序源代码的可移植性,特地为用户定义了一系列的宏。例如,在注册过的网络事件发生时,应用程序的窗口将收到一个指定网络事件的消息,消息的变量lParam的高字描述错误码,低字描述发生的网络事件。Windows sockdts为了方便用户对错误码和网络文件的提取,定义了如下的宏:
#define WSAGETSELECTERROR(lParam) HIWORD(lParam)
#define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
使用这两个宏定义,我们可以将本节第三部分的程序段改写为下面的形式,它比原来的程序段更健壮。
case UM_SOCK:
switch (WSAGETSELECTEVENT(lParam)) {
case FD_READ:
if (WSAGETSELECTERROR(lParam))
{.../*错误处理*/}
else
len = recv(wParam, lpBuffer, Length, 0);
break;
case FD_WRITE:
if (WSAGETSELECTERROR(lParam))
{....../*错误处理*/}
else {
while (send(wParam, lpBuffer, len, 0) != SOCKET_ERROR);
if (WSAGetLastError() != WSAEWOULDBLOCK)
{.../*错误处理*/}
}
break;
}
break;
类似的宏定义还有:
#define WSAGETASYNCERROR(lParam) HIWORD(lParam)
#define WSAGETASYNCBUFLEN(lParam) LOWORD(Lparam)
它们用于异步请求服务完成时,在消息中提供错误码和支持请求信息所必需的缓冲区大小。
另 外,由于Windows Sockets某些函数在接口上虽然与Berkeley Sockets一致,但是它们的内部实现却完全不一样。比如说,在函数select()的参数中,Berkeley Sockets实现套接字集合是使用的位掩码,但在Windows Sockets中却是使用一个SOCKET的数组。虽然套接字的集合仍由fd_set类型表示,但是在Berkeley Sockets源文件中直接修改fd_set结构的代码在Windows Sockets 环境下将不能正常工作。因此,Windows Sockets建议程序员在设计应用程序时,必须坚持使用宏FD_XXX来设置,初始化,清除和检查fd_set结构,以防止潜在的错误。关于 Windows Sockets提供的宏的使用,我们将在第六章中详细介绍。
3.4.6 移植应用程序
Windows Sockets实现与Berkeley Sockets的实现是有区别的,在前面的几部分中,我们已经对其中的重要部分作了较为详细的讨论,这里我们将对把应用程序从Berkeley Sockets环境中移植到Windows Sockets环境中应做的工作做一较为全面的总结。
在源文件开始增加#include <winsock.h>语句。
调用WSAStartup()函数初始化Windows Sockets DLL,并在结束时调用WSACleanup()函数通知Windows Sockets DLL释放资源。
将套接字的类型为int改为SOCKET。
获取、设置错误码不要使用全局变量errno, 将其改为Windows Sockets推荐的WSAGetLastError()函数和WSASetLastError()函数。
使用closesocket()函数代替close()函数来关闭套接字;使用ioctlsocket()函数代替ioctl()函数和fcntl()函数实现对套接字模式的控制。
getsockopt()函数和setsockopt()函数在Berkeley Sockets和Windows Sockets中提供的支持不同,源程序可能要做必要的修改。详见第六章函数的使用说明。
将源程序中对结构fd_set的直接修改改为通过使用FD_XXX宏来修改。
将源程序中错误码改为Windows Sockets推荐的以WSA打头的常数定义值。
将Windows Sockets应用程序的指针都设为FAR类型。
最好将源程序中的阻塞调用改为Windows Sockets推荐使用的基于消息的异步操作。
3.5 较深入的问题
3.5.1 中间DLL设计
Windows Sockets实现为应用程序提供了socket级的编程接口,应用程序可以通过直接存取WINSOCK.DLL来使用这些函数接口。对于较大型的应用程序组来说,它们可能希望设计一个虚拟网络应用程序接口层,它使用Windows Sockets API,为应用程序提供通用的网络功能。这样的接口层可以采用中间DLL的形式,它将应用程序和Windows Sockets API 隔离开来,使程序员能更方便地设计自己的应用程序。这样设计的应用程序也更易于移植,比如移植到将来的多线程环境下的Windows Sockets,就只需要对中间DLL做相应的修改即可。
中 间DLL可能同时为几个应用程序使用,因此,它必须注意到对每个间接使用了Windows Sockets API的任务都调用了WSAStartup()和WSACleanup()函数。这是因为对使用了Windows Sockets API的任务来说,Windows Sockets DLL必须首先调用一次WSAStartup()函数为它设置任务专用的数据结构; 并在结束使用Windows Sockets DLL之前调用一次WSACleanup()函数来释放分配给它的资源。我们可以采用两种方法来做到这一点。对中间DLL来说最简单的方法是拥有类似于 WSAStartup()和WSACleanup()的函数,让应用程序在合适的时候使用,这样中间DLL就可以在这些函数中调用 WSAStartup()和WSACleanup()了。对中间DLL来说另一种方法是建立一个任务句柄表(这些句柄可用Windows 应用程序接口函数GetCurrentTast()获得),并在每一个指向中间DLL的入口检查当前任务是否已经调用了WSAStartup()函数,然后在必要时调用WSAStartup()函数。有一点值得注意的是,对每一个任务来说,中间DLL为它调用的每一次WSAStartup()函数必须有一次WSACleanup()调用相对应。这是因为Windows Sockets DLL为WSAStartup()函数和WSACleanup()函数提供了一个内部参考计数器,对多次调用WSAStartup()函数的任务来说,前面调用的WSACleanup()函数只是简单地减少计数器的值,只有此任务的最后一次WSACleanup()调用才为任务做所有必须的资源释放工作。
对 于那些产生了阻塞调用的中间DLL来说,建议它使用WSASetBlockingHook()安装自己的阻塞处理例程。这是因为控制可能会通过应用程序安装的阻塞处理例程或默认的阻塞处理例程返回到应用程序。使中间DLL失去控制权。如果这样的话,应用程序就有可能通过调用 WSACancelBlockingCall()函数来取消中间DLL的阻塞操作,这时中间DLL的阻塞操作将以错误码WSAEINTR而失败,并且中间 DLL必须尽可能快地将控制权返回调用任务。这样应用程序和中间DLL之间就产生了不可预见的控制权交换,它可能会产生意外,而中间DLL通过安装自己的阻塞处理例程就可避免这一问题。
注意这对Windows NT的DLL来说是不必要的,因为它有不同的进程和DLL结构。在Windows NT环境下,中间DLL可以简单地在它的DLL初始化例程中调用WSAStartup()函数,此例程在一个新进程使用此DLL启动的任何时候都被调用。
3.5.2 多线程环境下的Windows Sockets
Windows Sockets 接口是设计为既可用于单线程版本Windows(如Windows 3.1),又可用于多线程版本Windows(如Windows NT)。在多线程环境中的套接字接口是基本相同的,但是多线程应用程序的设计者必须注意到是应用程序而不是Windows Sockets实现应该负责线程间对套接字的同步存取控制。这和应用程序对于其它形式的I/O,如文件I/O有相同的规则。在套接字上同步调用的失败将导致不可预见的结果; 例如,如果同时有两个send()调用,则无法确认究竟是哪个数据先发送。
在一个线程中关闭套接字,如果在此套接字上有另一个线程的未完成阻塞调用,那么将引起阻塞调用因WSAEINTR而失败,就象此操作被取消一样。这同样适用于select()调用未完成,而应用程序关闭了它选择的一个套接字。
在 抢先多线程版本Windows中,没有安装默认的阻塞处理例程。这是由于处理机不会因为单个应用程序等待操作完成而阻塞,因此不必调用在非抢先 Windows中引起应用程序放弃处理机的函数PeekMessage()或GetMessage()。应用程序可以通过调用 WSASetBlockingHook()函数来为当前线程安装阻塞处理例程,或调用WSAUnhookBlockingHook()函数关闭应用程序安装的所有阻塞处理例程。
源文档 <http://www.cic.tsinghua.edu.cn/jdx/book1/CHAPTER3.htm>