摘要

  • Windows Sockets 1.1 为 Windows 程序员提供了基于 Berkeley 模型的 TCP/IP 接口,却不能把 Berkeley 的阻塞行为原样搬进 16 位 Windows。
  • 被阻塞的 Winsock 调用仍须派发 Windows 消息,才能让协作式桌面继续响应;代价是嵌套调用、重入和缓冲区生命周期都受到严格限制。

套接字本来要把网络藏起来

套接字给程序提供一套稳定的收发方法,让应用不用绑定到底层网络栈的供应商。Windows 开发者想要的就是这笔交易。他们也想从熟悉的模型开始:在 Unix 上,Berkeley 套接字早已是 TCP/IP 编程的通行方式。

Windows Sockets 项目始于 1991 年 10 月 10 日在圣何塞 Interop ’91 举行的一场 Birds of a Feather 讨论会。1.1 规范记下了此后的过程:成立委员会、通过邮件争论、起草初稿——初稿大量吸收了 JSB 和 NetManage 的提案——12 月在 Microsoft 举行会议,随后由工作组继续完善文档。这是一套多人共同制定的接口,不是某个厂商强加的 TCP/IP 协议。

1.0 版先行发布,让网络软件供应商和应用开发者可以按一份共同约定开始制作实现和程序。标注为 1993 年 1 月 20 日的 1.1 版,则根据多家公司实现 1.0 的经验补充澄清和小幅修正。这个顺序很重要:在下一版文字定稿前,规范已经先经受了代码的检验。

规范采用 Berkeley 套接字模型,是为了让 Unix 程序员觉得熟悉,也让源代码移植更容易。它定义的是 Internet 协议套件,包括 TCP 流式套接字和 UDP 数据报套接字。它没有假装 1.1 版能标准化所有可能的网络。这个边界有用:开发者得到熟悉的接口,但文件没有承诺接口下的所有系统都会以相同方式运行。

然后 recv 遇到了 16 位桌面

麻烦出现在程序等待时。阻塞式 recv 会一直等到数据到达,或操作以其他方式结束。在支持抢占式调度的系统上,进程等待期间,操作系统可以调度其他任务。Windows Sockets 1.1 不能假定 16 位 Windows 具备这种调度方式。

Microsoft 的文档把补救办法写得很具体。如果阻塞操作无法立即完成,服务提供程序会先启动操作,再进入一个循环:派发 Windows 消息、检查套接字调用是否结束,然后继续检查。这样,网络读取尚未完成时,基于消息运行的应用仍能处理窗口事件。

这并没有把 16 位程序变成抢占式进程。它只是把 Windows 消息派发塞进了一次原本可能被程序员视作“等待”的调用。等待期间抵达的消息可能触发应用再次调用 Winsock。1.1 版不允许这种嵌套:一个任务同一时刻只能有一个未完成的 Winsock 函数调用。再次嵌套调用会以 WSAEINPROGRESS 失败。

API 提供了两个小出口。WSAIsBlocking 可以检查调用是否正在阻塞,WSACancelBlockingCall 可以取消它。多文档窗口等更复杂的应用,则可安装 WSASetBlockingHook,替换默认的消息派发例程。应用由此取得更多控制,也必须自己保证消息处理正确。

接口把工作挪了位置,没有把它抹掉

非阻塞和异步调用避开了阻塞钩子带来的重入问题,却带来另一项义务。如果操作保留了某个缓冲区或其他内存对象的指针,应用就必须让这个对象一直有效,直到操作完成。网络库无法替应用决定内存何时可以释放或重用。

熟悉的 API 与可移植程序之间,隔着一道很小却关键的缝。函数名一致可以降低开始移植的成本,却不会抹去主机的调度模型、消息循环或内存生命周期。Microsoft 后来记录了为方便移植 BSD 源代码而提供的头文件;Winsock 2 引入 Winsock2.h 后,旧的 Winsock.h 声明仍为现有应用保留。兼容性本身也成了需要继续维护的软件。

1.1 版把边界画在接口及其支持的 TCP/IP 套接字类型上。供应商提供实现;应用开发者决定如何等待、取消、派发消息并保护内存。共同规范让这些组件更容易连接,却没有让各自的责任变得可以互换。

真正值得注意的工程选择,不是 Windows 照搬了 Berkeley,而是工作组保留了足够多 Berkeley 的编程语言,让移植变得有吸引力,同时明确写下 Windows 无法假装成 Unix 的地方。消息循环就是那张账单:开放接口降低了一项成本,也准确指出下一项成本落在哪里。

来源