第四章 Windows Socket 1.1 库函数概览

套接口函数

Windows Sockets 规范包含了以下 Berkeley 风格的套接口例程:

accept()* 响应联结请求,并且新建一个套接口。原来的套接口则返回监听状态。

bind() 把一个本地的名字和一个无名的套接口捆绑起来。closesocket()* 把套接口从拥有对象参考表中取消。该函数只有在

SO_LINGER 被设置时才会阻塞。

connect()* 初始化到一个指定套接口上的连接。getpeername() 得到连接在指定套接口上的对等通讯方的名字。getsockname() 得到指定套接口上当前的名字。

getsockopt() 得到与指定套接口相关的属性选项。

htonl() 把 32 位的数字从主机字节顺序转换到网络字节顺序。htons() 把 16 位的数字从主机字节顺序转换到网络字节顺序。inet_addr() 把一个 Internet 标准的"."记号地址转换成 Internet 地

址数值。

inet_ntoa() 把 Internet 地址数值转换成带"."的 ASCII 字符串。ioctlsocket() 为套接口提供控制。

listen() 监听某一指定套接口上连接请求的到来。

ntohl() 把 32 位数字从网络字节顺序转换为主机字节顺序。ntons() 把 16 位数字从网络字节顺序转换为主机字节顺序。recv()* 从一个已连接的套接口接收数据。

recvfrom()* 从一个已连接的或未连接的套接口接收数据。select()* 执行同步 I/O 多路复用。

send()* 从一已连接的套接口发送数据。sendto()* 从已连接或未连接的套接口发送数据。setsockopt() 设置与指定套接口相关的属性选项。shutdown() 关闭一部分全双工的连接。

socket() 创建一个通讯端点并返回一个套接口。

*表示例程在应用于阻塞套接口时会阻塞。

阻塞/非阻塞和数据易失性

阻塞是在把应用程序从 Berkeley 套接口环境中移植到 Windows 环境中的一个主要焦点。阻塞是指唤起一个函数,该函数直到相关操作完成时才返回。由于操作可能需要任意长的时间才能完成,于是问题就出现了。最明显的一个例子就是 recv(),这个函数会一直处于阻塞状态直到收到对方系统发送的数据。在Berkeley 套接口模型中,一个套接口的操作的缺省行为是阻塞方式的,除非程序员显式地请求该操作为非阻塞方式。我们强烈推荐程序员在尽可能的情况下使用非阻塞方式(异步方式)的操作。因为非阻塞方式的操作能够更好地在非占先的 Windows 环境下工作。程序员应该在绝对必要的时候才采用阻塞方式。而且在你必须使用阻塞方式的操作前仔细阅读并理解这一部分。

即使在阻塞方式下,有些操作(例如 bind(),getsockopt(),getpeername()) 也会立刻完成。对于这些操作,阻塞方式和非阻塞方式并没有什么两样。其他一些操作(例如 recv())可能立刻完成,也可能需要阻塞一段随机的时间才能完成。这都取决于不同的传输情况。当用于阻塞套接口时,这些操作被认为是工作于阻塞方式的,所有会阻塞的例程在以前或以后的列表中都打上了星号作标记。

在 Windows Sockets 实现中,一个无法立刻完成的阻塞操作是按如下方式处理的。DLL 先初始化操作,然后进入一个循环,在循环中发送收到的任何信息- 为了使在必要时把处理器交给其他线程,然后检查 Windows Sockets 功能是否完成。如果功能完成了,或者 WSACancelBlockingCall()被唤起,阻塞操作以一个适当的返回值结束。完整的关于这种机制的描述,请参见 5.3.13 节, WSASetBlockingHook(),这一部分还包括了对于各种函数伪代码的讨论。

如果一个正在运行某一阻塞操作的进程收到了一个 Windows 消息,那么应用程序有可能试图发出另一个 Windows Sockets 调用,由于很难安全地处理这种情形,Windows Sockets 规范不支持这种应用程序的工作方式。在这种情况下,有两个函数可以帮助程序员。WSAIsBlocking()可以用来确定在该进程上是否有阻塞的 Windows Sockets 调用。WSACancelBlookingCall()可以用来取消在线的阻塞调用,如果有的话。任何其他的 Windows Sockets 函数如果在这种情况下被调用,则会失败并返回错误代码 WSAEINPROGRESS。要强调的是,这一限制适用于所有阻塞和非阻塞的操作。

虽然这种机制对于简单的应用程序已经足够了,但这不能支持高级应用程序的复杂的消息发送要求。(例如,那些 MDI 模型的用户)对于这样的应用程序, Windows Sockets API 设计了 WSASetBlockingHook()函数,这个函数可以允许程序员定义特殊的阻塞钩子来代替上面讨论的缺省消息发送例程。

只有在以下都为真时,Windows Sockets DLL 才调用阻塞钩子函数:例程是被定义为可以阻塞的,指定的套接口也是阻塞套接口,而且请求不能被立刻完成。(套接口是被缺省地设为阻塞方式的,但 IOCTL FIONBIO 和 WSAAsyncSelect() 都可以把套接口设置成为非阻塞模式)。如果应用程序只使用非阻塞方式的套接口,而且使用 WSAAsyncSelect()和/或 WSAAsyncGetXByY()例程,而不是使用select()和/或 getXbyY()例程,那么阻塞钩子函数就永远也不会被调用,应用程序也不用再操心由于阻塞钩子函数而带来的重入问题。

如果一个应用程序在唤起异步或非阻塞方式调用时使用了一个内存对象的 指针(例如一个缓冲区,或者一个全程变量)作为参数,那么应用程序要保证那个对象在 Windows Sockets 实现的整个操作中都可得到并使用。应用程序不能再唤起可能影响到内存唤射或寻址能力的其他的 Windows 函数。在多线程系统中, 应用程序也有责任使用某种同步机制来协调对内存对象的存取。Windows Sockets 实现不能,也不会提出这种事情。没有遵守这条规则,所可能产生的后果已不在规范讨论的范围之内。

数据库函数

Windows Sockets 规范定义了如下数据库例程。正如我们先前提出的,Windows Sockets 提供者有可能不采用依赖于本地数据库的方式来实现这些函数。某些数据库例程返回的指针(例如 gethostbyname())指向的区域是由 Windows Sockets

函数库分配的。这些指针指向的数据是易失的。它们只在该线程的下一个Windows Sockets API 调用前有效。此外,应用程序不应试图修改这个结构,或者释放其中的任何一部分。在一个线程中,这个结构只有一份拷贝。因此,应用程序应该在发出下一个 Windows Sockets API 调用以前把所需的信息拷贝下来。

gethostbyaddr()* 从网络地址得到对应的名字(有可能多个)和地址。gethostbyname()* 从主机名得到对应的名字(有可能多个)和地址。gethostname() 得到本地主机名。

getprotbyname()* 从协议名得到对应的协议名和数值。getservbyname()* 从一个服务的名字得到对应的服务名以及端口号。getservbyport()* 从一个端口号得到对应的服务名以及端口号。

*表示例程在某些情况下可能会阻塞。

针对 Microsoft Windows 的扩展函数

Windows Sockets 规范提供了许多在标准的Berkelet 套接口例程之外的扩展函数。本质上,这些扩展的 API 是为了应用程序能更好地处理基于消息的异步发送的网络事件。虽然基于 Windows Sockets 的编程并不强制要使用这个扩展的API 集(WSAStartup()和 WSACleanup()除外)但我们推荐应用程序开发者遵循Microsoft Windows 的编程范例。

WSAAsyncGetHostByAddr() 一个标准的Berkeley 的getXbyY()函数集合的异步版本。例如 WSAAsyncGetHostByName()函数提供了一个标准 Berkeley 的gethostbyname()函数的异步基于消息的实现。

WSAAsyncGetHostByName() WSAAsyncGetProtoByName() WSAAsyncGetProtByNumber() WSAAsyncGetServByName()

WSAAsyncGetServByPort()

WSAAsyncSelect() select()函数的异步版本。WSACancelAsyncRequest() 取消一个未完成的 WSAAsyncGetXByY()函数

的实例。

WSACancelBlockingCall() 取消未完成的阻塞的 API 调用。WSACleanup() 从底层的 Windows Sockets DLL 中撤销注

册。

WSAGetLastError() 得到最近的一个 Windows Sockets API 调用错误的详细情况。

WSAIsBlocking() 确定底层的 Windows Sockets DLL 是否在该线程已经被一个调用阻塞。

WSASetBlockingHook() 为底层的 Windows Sockets 实现设置阻塞钩子。

WSASetLastError() 设置下一次 WSAGetLastError()返回的错误信息。

WSAStartup() 初始化底层的 Windows Sockets DLL。WSAUnhookBlockingHook() 恢复原始的阻塞钩子。

异步选择机制

WSAAsyncSelect()调用允许应用程序程序注册一个或多个感兴趣的网络事件。这一 API 调用用来取代探寻网络 I/O 调用。在 select()或非阻塞 I/O 例程

(例如 send()和 recv())已经被调用或将要被调用的情况下都可以使用WSAAsyncSelect()调用。在这种情况下,在声明感兴趣的网络事件时,你必须提供一个通知时使用的窗口句柄。那么在你声明的感兴趣的网络事件发生时,对应的窗口将收到一个基于消息的通知。

Windows Sockets 允许对于一特定的套接口声明如下感兴趣的网络事件:

*套接口已准备读数据。

*套接口已准备写数据。

*带外数据准备好。

*套接口准备接收连接请求。

*非阻塞连接操作结束。

*连接关闭。

异步支持例程

异步数据库函数允许应用程序用异步方式请求信息。某些网络实现和/或配置,需要通过执行网络操作来应答这些请求。WSAAsyncGetXByY()函数允许应用程序开发者不必象在使用 Berkeley 标准函数时阻塞整个 Windows 环境。WSACancelAsyncRequest()函数可以允许一个应用程序取消任何未完成的异步的WSAAsyncGetXByY()请求。

阻塞钩子函数方法

正如 4.1.1 节所讲述的,Windows Sockets 实现以这样一种方式阻塞一个操作,Windows 消息处理可以继续,发出调用的应用程序仍然可以收到 Windows 消息。但在某些情况下,应用程序可能希望影响或改变这种伪阻塞的实现方式。WSASetBlockingHook()函数就提供了这样一种功能。它使得应用程序可以替换Windows Sockets 实现在“阻塞”操作中放弃处理器时调用的例程。

错误处理

为了与基于线程的环境兼容,API 调用的错误细节可以通过WSAGetLastError()调用得到。虽然已经为大家接收的 Berkeley 风格的机制是通过"errno"得到关于套接口的网络错误的,这种机制不能够保证在多线程环境中错误代码的完整性和正确性。WSAGetLastError()允许程序员能够得到对应于每一线程的最近的错误代码。

WSAGetLastError()所返回的错误代码尽量避免与标准的 Microsoft C 错误代码冲突。但是某些 Windows Sockets 例程返回的错误代码是在 Microsoft C 定义的标准错误代码之内的。如果你使用的应用程序开发环境中的错误代码定义与Microsoft C 不同,那么我们建议你使用 Windows Sockets 错误代码前缀"WSA" 来保证准确的检测错误。

这份规范定义了一个推荐的错误代码的集合,而且列举了每一个函数有可能返回的错误。但是某些 Windows Sockets 实现也有可能返回一些在我们列举之外

的错误代码。应用程序应该具备能够处理在每个 API 描述下列举的错误代码之外的错误的能力。然而 Windows Sockets 实现不能返回在附录 4.1 中列举的合法Windows Sockets 错误之外的任何数值。

通过中介 DLL 调用 Windows Sockets DLL

Windows Sockets DLL 既可以直接从应用程序中调用也可以通过中介 DLL 调用。通过中介 DLL 的例子是:使用 Windows Sockets 为应用程序实现一个提供通用网络功能的虚拟网络 API 层,这样的 DLL 可以同时被多个应用程序使用。担这样的 DLL 必须对 WSAStartup()和 WSACleanup()这两个函数非常警惕,它们必须保证在任何一个使用 Windows Sockets 调用的任务前后均调用了 WSAStartup() 和 WSACleanup()。这是因为 Windows Sockets DLL 需要一个对 WSAStartup()的调用来为每个任务初始化其数据结构,也需要一个对 WSACleanup()的调用来释放为任务分配的所有资源。

有至少两种方法去完成这一任务。最简单的方法是中介 DLL 具有与WSAStarup()和 WSACleanup()类似的调用提供给应用程序使用,DLL 将在这些例程中调用 WSAStartup()和 WSACleanup()。另一种机制就是中介 DLL 建立一个任务句柄列表。任务句柄是从 GetCurrentTask()这一个 Windows API 中获得的。在中介 DLL 的每一个入口处检查对于当前任务 WSAStartup()函数是否已被调用,并且在必要的时候调用 WSACleanup()函数。

在 Windows NT 环境中,这一点是没有必要的。因为 Windows NT 中的 DLL 结构与流程是与 Windows 不同的。在 Windows NT 中,中介 DLL 只需简单的在它的DLL 初始化例程中调用 WSAStartup()即可。这个例程将在任何一个新的进程试图使用 DLL 的开始时刻被执行。

如果中介DLL 调用了阻塞操作而又没有安装任何它自己的阻塞钩子,那么DLL 作者必须清楚地认识到控制有可能会通过应用程序安装的阻塞钩子或缺省的阻塞钩子回到应用程序手中。这样应用程序有可能通过 WSACancelBlockingCall() 来取消中介 DLL 的阻塞操作,如果这种情况发生了,中介 DLL 的阻塞操作会失败并返回错误代码 WSAEINTR。这时候,中介 DLL 必须尽快地把控制交还给调用它的任务。因为用户有可能按了 Cancel 或者 Close 按钮。应用程序正在急切地盼望获得 CPU 的控制权。我们推荐中介 DLL 在进行阻塞调用时安装自己的阻塞钩子来防止不可预见的中介 DLL 和应用程序之间的互相影响。

Windows Sockets 实现内部对消息的使用

为了把 Windows Sockets 实现成一个纯粹的 DLL,有时在 DLL 内部互相发送消息来通讯和定时是必要的。这是非常合法的。但是 Windows DLL 不应该无缘无

故地发送消息给一个由客户打开的窗口句柄,除非应用程序要求这些消息。所以为了自身的目的而需要使用消息的 Windows Sockets DLL 打开了一个隐藏的窗口,并且发送必要的消息给这个窗口的句柄。

私有的 API 接口

附录 B.3 中的 WINSOCK.DEF 文件列出了 Windows Sockets API 功能调用的序数。除了已经列出的序数值外,所有小于 999 的序数都是保留给将来的 Windows Sockets 使用的。对于一个 Windows Sockets 实现来说,提供附加的私有的接口也是很方便的。这是完全可以接受的,只要这些调用的序数大于 1000,要注意的是,任何使用了某个特定 Windows Sockets DLL 私有的 API 的应用程序极有可能在任何其他供应商的 Windows Sockets DLL 上无法工作。应该注意到,只有使用在这份规范中定义的 API 才能可以保证每一个 Windows Sockets 实现都支持。

如果一个应用程序使用了某个供应商的 Windows Sockets DLL 的特定接口, 最好不要把应用程序与 DLL 静态连接,而通过 Windows Sockets 例程LoadLibrary()和 GetProcAddress()动态载入,这就使得应用程序在其他不支持同样的扩展功能集的 Windows Sockets DLL 系统上运行时,可以得到适当的错误信息。