Linux 和 Windows 套接字之间的互相操作天衣无缝,并且在编写两种系统上都可以编译的程序方面也不会面临太大的编程挑战。
套接字有许多形式:
- 流
- 数据报
- 原始数据
- 顺序信息包
- 可靠传递的消息
为传送大量数据,虚拟线路是最佳选择,而套接字流就是一种虚拟线路。我们将在这一部分中注重流类型的套接字。
简单而言,客户机创建一个套接字,并尝试与已知的端点连接。服务器创建一个套接字,将其与端点名称(给它一个名称)绑定,然后等待连接。当客户机连接,而服务器接收连接时,数据通信就分别由这两个端点开始了。套接字支持双向的数据传送。
Windows 和 Linux 都支持 Berkeley 样式的套接字。Windows 还支持“Windows 套接字(Windows Sockets)”。Windows 上两个版本的套接字都需要下列初始化代码:
清单 1. WSAStartup
|
这里, 2
是版本。可以使用任何非零数作为第一个参数。(第一个参数是一个无符号短型整数。)
与 vanilla Berkeley 套接字接口相比,我不太清楚 Windows 套接字接口是否必需。Windows 套接字好象支持其它的传送协议。但是,随着因特网及其 TCP/IP 协议被一致接受,我不知道“Windows 套接字”额外的复杂功能有什么价值。
有一个例外,我已经使用 Berkeley 样式的接口在本文中编写了这个程序。如果存在应该首选 WSA 接口而不是 Berkeley 样式的接口的根本原因,那是我还不了解。
|
使用 Linux 和 Windows 上支持的 socket()
API 创建套接字:
清单 2. socket() API
|
af
是地址系列,我使用 AF_INET
。 type
是 SOCK_STREAM
或 SOCK_DGRAM
,这里我只关注 SOCK_STREAM
。 protocol
是从 Linux 的 /etc/protocols 文件和 Windows 的 /winnt/system32/drivers/etc/protocol 文件中选择的一个数。我坚持用 0,它是这两个系统上的 IP 协议。
Windows 还有一个使用如下所述 API 的名为“Windows 套接字”的 Microsoft 专有接口:
清单 3. WSASocket 接口
|
(对于 Linux 和非 Windows 程序员,Microsoft DWORD
只是一个无符号长型整数。) 前三个参数与标准的 socket()
接口等同。三个新参数很有趣,它们为测试提供了丰富的背景。第一个额外参数是 lpProtocolInfo
指针。由 lpProtocolInfo
引用的结构是:
清单 4. WSAPROTOCOL_INFO 结构
|
保留 GROUP g
。不过,对它的值没有任何限制。两个参数 lpProtocolInfo
和 dwFlags
引入了编程的巨大复杂性。我对全面测试 WSASocket()
API 所需的测试程序数量进行了估算。例如, WSAPROTOCOL_INFO
结构显示了下列保守估计下的合法值数目。
注:下面的值(x n)显示每个参数可能的合法值数目。这些都是在我保守地假设只认为 szProtocol
字符串设置具有一个可能的合法值的情况,而实际上,字符串可以是任何最长 255 个字符的字符串。我的假设看来与文档没有冲突。“--?”表示我不知道该参数的有关文档或者该参数的用途是什么。我使用了 2001 年 2 月和 6 月的“平台 SDK(Platform SDK)”。
表 1. 对 WASPROTOCOL_INFO 的合法值数目的保守估计
dwServiceFlags1 | 位字段。已定义了 19 位。2^19 = 512K 个合法值。(x 524,288) |
dwServiceFlags2 | 保留(x 1) |
dwServiceFlags3 | 保留(x 1) |
dwServiceFlags4 | 保留(x 1) |
ProviderId | 一个 GUID,消除提供相同协议的多个供应商之间的歧义。(x 1) |
dwCatalogEntryId | 由 WS2_32.DLL 为每个 WSAPROTOCOL_INFO 结构指定的唯一标识。(x 1) |
ProtocolChain | 由 7 个项组成的结构。该结构表示在基本协议的顶部由一个或多个协议构成的协议链。(x 1) |
iVersion | 协议版本标识符。(x 1) |
iAddressFamily | 地址系列。大概与 WSASocket 接口中的相同。(x 1) |
iMaxSockAddr | “最大地址长度”--? (x 1) |
iMinSockAddr | “最小地址长度”--? (x 1) |
iSocketType | 套接字类型。2 个值,但参数已在 socket() API 中说明。(x 1) |
iProtocol | 与 socket() API 中的相同。我们将只考虑一个(x 1) |
iProtocolMaxOffset | 特定于 Windows --? (x 1) |
iNetworkByteOrder | BIGENDIAN 或 LITTLEENDIAN(x 2) |
iSecurityScheme | 只定义了一个。(x 1) |
dwMessageSize | 最大消息大小。定义了三个特殊值,加上任何实际协议支持的值。(x 3) |
dwProviderReserved | 保留。 |
szProtocol[WSAPROTOCOL_LEN+1] | 可能是一个标识协议的 Unicode 字符数组。(x 1) |
dwFlags
参数有 5 个已定义的位字段。
WSA_FLAG_OVERLAPPED
WSA_FLAG_MULTIPOINT_C_ROOT
WSA_FLAG_MULTIPOINT_C_LEAF
WSA_FLAG_MULTIPOINT_D_ROOT
WSA_FLAG_MULTIPOINT_D_LEAF
dwFlags
- 选项 2^5 = (x 32)
复杂性问题是一个实际问题。程序是用现有文档编写的,程序员希望文档都是正确的。如果象 sockets()
API 这样的简单 API 因附加参数化而变得复杂,而且不太可能会全面测试附加参数空间,那么在使用更大参数空间前,应该要有令人信服的理由。劝告程序员最好跟着主线走。Microsoft 和 Linux 不会在简单的套接字 open/connect/send-recv/close 情况中犯错误。但是,很少会全面测试复杂 API 中较隐秘的角落。 尝试使 API 中的隐秘特性都会按文档指明的那样执行,会惊人地浪费大量时间。几乎在所有情况中,某些程序员只要稍许多做一些,就可使他或她避免走上未经测试的复杂 API() 的道路。
在这一思想前提下,我计算了 WSASocket() API 的附加参数空间的大小。这里发现的复杂度是在“Berkeley 套接字(Berkeley Sockets)”接口中仍存在的参数化以外的部分。根据上面的可能性,看来有
32 * 3 * 2 * 524,288 = 100,663,296
个调用组合可能出现在 Windows 操作系统。这个数不包括任何对 WSASocket()
API 调用的前三个参数的参数化。对我来说,即使是理解如何能测试或验证这些参数化的实际子集,都是困难的。我承认,我不知道其中一些参数的用法,甚至它们的含义。针对这一专栏的目的,除 非读者能够显示如何通过使用 WSA 套接字的更隐秘接口来改进程序,我是避免使用这些接口的。
为回避“预言灾难的人却遭受了两次灾难”这一警语,我使用了 WSASocket()
API,但是利用了 NULL WSAProtocol_Info
结构,而且只使用 WSA_FLAG_OVERLAPPED
位。仅当 WSASend
、 WSASendTo
等的参数化反映了重叠的 IO 请求时,才证明 WSA_FLAG_OVERLAPPED
是有意义的。我也不使用它们。因此,我用于“Windows 套 接字”(WSA 接口)的参数化与用于标准 Berkeley 样式的接口的参数化一样。那些对 WSA 接口和重叠 IO 问题有较好理解的人可能要尝试一下,看看您是否可以改进这里出现的性能数。
这里是为 Windows 和 Linux 创建套接字的代码:
清单 5. WSASocket 接口
|
代码片断包含了 BADSOCK
的定义,以方便阅读。在实际程序中,它们与其它特定于平台的定义捆在一起。
|
一旦我们过了准备阶段,Windows 和 Linux 上的套接字编程就相当类似了。这里是执行与侦听器连接的客户机代码:
清单 6. 连接
|
不需要条件定义。父进程创建套接字,并等待用该代码连接。
清单 7. 接受连接
|
这也不需要条件定义。最后,传送和接收数据。
|
发送数据的代码是:
清单 8. 套接字 send() 操作
|
和
清单 9. 套接字 recv() 操作
|
|
Sockspeedp6 是 sockspeed 程序的第 6 版。它获得了定时测试能力(请参阅我的 上一专栏),因此永远不会用它来生成性能数。 Sockspeedp6.cpp创建一个子进程,并将数据发送给它。 因为它是面向进程的,所以父进程和子进程都可以单独启动。这一特性使您能够在一个机器上启动父进程,在另一个上启动子进程。这正是使用进程而不是线程的原因。它的用法消息显示如下:
sockspeedp6 用法消息
|
Sockspeedp6.cpp 完全可以在 Red Hat 7.2 和 Windows 2000/Visual Studio 6.0 上编译。
编译器 | 版本 |
Windows/Microsoft C/C++ | 用于 80x86 的版本 12.00.8804 |
Linux/GNU C/C++ | 2.96 |
Sockspeed6 用来查看在一台机器上从一个进程到另一个进程的数据传输率。生成的值应该为我们提供了一个很好的想法:底层联网代码在独立于媒体速度的情况下可以传送数据的速度是多少。我研究了从 16 字节到 1 兆字节的传送大小。我没有研究无延迟选项(关闭 Nagel 算法)、非阻塞选项或者更改接收或发送缓冲区大小。这些研究将在另一个专栏中介绍。
sockspeedp6.cpp 表示的编程实践是直接明了的。 如果有更好的 Windows 或 Linux 编程技术,我很愿意了解(请使用 论坛),并将它们编码到 sockspeedp6.cpp 中以演示改进之处。Sockspeedp6 根据过去的经验不断改进。它在每个 send()
操作前更改内存,并读取 recv()
操作所获得的所有内存。正如提到的那样,它还使用我的 上一专栏中讲到的定时测试技术。
|
所有测试都是在 576 MB 内存和 18 GB 磁盘的 IBM ThinkPad 600X 上运行的。系统引导所有三个操作系统。图 1 显示了 Windows 2000 Advanced Server、Windows XP Professional 和 Red Hat 7.2(Linux 2.4.2)的结果。
在进行这里提到的开发和评测期间,我注意到 Windows 2000 在使用本地主机 IP 地址 127.0.0.1 时好象执行得好些。我使用本地主机 IP 地址重新对这三个平台进行测试。图 2 显示了这次的结果。对 127.0.0.1 的测试,我只是用图 1 上部的标记绘制了几条很粗糙的线。它显示 Windows 2000 真的在 127.0.0.1 地址上数据传送得比较快。但是,它也显示了 Windows XP 好象已除去了这一特性。
结果显示 Red Hat 7.2 提供了一个极为快速的套接字实现。使用这里演示的编码技术,Linux 达到的传送速率比任何一种 Windows 平台要快 2.5 倍。这些编码技术对于 Windows 是否是最理想的呢?我不知道这个问题的答案。我确实知道许多程序都用类似于 sockspeedp6 程序中包含的 send() 和 recv() 循环那样的简单循环编写。如果有改进 Windows 性能的技术,我和这里的读者都很愿意在 论坛中了解这些技术。
我们可以把这些数字与上一专栏中为管道生成的数字相比较。Linux“访问”内存的最大速度达到 400 MB/sec。在同一个图(我 上一专栏中的图 3)中,Windows“访问”内存的最大速度为 100 MB/sec,Linux 是它 的 4 倍。经过比较管道和套接字,我们发现 Linux 管道的速度比 Linux 套接字的速度将近快 5 倍,而 Windows 管道的速度比 Windows 套接字的速度快 3 倍。
以后的专栏中,我们将研究套接字的参数化,以努力找到更快的传送速率。在以后的专栏中,我们还会研究在物理介质上使用套接字。
有了从这次学习得到的知识,我们就能够预料下列问题的答案了: 如果当前没有使用介质的系统可以以 X MB/sec 的速度传送数据,则需要多少个 100 Mb/sec 适配器满足 CPU 的需要呢?一个简单的推测是:100 Mb/sec 相应于每秒 10 兆字节。将无介质的最大传送速率除以 10,您就可以得到满足 CPU 需要所需的适配器数量。这是一个前端推测。以后的专栏将判断这一推测是否合理。
|
我编写了一个程序 sockspeedp6.cpp
和一个 shell 脚本 sockspeedp6-sh.sh
来演示没有使用实际介质的情况下,Windows 和 Linux 上套接字的用法并测量它们的性能。 这些结果显示:通过套接字传送数据,Windows(两个版本)要比 Linux(Red Hat 7.2)慢得多。