📄 cwsbook.txt
字号:
任何能够与Windows Sockets兼容实现协同工作的应用程序就被认为是具有
Windows Sockets接口。我们称这种应用程序为Windows Sockets应用程序。
Windows Sockets规范定义并记录了如何使用API与Internet协议族(IPS,通常
我们指的是TCP/IP)连接,尤其要指出的是所有的Windows Sockets实现都支持流
套接口和数据报套接口.
应用程序调用Windows Sockets的API实现相互之间的通讯。Windows Sockets
又利用下层的网络通讯协议功能和操作系统调用实现实际的通讯工作。它们之间的
关系如图1-1。
虽然我们并不反对使用这一套API来实现另一通讯协议栈(而且我们期望在将
来规范的修改中能够讨论这个问题),但这种用法已经超出了我们这一份规范所规
定的范围,我们在此将不作讨论。
1.2 Bekeley套接口
Windows Sockets规范是建立在Bekeley套接口模型上的。这个模型现在已是
TCP/IP网络的标准。它提供了习惯于UNIX套接口编程的程序员极为熟悉的环境,
并且简化了移植现有的基于套接口的应用程序源代码的工作。Windows Sockets API
也是和4.3BSD的要求一致的。
1.3 Microsoft Windows和针对Windows的扩展
这一套Windows Sockets API能够在所有3.0以上版本的Windows和所有
Windows Scokets实现上使用,所以它不仅为Windwos Sockets实现和Windows Sockets
应用程序提供了16位操作环境,而且也提供了32位操作环境。
Windows Sockets也支持多线程的Windows进程。一个进程包含了一个或多个同
时执行的线程。在Windows 3.1非多线程版本中,一个任务对应了一个仅具有单个
线程的进程。而我们在本书中所提到的线程均是指在多线程Windows环境中的真正
意义的线程。在非多线程环境中(例如Windows 3.0)这个术语是指Windows Sockets
进程.
Windows Sockets规范中的针对Windows的扩展部分为应用程序开发者提供了开
发具有Windows应用软件的功能。它有利于使程序员写出更加稳定并且更加高效的
程序,也有助于在非占先Windows版本中使多个应用程序在多任务情况下更好地运
作。除了WSAStartup()和WSACleanup()两个函数除外,其他的Windows扩展函数的
使用不是强制性的。
1.4 这份规范的地位
Windows Sockets是一份独立的规范。它的产生和存在是为了造益于应用程序开
发者,网络软件供应商和广大计算机用户。这份规范的每一份正式出版的版本(非
草稿)实际上代表了为网络软件供应商实现所需和应用程序开发者所用的一整套
API。关于这套规范的讨论和改进还正在进行之中。这样的讨论主要是通过Internet
上的一个电子邮件论坛-winsock@microdyne.com进行的。同时也有不定期的会议举
行。会议的具体内容会在电子邮件论坛上发表。
1.5 曾经作过的修改
1.5.1 Windows Sockets 1.0
Windows Sockets 1.0代表了网络软件供应商和用户协会细致周到的工作的结
晶。Windows Sockets 1.0规范的发布是为了让网络软件供应商和应用程序开发者能
够开始建立各自的符合Windows Sockets标准的实现和应用程序。
1.5.2 Windows Sockets 1.1
Windows Sockets 1.1继承了Windows Sockets 1.0的准则和结构,并且仅在一些
绝对必要的地方作了改动。这些改动都是基于不少公司在创作Windows Sockets 1.0
实现时的经验和教训的。Windows Scokets 1.1包含了一些更加清晰的说明和对
Windows Sockets 1.0的小改动。此外1.1还包含了如下重大的变更:
* 加入了gethostname()这个常规调用,以便更加简单地得到主机名字和地址。
* 定义DLL中小于1000的序数为Windows Sockets保留,而对大于1000的序
数则没有限制。这使Windows Sockets供应商可以在DLL中加入自己的界面,而不
用担心所选择的序数会和Windows Scokets将来的版本冲突。
* 增加了WSAStartup()函数和WASClearup()函数之间的关联,要求两个函数互
相对应。这使得应用程序开发者和第三方DLL在使用Windows Sockets实现时不需
要考虑其他程序对这套API的调用。
* 把函数intr_addr()的返回类型,从结构in_addr改为了无符号长整型。这个改
变是为了适应Microsoft C编译器和Borland C编译器对返回类型为四字节结构的函
数的不同处理方法。
* 把WSAAsyncSelect()函数语义从“边缘触发”改为“电平触发”。这种方式
大大地简化了一个应用程序对这个函数的调用。
* 改变了ioctlsocket()函数中FIONBIO的语义。如果套接口还有未完成的
WSAAsyncSelect()调用,该函数将失败返回。
* 为了符合RFC 1122,在套接口选项中加入了TCP_NODELAY这一条。
所有Windows Sockets 1.1对于Windows Sockets 1.0的改动在以下都作了记号。
第二章 使用Windows Sockets 1.1编程
在这一章,我们将介绍如何使用Windows Sockets 1.1编程,并讨论了使用
Windows Sockets 1.1编程的一些细节问题。本章的讨论均是基于Windows Sockets 1.1
规范的,在某些方面可能会和第六、七章对Windows Sockets 2的讨论不一致,请读
者注意这一区别。
2.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同时存在并且被一个或者多个应用程序同时调用的。
2.2 套接口
2.2.1 基本概念
通讯的基石是套接口,一个套接口是通讯的一端。在这一端上你可以找到与其
对应的一个名字。一个正在被使用的套接口都有它的类型和与其相关的进程。套接
口存在于通讯域中。通讯域是为了处理一般的线程通过套接口通讯而引进的一种抽
象概念。套接口通常和同一个域中的套接口交换数据(数据交换也可能穿越域的界
限,但这时一定要执行某种解释程序)。Windows Sockets规范支持单一的通讯域,
即Internet域。各种进程使用这个域互相之间用Internet协议族来进行通讯(Windows
Sockets 1.1以上的版本支持其他的域,例如Windows Sockets 2)。
套接口可以根据通讯性质分类;这种性质对于用户是可见的。应用程序一般仅
在同一类的套接口间通讯。不过只要底层的通讯协议允许,不同类型的套接口间也
照样可以通讯。
用户目前可以使用两种套接口,即流套接口和数据报套接口。流套接口提供了
双向的,有序的,无重复并且无记录边界的数据流服务。数据报套接口支持双向的
数据流,但并不保证是可靠,有序,无重复的。也就是说,一个从数据报套接口接
收信息的进程有可能发现信息重复了,或者和发出时的顺序不同。数据报套接口的
一个重要特点是它保留了记录边界。对于这一特点,数据报套接口采用了与现在许
多包交换网络(例如以太网)非常类似的模型。
2.2.2 客户机/服务器模型
一个在建立分布式应用时最常用的范例便是客户机/服务器模型。在这种方案中
客户应用程序向服务器程序请求服务。这种方式隐含了在建立客户机/服务器间通讯
时的非对称性。客户机/服务器模型工作时要求有一套为客户机和服务器所共识的惯
例来保证服务能够被提供(或被接受)。这一套惯例包含了一套协议。它必须在通
讯的两头都被实现。根据不同的实际情况,协议可能是对称的或是非对称的。在对
称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变地
认为是主机,而另一方则是从机。一个对称协议的例子是Internet中用于终端仿真的
TELNET。而非对称协议的例子是Internet中的FTP。无论具体的协议是对称的或是
非对称的,当服务被提供时必然存在“客户进程”和“服务进程”。
一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务
进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个
时刻,服务程序被“惊醒”并且为客户提供服务-对客户的请求作出适当的反应。
这一请求/相应的过程可以简单的用图2-1表示。虽然基于连接的服务是设计客户机
/服务器应用程序时的标准,但有些服务也是可以通过数据报套接口提供的。
2.2.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()函数可以用于处理对带外数据到来的通知。
2.2.4 广播
数据报套接口可以用来向许多系统支持的网络发送广播数据包。要实现这种功
能,网络本身必须支持广播功能,因为系统软件并不提供对广播功能的任何模拟。
广播信息将会给网络造成极重的负担,因为它们要求网络上的每台主机都为它们服
务,所以发送广播数据包的能力被限制于那些用显式标记了允许广播的套接口中。
广播通常是为了如下两个原因而使用的:1. 一个应用程序希望在本地网络中找到一
个资源,而应用程序对该资源的地址又没有任何先验的知识。2. 一些重要的功能,
例如路由要求把它们的信息发送给所有可以找到的邻机。
被广播信息的目的地址取决于这一信息将在何种网络上广播。Internet域中支持
一个速记地址用于广播-INADDR_BROADCAST。由于使用广播以前必须捆绑一个
数据报套接口,所以所有收到的广播消息都带有发送者的地址和端口。
某些类型的网络支持多种广播的概念。例如IEEE802.5令牌环结构便支持链接
层广播指示,它用来控制广播数据是否通过桥接器发送。Windows Sockets规范没有
提供任何机制用来判断某个应用程序是基于何种网络之上的,而且也没有任何办法
来控制广播的语义。
2.3 字节顺序
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实现有可能在主机字节顺
序与网络字节顺序相同的机器上运行。因此只有使用标准的转换函数的应用程序是
可移植的。
2.4 套接口属性选项
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 为0
FAR *
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()时返回相应的数值,但事实上并没有在任何地方
使用它。
2.5 数据库文件
getXbyY()和WSAAyncGetXByY()这一类的例程是用来得到某种特殊的网络信
息的。getXbyY()例程最初(在第一版的BERKELY UNIX中)是被设计成一种在文
本数据库中查询信息的机制。虽然Windows Sockets实现可能用不同的方式来得到这
些信息,但Windows Sockets应用程序要求通过getXbyY()或WSAAyncGetXByY()这
一类例程得到的信息是一致。
2.6 与Berkeley套接口的不同
有一些很有限的地方,Windows Sockets API必须与从严格地坚持Berkeley传统
风格中解放出来。通常这么做是因为在Windows环境中实现的难度。
2.6.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.6.2 select()函数和FD_*宏
由于一个套接口不再表示了UNIX风格的小的非负的整数,select()函数在
Windows Sockets API中的实现有一些变化:每一组套接口仍然用fd_set类型来代
表,但是它并不是一个位掩码。整个组的套接口是用了一个套接口的数组来实现的。
为了避免潜在的危险,应用程序应该坚持用FD_XXX宏来设置,初始化,清除和检
查fd_set结构。
2.6.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 /* 但请见下文 */
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -