使用Windows Sockets 1.1编程

使用Windows Sockets 1.1编程

Windows Sockets协议栈安装检查
任何一个与 Windows Sockets Import Library 联接的应用程序只需简单地调用 WSAStartup() 函数便可检测系统中有没有一个或多个 Windows Sockets 实现。而对于一个稍微聪明一些的应用程序来说,它会检查 PATH 环境变量来寻找有没有 Windows Sockets 实现的实例( Windows Sockets.DLL )。对于每一个实例,应用程序可以发出一个 LoadLibrary() 调用并且用 WSAStartup() 函数来得到这个实现的具体数据。
这一版本的 Windows Sockets 规范并没有试图明确地讨论多个并发的 Windows Sockets 实现共同工作的情况。但这个规范中没有任何规定可以被解释成是限制多个 Windows Sockets DLL 同时存在并且被一个或者多个应用程序同时调用的。
套接口
(1)基本概念
通讯的基石是套接口,一个套接口是通讯的一端。在这一端上你可以找到与其对应的一个名字。一个正在被使用的套接口都有它的类型和与其相关的进程。套接口存在于通讯域中。通讯域是为了处理一般的线程通过套接口通讯而引进的一种抽象概念。套接口通常和同一个域中的套接口交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。 Windows Sockets 规范支持单一的通讯域,即 Internet 域。各种进程使用这个域互相之间用 Internet 协议族来进行通讯( Windows Sockets 1.1 以上的版本支持其他的域,例如 Windows Sockets 2 )。
套接口可以根据通讯性质分类;这种性质对于用户是可见的。应用程序一般仅在同一类的套接口间通讯。不过只要底层的通讯协议允许,不同类型的套接口间也照样可以通讯。
用户目前可以使用两种套接口,即流套接口和数据报套接口。流套接口提供了双向的,有序的,无重复并且无记录边界的数据流服务。数据报套接口支持双向的数据流,但并不保证是可靠,有序,无重复的。也就是说,一个从数据报套接口接收信息的进程有可能发现信息重复了,或者和发出时的顺序不同。数据报套接口的一个重要特点是它保留了记录边界。对于这一特点,数据报套接口采用了与现在许多包交换网络(例如以太网)非常类似的模型。
(2)客户机/服务器模型
一个在建立分布式应用时最常用的范例便是客户机 / 服务器模型。在这种方案中客户应用程序向服务器程序请求服务。这种方式隐含了在建立客户机 / 服务器间通讯时的非对称性。客户机 / 服务器模型工作时要求有一套为客户机和服务器所共识的惯例来保证服务能够被提供(或被接受)。这一套惯例包含了一套协议。它必须在通讯的两头都被实现。根据不同的实际情况,协议可能是对称的或是非对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变地认为是主机,而另一方则是从机。一个对称协议的例子是 Internet 中用于终端仿真的 TELNET 。而非对称协议的例子是 Internet 中的 FTP 。无论具体的协议是对称的或是非对称的,当服务被提供时必然存在“客户进程”和“服务进程”。
一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程序被“惊醒”并且为客户提供服务-对客户的请求作出适当的反应。这一请求 / 相应的过程可以简单的用图 2-1 表示。虽然基于连接的服务是设计客户机 / 服务器应用程序时的标准,但有些服务也是可以通过数据报套接口提供的。
使用Windows Sockets 1.1编程

(3)带外数据
注意:以下对于带外数据(也称为 TCP 紧急数据)的讨论,都是基于 BSD 模型而言的。用户和实现者必须注意,目前有两种互相矛盾的关于 RFC 793 的解释,也就是在这基础上,带外数据这一概念才被引入的。而且 BSD 对于带外数据的实现并没有符合 RFC 1122 定下的主机的要求,为了避免互操作时的问题,应用程序开发者最好不要使用带外数据,除非是与某一既成事实的服务互操作时所必须的。 Windows Sockets 提供者也必须提供他们的产品对于带外数据实现的语义的文挡(采用 BSD 方式或者是 RFC 1122 方式)。规定一个特殊的带外数据语义集已经超出了 Windows Sockets 规范的讨论范围。
流套接口的抽象中包括了带外数据这一概念,带外数据是相连的每一对流套接口间一个逻辑上独立的传输通道。带外数据是独立于普通数据传送给用户的,这一抽象要求带外数据设备必须支持每一时刻至少一个带外数据消息被可靠地传送。这一消息可能包含至少一个字节;并且在任何时刻仅有一个带外数据信息等候发送。对于仅支持带内数据的通讯协议来说(例如紧急数据是与普通数据在同一序列中发送的),系统通常把紧急数据从普通数据中分离出来单独存放。这就允许用户可以在顺序接收紧急数据和非顺序接收紧急数据之间作出选择(非顺序接收时可以省去缓存重叠数据的麻烦)。在这种情况下,用户也可以“偷看一眼”紧急数据。
某一个应用程序也可能喜欢线内处理紧急数据,即把其作为普通数据流的一部分。这可以靠设置套接口选项中的 SO_OOBINLINE 来实现(参见 5.1.21 节, setsockopt() )。在这种情况下,应用程序可能希望确定未读数据中的哪一些是“紧急”的(“紧急”这一术语通常应用于线内带外数据)。为了达到这个目的,在 Windows Sockets 的实现中就要在数据流保留一个逻辑记号来指出带外数据从哪一点开始发送,一个应用程序可以使用 SIOCATMARK ioctlsocket() 命令(参见 5.1.12 节)来确定在记号之前是否还有未读入的数据。应用程序可以使用这一记号与其对方进行重新同步。
WSAAsyncSelect() 函数可以用于处理对带外数据到来的通知。
(4)广播
数据报套接口可以用来向许多系统支持的网络发送广播数据包。要实现这种功能,网络本身必须支持广播功能,因为系统软件并不提供对广播功能的任何模拟。广播信息将会给网络造成极重的负担,因为它们要求网络上的每台主机都为它们服务,所以发送广播数据包的能力被限制于那些用显式标记了允许广播的套接口中。广播通常是为了如下两个原因而使用的: 1. 一个应用程序希望在本地网络中找到一个资源,而应用程序对该资源的地址又没有任何先验的知识。 2. 一些重要的功能,例如路由要求把它们的信息发送给所有可以找到的邻机。
被广播信息的目的地址取决于这一信息将在何种网络上广播。 Internet 域中支持一个速记地址用于广播- INADDR_BROADCAST 。由于使用广播以前必须捆绑一个数据报套接口,所以所有收到的广播消息都带有发送者的地址和端口。
某些类型的网络支持多种广播的概念。例如 IEEE802.5 令牌环结构便支持链接层广播指示,它用来控制广播数据是否通过桥接器发送。 Windows Sockets 规范没有提供任何机制用来判断某个应用程序是基于何种网络之上的,而且也没有任何办法来控制广播的语义。
字节顺序
Intel 处理器的字节顺序是和 DEC VAX 处理器的字节顺序一致的。因此它与 68000 型处理器以及 Internet 的顺序是不同的,所以用户在使用时要特别小心以保证正确的顺序。
任何从 Windows Sockets 函数对 IP 地址和端口号的引用和传送给 Windows Sockets 函数的 IP 地址和端口号均是按照网络顺序组织的,这也包括了 sockaddr_in 结构这一数据类型中的 IP 地址域和端口域(但不包括 sin_family 域)。
考虑到一个应用程序通常用与“时间”服务对应的端口来和服务器连接,而服务器提供某种机制来通知用户使用另一端口。因此 getservbyname() 函数返回的端口号已经是网络顺序了,可以直接用来组成一个地址,而不需要进行转换。然而如果用户输入一个数,而且指定使用这一端口号,应用程序则必须在使用它建立地址以前,把它从主机顺序转换成网络顺序(使用 htons() 函数)。相应地,如果应用程序希望显示包含于某一地址中的端口号(例如从 getpeername() 函数中返回的),这一端口号就必须在被显示前从网络顺序转换到主机顺序(使用 ntohs() 函数)。
由于 Intel 处理器和 Internet 的字节顺序是不同的,上述的转换是无法避免的,应用程序的编写者应该使用作为 Windows Sockets API 一部分的标准的转换函数,而不要使用自己的转换函数代码。因为将来的 Windows Sockets 实现有可能在主机字节顺序与网络字节顺序相同的机器上运行。因此只有使用标准的转换函数的应用程序是可移植的。
套接口属性选项
Windows Sockets 规范支持的套接口属性选项都列在对 setsockopt() 函数和 getsockopt() 函数的叙述中。任何一个 Windows Sockets 实现必须能够识别所有这些属性选项,并且对每一个属性选项都返回合理的数值。每一个属性选项的缺省值列在下表中:
选项 类型 含义 缺省值 注意事项
SO_ACCEPTCON BOOL 套接口正在监听。 FALSE
SO_BROADCAST BOOL 套接口被设置为可以 FALSE 发送广播数据。
SO_DEBUG BOOL 允许 Debug FALSE(*)
S0_DONTLINGER BOOL 如果为真, SO_LINGER 选项被禁止 TRUE
SO_DONTROUTE BOOL 路由被禁止。 FALSE (*)
SO_ERROR int 得到并且清除错误状态。 0
SO_KEEPALIVE BOOL 活跃信息正在被发送。 FALSE
SO_LINGER struct 返回目前的 linger 信息。 l_onoff linger 0FAR *
SO_OOBINLINE BOOL 带外数据正在普通数据流中被接收。 FALSE
SO_RCVBUF int 接收缓冲区大小。 决定于实现 (*)
SO_REUSEADDR BOOL 该套接口捆绑的地址 FALSE 是否可被其他人使用。
SO_SNDBUF int 发送缓冲区大小。 决定于实现 (*)
SO_TYPE int 套接口类型(如 SOCK_STREAM )。 和套接口被创建时一致
TCP_NODELAY BOOL 禁止采用 Nagle 决定于实现进行合并传送。
(*) Windows Sockets 实现有可能在用户调用 setsockopt() 函数时忽略这些属性,并且在用户调用 getsockopt() 函数时返回一个没有变化的值。或者它可能在 setsockopt() 时接受某个值,并且在 getsockopt() 时返回相应的数值,但事实上并没有在任何地方使用它。
数据库文件
getXbyY() WSAAyncGetXByY() 这一类的例程是用来得到某种特殊的网络信息的。 getXbyY() 例程最初(在第一版的 BERKELY UNIX 中)是被设计成一种在文本数据库中查询信息的机制。虽然 Windows Sockets 实现可能用不同的方式来得到这些信息,但 Windows Sockets 应用程序要求通过 getXbyY() WSAAyncGetXByY() 这一类例程得到的信息是一致。
Berkeley套接口的不同
有一些很有限的地方, Windows Sockets API 必须与从严格地坚持 Berkeley 传统风格中解放出来。通常这么做是因为在 Windows 环境中实现的难度。
(1)套接口数据类型和错误数值
Windows Sockets 规范中定义了一个新的数据类型 SOCKET ,这一类型的定义对于将来 Windows Sockets 规范的升级是必要的。例如在 Windows NT 中把套接口作为文件句柄来使用。这一类型的定义也保证了应用程序向 Win/32 环境的可移植性。因为这一类型会自动地从 16 位升级到 32 位。
UNIX 中所有句柄包括套接口句柄,都是非负的短整数,而且一些应用程序把这一假设视为真理。 Windows Sockets 句柄则没有这一限制,除了 INVALID_SOCKET 不是一个有效的套接口外,套接口可以取从 0 INVALID_SOCKET-1 之间的任意值。
因为 SOCKET 类型是 unsigned ,所以编译已经存在于 UNIX 环境中的应用程序的源代码可能会导致 signed/unsigned 数据类型不匹配的警告。
这还意味着,在 socket() 例程和 accept() 例程返回时,检查是否有错误发生就不应该再使用把返回值和 -1 比较的方法,或判断返回值是否为负(这两种方法在 BSD 中都是很普通,很合法的途径)。取而代之的是,一个应用程序应该使用常量 INVALID_SOCKET ,该常量已在 WINSOCK.H 中定义。
例如:
典型的 BSD 风格:
s = socket(...);
if (s == -1) /* of s<0 */
{...}
更优良的风格:
s = socket(...);
if (s == INVALID_SOCKET)
{...}
(2)select()函数和FD_*
由于一个套接口不再表示了 UNIX 风格的小的非负的整数, select() 函数在 Windows Sockets API 中的实现有一些变化:每一组套接口仍然用 fd_set 类型来代表,但是它并不是一个位掩码。整个组的套接口是用了一个套接口的数组来实现的。为了避免潜在的危险,应用程序应该坚持用 FD_XXX 宏来设置,初始化,清除和检查 fd_set 结构。
(3)错误代码-errno,h_errno,WSAGetLastError()
Windows Sockets 实现所设置的错误代码是无法通过 errno 变量得到的。另外对于 getXbyY() 这一类的函数,错误代码无法从 h_errno 变量得到。错误代码可以使用 WSAGetLastError() 调用得到。这一函数在 5.3.11 中讨论。这个函数在 Windows Sockets 实现中是作为 WIN/32 函数 GetLastError() 的先导函数(最终是一个别名)。这样做是为了在多线程的进程中为每一线程得到自己的错误信息提供可靠的保障。
为了保持与 BSD 的兼容性,应用程序可以加入以下一行代码:
#define errno WSAGetLastError()
这就保证了用全程的 errno 变量所写的网络程序代码在单线程环境中可以正确使用。当然,这样做有许多明显的缺点:如果一个原程序包含了一段代码对套接口和非套接口函数都用 errno 变量来检查错误,那么这种机制将无法工作。此外,一个应用程序不可能为 errno 赋一个新的值(在 Windows Sockets 中, WSASetLastError() 函数可以做到这一点)。
例如:
典型的 BSD 风格:
r = recv(...);
if (r == -1/* 但请见下文 */
&& errno == EWOULDBLOCK)
{...}
更优良的风格:
r = recv(...);
if (r == -1/* 但请见下文 */
&& WSAGetLastError() == EWOULDBLOCK)
{...}
虽然为了兼容性原因,错误常量与 4.3BSD 所提供的一致;应用程序应该尽可能地使用“ WSA ”系列错误代码定义。例如,一个更准确的上面程序片断的版本应该是:
r = recv(...);
if (r == -1/* 但请见下文 */
&& WSAGetLastError() == WSAEWOULDBLOCK)
{...}
(4)指针
所有应用程序与 Windows Sockets 使用的指针都必须是 FAR 指针,为了方便应用程序开发者使用, Windows Sockets 规范定义了数据类型 LPHOSTENT
(5)重命名的函数
有两种原因 Berkeley 套接口中的函数必须重命名以避免与其他的 API 冲突:
close()closesocket()
Berkeley 套接口中,套接口出现的形式与标准文件描述字相同,所以 close() 函数可以用来和关闭正规文件一样来关闭套接口。虽然在 Windows Sockets API 中,没有任何规定阻碍 Windows Sockets 实现用文件句柄来标识套接口,但是也没有任何规定要求这么做。套接口描述字并不认为是和正常文件句柄对应的 , 而且并不能认为文件操作,例如 read() write() close() 在应用于套接口后不能保证正确工作。套接口必须使用 closesocket() 例程来关闭,用 close() 例程来关闭套接口是不正确的,这样做的效果对于 Windows Sockets 规范说来也是未知的。
ioctl()iooctlsocket()
许多 C 语言的运行时系统出于与 Windows Sockets 无关的目的使用 ioctl() 例程,所以 Windows Sockets 定义 ioctlsocket() 例程。它被用于实现 BSD 中用 ioctl() fcntl() 实现的功能。
(6)阻塞例程和EINPROGRESS
虽然 Windows Sockets 支持关于套接口的阻塞操作,但是这种应用是被强烈反对的 . 如果程序员被迫使用阻塞模式(例如一个准备移植的已有的程序),那么他应该清楚地知道 Windows Sockets 中阻塞操作的语义。有关细节请参见 4.1.1
(7)Windows Sockets支持的最大套接口数目
一个特定的 Windows Sockets 提供者所支持的套接口的最大数目是由实现确定的。任何一个应用程序都不应假设某个待定数目的套接口可用。这一点在 4.3.15 WSAStartup() 中会被重申。而且一个应用程序可以真正使用的套接口的数目和某一特定的实现所支持的数目是完全无关的。
一个 Windows Sockets 应用程序可以使用的套接口的最大数目是在编译时由常量 FD_SETSIZE 决定的。这个常量在 select() 函数(参见 4.1.18 )中被用来组建 fd_set 结构。在 WINSOCK.H 中缺省值是 64 。如果一个应用程序希望能够使用超过 64 个套接口,则编程人员必须在每一个源文件包含 WINSOCK.H 前定义确切的 FD_SET 值。有一种方法可以完成这项工作,就是在工程项目文件中的编译器选项上加入这一定义。例如在使用 Microsoft C 时加入 -D FD_SETSIZE=128 作为编译命令的一个命令行参数 . 要强调的是: FD_SET 定义的值对 Windows Sockets 实现所支持的套接口的数目并无任何影响。
(8)头文件
为了方便基于 Berkeley 套接口的已有的源代码的移植, Windows Sockets 支持许多 Berkeley 头文件。这些 Berkeley 头文件被包含在 WINSOCK.H 中。所以一个 Windows Sockets 应用程序只需简单的包含 WINSOCK.H 就足够了(这也是一种被推荐使用的方法)。
(9)API调用失败时的返回值
常量 SOCKET_ERROR 是被用来检查 API 调用失败的。虽然对这一常量的使用并不是强制性的,但这是推荐的。如下的例子表明了如何使用 SOCKET_ERROR 常量
典型的 BSD 风格:
r = recv(...);
if (r == -1 /* or r < 0 */
&& errno == EWOULDBLOCK)
{...}
更优良的风格:
r = recv(...);
if (r == SOCKET_ERROR
&& WSAGetLastError == WSAEWOULDBLOCK)
{...}
(10)原始套接口
Windows Sockets 规范并没有规定 Windows Sockets DLL 必须支持原始套接口-用 SOCK_RAW 打开的套接口。然而 Windows Sockets 规范鼓励 Windows Sockets DLL 提供原始套接口支持。一个 Windows Sockets 兼容的应用程序在希望使用原始套接口时应该试图用 socket() 调用(参见 5.1.23 节)来打开套接口。如果这么做失败了,应用程序则应该使用其他类型的套接口或向用户报告错误。
在多线程Windows版本中的Windows Sockets
Windows Sockets 接口被设计成既能够在单线程的 Windows 版本(例如 Windows 3.1 )又能够在占先的多线程 Windows 版本(例如 Windows NT )中使用,在多线程环境中,套接口接口基本上是不变的。但多线程应用程序的作者必须知道,在线程之间同步对套接口的使用是应用程序的责任,而不是 Windows Sockets 实现的责任。这一点在其他形式的 I/O 中管理,例如文件 I/O 中是一样的。没有对套接口调用进行同步将导致不可预测的结果。例如,如果有两个线程同时调用同一套接口进行 send() ,那么数据发送的先后顺序就无法保证了。
在一个线程中关闭一个未完成的阻塞的套接口将会导致另一个线程使用同一套接口的阻塞调用出错( WSAEINTER )返回,就象操作被取消一样。这也同样适用于某一个 select() 调用未完成时,应用程序关闭了其中的一个被选择的套接口。
在占先的多线程 Windows 版本中,并没有缺省的阻塞钩子函数。这是因为如果一个单一的应用程序在等待某一操作结束时并不会调用 PeekMessage() GetMessage() 这些会使应用程序产生一个非占先窗口的函数。因此机器在这种情况下不会被阻塞。然而,为了向后的兼容性,在多线程 Windows 版本中, WSASetBlockingHook() 函数也被实现了。任何使用缺省阻塞钩子的应用程序可以安装它们自己的阻塞钩子函数来覆盖缺省的阻塞钩子函数。

你可能感兴趣的:(windows,socket)