现在的位置: 首页 > 综合 > 正文

Writing a Winsock 2 Layered Service Provider(LSP) 译文(zz)

2013年08月15日 ⁄ 综合 ⁄ 共 27881字 ⁄ 字号 评论关闭
Wei Hua, Jim Ohlund, Barry Butterklee 著
来源:http://greatdong.blog.edu.cn
作者:董岩 译
greatdong_2001@163.com
使用分层的 transport service provider 来扩展基本的传输功能的做法可是很厉害的。分层的 service provider 仅实现了高层的自定义通讯功能而且与远程端进行数据交换时还要依赖于现有的下层的 base provider。

本文假定读者熟悉 C++, Winsock, Overlapped I/O。

本文的代码: layered.exe (86KB)

Wei Hua 和 Barry Butterklee 都是 Microsoft Technical Support 的 developer support engineers,专长于网络编程。Jim Ohlund 是 Microsoft Proxy Server team 的 software test engineer。

Winsock 2 中最有趣却最不易理解的特性之一就是 service provider interface (SPI)。与众多书籍、文档、样例所涉及的人们熟知的 Winsock 2 API 不同,Winsock 2 SPI 相对来说尚有待探索。Winsock 2 SPI 由 network transport service providers 和 namespace resolution service providers 实现。Winsock 2 SPI 可用通过实现一个 layered service provider (LSP) 来扩展现有的 transport service provider。例如,Windows 98 和 Windows 2000 上的 quality of service (QOS) 就是以 TCP/IP 协议栈上的 LSP 的形式实现的。其它用到 LSP 的地方还有特殊的 URL 过滤软件,这种软件可以阻止 Web 浏览器访问某站点,无论桌面上安装的是哪种浏览器。

Winsock 2 是在 Windows NT 4.0 发行时推出的;在 Windows 95 ( 是个 add-on 包),Windows 98 和 Windows 2000 上也有。Winsock 2 SPI 的规范可以从 Platform SDK 或 ftp://ftp.microsoft.com/bussys/Winsock/Winsock2 得到。唯一的样例代码就是 Microsoft Platform SDK 里的“layered” 样例。下面我们就来通过深入研究这个样例来揭开 Winsock 2 SPI 的神秘面纱。我们首先来介绍一下所需的背景,然后再研究这个分层样例以及扩展此样例的方法。

Background Information

Winsock 2 与 Windows Open Service Architecture (WOSA) 模型是密不可分的(见 Figure 1)。WOSA 体系允许向其中插入第三方的 service providers 而无需开发人员重写代码也无需替换 Winsock 2 的 DLL,ws2_32.dll。

+-------------++-------------+
| Winsock 2   || Winsock 2   |
| Application || Application |
+-------------++-------------+
|     Winsock 2 API     |
+----------------------------+
|     Winsock 2 DLL       |
| Transport     Namespace |
| Function       Function |
+----------------------------+
| Transport     Namespace |
| API         API     |    
+----------------------------+
| Transport     Namespace |
| Service         Service |
| Provider       Provider |
+----------------------------+

Figure 1 Winsock 2 Architecture

Winsock 2 SPI 使开发者能够开发两种不同类型的 service providers —— transport 和 namespace。Transport providers(通常称之为 协议栈)都是些服务,这些服务提供建立连接、传输数据、进行流控制和差错控制等功能。Namespace providers 的服务在网络协议的寻址属性与一个或多个人性化的名字见建立关联并可进行协议无关的名字解析。SPI 还允许开发者开发两类 transport service providers —— base service providers 和 layered service providers。

Figure 2 Protocols

Base service providers 实现了传输协议的实际细节:建立连接、传送数据、流控制和差错控制。Layered service providers 仅实现了高层的自定义的通讯功能,而且依赖于已有的下层的 base provider 来与远程端进行实际的数据交换(见 Figure 2)。例如,开发者可以在 base TCP/IP 栈的顶端实现一个安全管理器或带宽管理器。只要位于上下层的这些 providers 支持 Winsock 2 SPI,就可以将这些 providers 链接起来。MSDN Platform SDK 里的分层样例是一个一般化的 layered service provider,一旦安装上它,它就位于所有的 base providers 之上。

Winsock 2 当前还不支持 namespace providers 的分层,因此就可以使用 Winsock 2 SPI 来实现一个新的 namespace provider,但是不能改变或扩展已有 namespace provider 的命名、注册和查询行为。

我们这里只讨论开发 layered transport service provider 用到的 SPI 函数,因为 base transport providers 和 namespace providers 一般都可以从操作系统厂商和传输协议栈厂商那里取得。使用分层的 transport service provider 来扩展基本的传输功能的做法是很厉害的。但是应注意的是只有使用 Winsock 接口的应用程序才能尝到所添加的功能所带来的甜头,而使用其它网络接口的程序不会受到影响。当我们使用术语“service provider”,我们实际指的是 transport service provider,这个 transport service provider 既可以是 layered transport service provider 也可以是 base transport service provider。我们使用“LSP”来特指 layered transport service provider。

Figure 3 所示为 Winsock 2 SPI 使用的函数前缀的命名规则。

当编写 service provider 时,导出作入口点的只有 WSPStartup 和 NSPStartup。WSPStartup 和 NSPStartup 实际还提供了一些其它函数的信息,这些函数通过用作参数的一个特殊的 function dispatch table 组成了一个 service provider。

Winsock 2 LSPs 都是以标准的 Windows DLLs 实现的,这些 DLLs 只有一个导出的入口函数,WSPStartup。ws2_32.dll 和 upper chain layered provider 中所有其它的 transport SPI 都是通过 LSP 的 dispatch table 来访问的,即 WSPStartup 函数中的参数 lpProcTable。我们通过 WSPStartup 的参数 UpcallTable 来将 ws2_32.dll 的 upcall dispatch table 送给 LSP。结构体 WSPPROC_TABLE(见 Figure 4 )定义了 LSP 必须实现的函数以及那些函数的入口点要填入 WSPStartup 的 lpProcTable 参数。

int WSPStartup ( WORD wVersionRequested,
LPWSPDATAW lpWSPData,
LPWSAPROTOCOL_INFOW lpProtocolInfo,
WSPUPCALLTABLE UpcallTable,
LPWSPPROC_TABLE lpProcTable );

尽管看上去 LSP 似乎要为各种情况来实现一大堆的函数,但可以直接调用协议链中下一个相应的 function service provider。结构体 WSPUPCALLTABLE(见 Figure 5)定义了 ws2_32.dll 的 upcall 函数们以供 LSP 调用。这些 upcall 函数们都是从 WSPStartup 的 UpcallTable 参数中得到的。WSPUPCALLTABLE 是一个定长的结构体。如果要往 ws2_32.dll 中添加新的 upcall 函数,就需要在 LSP 的 WSPStartup 调用 GetProcAddress 来取得指向新的 upcall 函数的指针。当前只存在一个这样的函数——WPUCompleteOverlappedResult。

LSP Chaining

LSPs 和 base providers 都被串了起来形成了一个协议链。WSAPROTOCOL_INFOW 结构体指的就是整个的协议链。它描述了 layered providers 的连接顺序。后面我们会解释此节构体的细节。

实现 LSP 的 DLL 要么由另一个 LSP 加载,要么直接由 ws2_32.dll 加载,这要看此 LSP 在协议链中的位置。如果 LSP 没处在协议链的顶端,则该 LSP 由上一层的 LSP 加载,否则就由 ws2_ 32.dll 加载(在 layered sample 中,低一层的 chain provider 是在第一个 WSPSocket 调用中加载的)。加载 LSP 后,必须调用该 LSP 的 WSPStartup。最终从 ws2_32.dll 中取得的 UpcallTable 必须向下传递到该 LSP,并且此时必须取得该 LSP 的 lpProcTable。

当 LSP 调用了协议链中的低一层的 provider 的 WSPStartup 时,如果低一层的 provider 又是一个 layered provider,则此链的 WSAPROTOCOL_INFOW 结构体必须传递给 WSPStartup 调用。当低一层是一个 base protocol(链的结尾)时,此链的 WSAPROTOCOL_INFOW 结构体就不再向下传递,而此时必须取得 base provider 的 WSAPROTOCOL_ INFOW 结构体并将其传递给 base provider 的 WSPStartup 调用。因此,此种迹象表明 base provider 并不在协议链中。当专门函数(如 WSPStringToAddress 和 WSPAddressToString)向下一环节传递 WSAPROTOCOL_INFOW 结构体时,情形也是一样的。

必须特别注意保证 WSAPROTOCOL_ INFOW 结构体的 dwProviderReserved 域总是向下传递的,尽管低一层是一个 base provider。这是因为 dwProviderReserved 是由 WSPDuplicateSocket 生成用以保存封包副本的上下文信息,包括 WSPSocket 使用的用以重建复制的 socket 句柄的复制句柄。

还应注意的是 LSP 的 SPI 函数的调用方式都是相同的,与 LSP 在协议链中所处的位置无关。这样,为了简便起见,我们在本文中假定我们 LSP 的客户端为 ws2_32.dll,低一层的 provider 为一个 base provider。

再有,一个 service provider 的 WSPStartup 可以被多次调用。进一步讲,ws2_32.dll 对其所作的每个 WSPStartup 调用都会调用一次 WSPCleanup。因此,每一个 service provider 都应该实现一个每进程都有的引用计数器,此计数器在 WSPStartup 增一,在 WSPCleanup 中减一。当此计数器为零时,service provider 必须做好从内存中卸载的准备。

Mapping Between Winsock 2 API and SPI Functions

当开发者调用了一个 Winsock 2 API 时,ws2_32.dll 最终调用一个相应的 Winsock 2 SPI 函数来使用制定的 service provider 来实现所请求的功能(注意后面讲到的例外情况)。例如,select API 映射到了 WSPSelect SPI,connect 和 WSAConnect APIs 都映射到了 WSPConnect SPI,accept 和 WSAAccept APIs 都映射到了 WSPAccept SPI。但并非所有的 Winsock APIs 都有相应的 SPI。

像 htonl, htons, ntohl 和 ntohs 这样的工具函数只在 ws2_32.dll 中实现,而且并不下传至 service providers。这同样适用于这些函数的 WSA 版本。

inet_addr 和 inet_ntoa 这类的转换函数只在 ws2_32.dll 中实现。

Winsock 1.1 中的所有的 TCP/IP 相关的名字转换和解析函数,如 getXbyY, WSAAsyncGetXByY 和 WSACancelAsyncRequest 以及 gethostname 都在 ws2_32.dll 中实现。

Winsock service provider 的枚举和 blocking hook 相关的函数都在 ws2_32.dll 中实现。因此 WSAEnumProtocols, WSAIsBlocking, WSASetBlockingHook 和 WSAUnhookBlockingHook 都不是 SPI 函数。因为 error codes 是随 SPI 函数一起返回的,所以 SPI 中并不需要等同于 WSAGetLastError 和 WSASetLastError 的函数。

时间对象的操作和等待函数,包括 WSACreateEvent, WSACloseEvent, WSASetEvent, WSAResetEvent 和 WSAWaitForMultipleEvents 都被直接映射到 native Windows operating system services,并不在 SPI 中。

LSP Socket Creation and IFS Handles

socket 句柄有三类:由 base providers 返回给 LSP 的 socket 句柄,由 LSP 返回给 ws2_32.dll 的 socket 句柄以及用户程序中由 ws2_32.dll 返回的句柄。

ws2_32.dll 维护了一个关联列表,表中相关联的是从 LSP 取得的 socket 句柄和返回给用户程序的 socket 句柄。LSP 也应该用类似的做法并维护一个从 base provider 取得的 socket 句柄和返回给 ws2_32.dll 的 socket 句柄的关联列表。这样就使得给定某一层的 socket 句柄,LSP 能找到相应低层的 socket,而且当 LSP 被卸载时还能保证所有的 base socket 句柄能被正确地关闭。

在研究 LSP 应使用哪个函数来生成返回给 ws2_32.dll 的句柄之前,我们还需要先来看一下 Installable File System (IFS)句柄。当 socket 有 IFS 句柄时,此句柄可用在文件 I/O 函数中来完成 Winsock 的 recv 和 send 调用。在 Windows NT 下,IFS 句柄可以加到 I/O completion ports(IOCP)来实现可伸缩性。这是由带有 IFS 句柄的 providers 通过 WSAPROTOCOL_INFOW 结构体中的 XP1_IFS_HANDLES 属性位来指示的。所有 Microsoft 的 base providers 都将 sockets 实现为 IFS 句柄。LSP 不能创建本身是真正的 IFS 句柄的 socket 句柄,因为在 LSP 中不能实现 IFS。然而,由调用 WPUCreateSocketHandle 或 WPUModifyIFSHandle 返回给 ws2_32.dll 的 socket 句柄都可以在文件 I/O 调用中使用。

LSP 通过调用 base provider 的 WSPSocket 来取得 base provider 的 socket 句柄。若 base provider 用的是 IFS 句柄,则 LSP 可以调用 WPUModifyIFSHandle 来生成一个修改过的句柄来返回给 ws2_32.dll。只要考虑操作系统,这个修改过的句柄就与 IFS 没什么区别。实际上,LSP 可以选择在所有内部的处理中只使用修改过的句柄。蹊跷之处是 LSP 不能用 WSPSend (WriteFile), WSPSendTo, WSPRecv (ReadFile), WSPRecvFrom 或 WSPIoctl 进行 overlapped I/O 的后处理。为了使得前面任一调用中的 overlapped I/O 完成后,在 LSP 中进行额外的处理,或是若 base provider 的句柄不是一个 IFS 句柄,WPUCreateSocketHandle 就必须生成一个返回给 ws2_32.dll 的句柄

为了给 LSP 提供方便,在 WPUCreateSocketHandle 中由 LSP 提供的输入参数之一是一个 DWORD 的上下文值。Winsock 2 的 DLL 将这个上下文值与所分配的 socket 句柄间建立关联并使得 LSP 通过 WPUQuerySocketHandleContext 调用能够在任何时刻取得此上下文值。此上下文值一般用于保存一个指向 LSP 所维护的包含着 base provider 的 socket 句柄的数据结构。用 WPUCreateSocketHandle 创建的 socket 句柄与真正的文件系统句柄也没什么区别。然而,任何使用此技术的 layered service provider 仍应将自己标识为 non-IFS provider,这是通过 provider information 结构体中的 XP1_IFS_HANDLES 标志实现的。应用程序可以使用此标志来指示其是否使用文件系统调用。

若在这些 providers 上使用 ReadFile 和 WriteFile,ws2_32.dll 就必须进行额外的参数安排和 user/kernel 模式的转换。准确的说,甚至在 WSASend 和 WSARecv 的情况下,这样的 providers 都无法避免额外的模式转换,尽管 ReadFile 和 WriteFile 的转换更多。layered sample 允许所有 Winsock I/O 调用的 pre- 和 post-processing。因此,它使用 WPUCreateSocketHandle 来创建返回给 ws2_32.dll 的句柄。

直到 Windows NT 4.0 SP3,人们都可以在 administrator 权限的上下文中使用 WPUCreateSocketHandle 函数。这个问题使得在 Windows NT 上一个 LSP 不能大规模地展开。Windows 95 和 Windows 98 则没有这个限制,因为它们没有使用 Windows NT 的安全模型。Windows NT 4.0 SP4 正致力于此问题。对于 LSP 的开发者们来说,这可真是个好消息。

LSP Socket I/O

Winsock 2 中有三种基本的 I/O 模型:blocking, nonblocking 和 overlapped。发生在 IOCPs 上的 I/O 操作使用 overlapped I/O。我们将讨论每一种使用 WSPRecv 的 I/O 模型。细节与 WSPSend 的类似。

Blocking I/O 是 Winsock 2 中最简单的形式。blocking socket 上的任意 I/O 操作都是直到操作完成后才返回。因此,任意线程一次只能执行一个 I/O 操作。当 LSP 的 WSPRecv 被 ws2_32.dll 以 blocking 的方式调用,则 lpOverlapped 参数就会为 NULL。LSP 只需要将调用传递给 base provider 的 WSPRecv 调用。LSP 的 WSPRecv 只在 base 的 WSPRecv 完成时才返回。

即使 blocking I/O 很容易实现,我们还必须考虑同 Winsock 1.1 的 blocking hooks 的向后兼容性。 WSASetBlockingCall 和 WSACancelBlockingCall 调用被移出了 Winsock 2 API 规范。如果 Winsock 1.1 的应用程序调用了 WSPCancelBlockingHook 和 WSACancelBlockingCall 函数,则 WSPCancelBlockingHook 仍然可以由 ws2_32.dll 调用。在 LSP 里,只需将 WSPCancelBlockingHook 调用传递给 base providers 调用。如果正在实现的是 base provider 且在开发一个 blocking call,则必须实现一种周期调用 WPUQueryBlockingCallback 函数的机制。

Nonblocking I/O

若 socket 是 nonblocking 模式的,则任何 I/O 操作都必须要么立即完成要么返回 error code WSAEWOULDBLOCK 来指示此操作不能立即完成。后一种情况需要一种机制来找到再次执行该操作的时机。为此,一组网络事件被定义出来,可以使用 WSPSelect 来等待这些事件,也可以通过调用 WSPAsyncSelect 或 WSPEventSelect 来注册异步的传送。

若是用 WSPSelect,则三个 fd_sets(readfds, writefds 和 exceptfds)都从 ws2_32.dll 传递下来。LSP 的 WSPSelect 需要通过调用 WPUQuerySocketHandleContext 并创建自己的 read fd_set, write fd_set 和 except fd_set 来找到 base provider 的 sockets。然后它调用 base provider 的 WSPSelect。当 base 的 WSPSelect 返回时,它还必须将 base socket 的 fd_set 转换为原先接受到的 fd_sets。Winsock 2 service providers 的体系使得用户程序中的 select 在一次调用中只能正确处理同一个 service provider 的 sockets。LSP 的 WSPEventSelect 实现其实很简单。当用户程序的事件对象由 ws2_32.dll 向下传递时,LSP 只需将 ws2_32.dll 的 socket handle 转换为相应的 base socket handle 并将调用传递给 base providers 的 WSPEventSelect。当所要的 I/O 可被调用时,base provider 直接通知事件对象,用户程序此时就知道有网络事件到了。

LSP 的 WSPAsyncSelect 实现就有点棘手了。因为 LSP 不能依赖于用户程序的 message pump 来 pump socket 的通知消息,所以一般 LSP 必须创建另一个窗口并有一个专门的 worker thread 来 pump messages。在 layered sample 中,当 WSPAsyncSelect 被第一次调用时,一个隐藏的窗口和一个 worker thread 也被创建,并且用户程序的窗口句柄和通知消息被保存起来。在将 ws2_ 32.dll 的 socket 句柄转换为 base provider 句柄后,这个新的隐藏窗口和一条新消息被用来调用 base provider 的 WSPAsyncSelect。当接受到那个指示所需 I/O 可用的新消息时,隐藏窗口的 window procedure 就用 WPUPostMessage 将用户程序的通知消息发送到用户程序的窗口。

LSP 应使用一个中间窗口还有另一个实践中的原因。当在 Windows 9x 上运行16位的 Winsock 1.1 的应用程序时,由于 wsock2.vxd 和 winsock.dll 的实现方式上的原因,如果直接将用户程序的窗口传递给 base WSPAsyncSelect,LSP 就不会得到通知消息。

Overlapped I/O

所有 Winsock 2 transport providers 都必须支持 overlapped I/O。LSP 中能进行 overlapped I/O 的 sockets 都是通过 WSA_FLAG_ OVERLAPPED 标志置位的 WSPSocket 函数创建的,而且都遵从 Win32 中建立的 overlapped I/O 模型。Overlapped I/O 是所有三种 Winsock 2 I/O 模型中最复杂的一种。

ws2_32.dll 记录着用户程序中所有的 overlapped I/O,包括在 IOCP 上的。当用户程序从 LSP 角度使用了 overlapped 的 WSARecv 时,其 WSPRecv 就由 ws2_32.dll 调用,而且 lpOverlapped 参数就不为 NULL。socket 句柄转换之后,就需要调用 base provider 的 WSPRecv,base provider 的 WSPRecv 完成时还必须被通知到。反过来,还需要向 ws2_32.dll 发送 completion notification。换句话说,LSP 必须处理两级的 completion notifications:一个是从 base provider 到 LSP,另一个从 LSP 到 ws2_32.dll。此中所有操作都不能阻塞调用线程。

我们再来详细看一下两级 completion notifications。第一级就是在 base provider 上发出一个 overlapped I/O 并检测其是否完成。当 LSP 在 base provider 上启动 overlapped I/O 时,有两种方法可以管理 I/O 请求的完成:通知一个 LSP 相关的事件对象或调用一个 LSP 相关的 completion routine。在这两种情况下,一个数据结构,WSAOVERLAPPED,与 overlapped 的操作相关联。WSAOVERLAPPED 结构体可由 LSP 用来一个保存 overlapped 操作的结果(如传送的字节个数、更新的标志和 error codes)的“句柄”。要得到这些结果,LSP 必须调用 base provider 的 WSPGetOverlappedResult,向其传递一个指向 WSAOVERLAPPED 结构体的指针。

若为 overlapped I/O 选择基于事件的 completion,则 base provider 的 WSPGetOverlappedResult 函数可以由 LSP 用来查询或等待 base provider 的 overlapped 操作的完成。LSP 还可以使用其它的方法(如 WSAWaitForMultipleEvents)来等待,直到相应的事件对象被通知。如果为 overlapped I/O 请求指定了一个 completion routine,则 WSPGetOverlappedResult 的选项中只有查询项可用。因为 Winsock 2 的 completion routine 是通过异步过程调用(asynchronous procedure call,APC)机制调用的,所以 LSP 需要将发出 I/O 的线程置为一个 alertable 的等待状态以使得在 base provider 的 overlapped I/O 完成后 completion routine 能得以执行。因此,LSP 需要调用一个等待函数(如 WSAWaitForMultipleEvents 或 SleepEx)并将等待调用中的 fAlertable 参数指定为 TRUE。一旦指示了 completion,LSP 可以调用 base provider 的 WSPGetOverlappedResult,并期待调用能立即完成。Figure 6 总结了 overlapped socket 的 completion 的语义并展示了 WSPRecv 中的参数 lpOverlapped、hEvent 和 lpCompletionRoutine 的各种组合。

在 Windows NT 上,实际上有不止一种的机制可供 LSP 用来得到 base provider 的 overlapped I/O completion 的通知。LSP 可以向 IOCP 添加 base provider 的 socket 句柄,在 base provider 上发出 overlapped I/O,然后在一个 worker thread 中调用 GetQueuedCompletionStatus 来取得 base provider 的 overlapped I/O 的 completion notification。

我们再来看第二级的 completion notification,在这一级中 LSP 将 overlapped I/O 的完成通知给 ws2_32.dll。根据 WSPRecv 的 LSP 实现被 ws2_32.dll 调用的方式,LSP 可以使用 WPUCompleteOverlappedRequest 或 WPUQueueApc。

WPUCompleteOverlappedRequest 是一个新函数,Winsock 2 SPI Revision 2.2.2 添加了此函数来支持 IFS 句柄。只有在用户程序不为其 overlapped I/O 使用 completion routine 函数时才使用这个函数。WPUCompleteOverlappedRequest 只是使 LSP 通知 ws2_32.dll 一个 overlapped I/O 已经完成了。ws2_32.dll 会使用上面的 completion 机制中的一种来将 completion 通知给用户程序。Windows NT 4.0 SP3 及之前的版本中的 Winsock 2 实现不支持WPUCompleteOverlappedRequest。拥有最新 Winsock 2 add-on 的 Windows 95、Windows 98、Windows NT 4.0 SP4 和 Windows 2000 在 ws2_32.dll 中支持此函数。对于 Windows NT 的早期版本,LSP 可以调用 SetEvent 来通知在用户程序指定的 overlapped 结构体中的事件句柄。当 LSP 调用 WPUCompleteOverlappedRequest 来支持 IFS 句柄时,会出现较重的负荷。如果对 IFS 句柄的支持不是必需的,则在 LSP 中调用 SetEvent 来直接向用户程序通知 overlapped I/O 的完成更为高效。

如果用户程序为 overlapped I/O 指定了 completion routine,ws2_32.dll 就会调用 LSP 的 WSPRecv SPI 并用 lpCompletionRoutine 参数向下传递用户指定的 completion routine。当下层的 I/O 完成后 LSP 就来负责安排此 completion routine 的调用。因为 completion routine 必须在发出 overlapped 操作的同一线程的上下文中执行,所以就不能从 LSP 中直接调用。LSP 需要调用 WPUQueueApc 来使得 completion routine 能在正确的线程中执行。此函数可以在任意进程和线程上下文中调用,甚至可以在与此进程和线程不同的上下文中发出 overlapped operation。

WPUQueueApc 的参数为一个指向 WSATHREADID 结构体的指针,一个指向要调用的 APC 函数的指针以及一个32位的上下文值,之后会将这个值传递给 APC 函数。LSPs 总是提供一个指向正确的 WSATHREADID 结构体指针,此结构体指针由 overlapped 函数的 lpThreadId 参数传来。LSP 应该本地存储 WSATHREADID 结构体并提供一个指向此 WSATHREADID 结构体副本的指针,此指针要用作 WPUQueueApc 的输入参数。一旦 WPUQueueApc 函数返回,provider 可以处理它那份 WSATHREADID。

函数 WPUQueueApc 只是将足够的信息排队来用所给参数调用 APC 函数的,但直接调用是不行的。当目标线程进入了 alertable 的等待状态时,这个信息就出队列并且 APC 函数会在目标线程和进程的上下文中被调用。在一些环境下,LSP 可能需要在 worker thread 的内部发出并完成 overlapped 操作。这时,进来的函数调用中就不会有 WSATHREADID。SPI 提供一个 upcall,WPUOpenCurrentThread,来从当前线程中取得一个 WSATHREADID。当不再需要此 WSATHREADID 时,其资源应通过调用 WPUCloseThread 来返回。

正如所提到的,WSAOVERLAPPED 结构体提供了 overlapped I/O 操作的发起和完成之间的媒介。WSAOVERLAPPED 结构体从设计上与 Win32 的 OVERLAPPED 结构体是兼容的(见 Figure 7)。

layered sample 使用一个 WSAOVERLAPPED 结构体来保存所发生的 overlapped I/O 的状态信息。当 base overlapped I/O 被调用时, Internal 成员就被设为 WSS_OPERATION_IN_PROGRESS。当 overlapped I/O 完成且没有用户的 completion function 被使用,则 Internal 成员就设为 Offset 成员中的值。Offset 成员是用来报告 base overlapped I/O 的 error code 的。OffsetHigh 成员用于保存 Winsock I/O 的标志(WSPRecv 的 lpFlags 参数)。InternalHigh 的作用是报告所传输的字节数。当用户的 completion function 被调用时,除 Internal 成员被设为用户 completion function 的函数指针以及其被作为 WPUQueueApc 的上下文递交之外,其它所有都一样。

Winsock 2 支持一种 debug/trace 机制,此机制允许开发者来 trace Winsock 2 的函数调用,函数返回,参数值和返回值。参数值和返回值可以在函数调用或函数返回时修改。debug/trace 的 dt_dll.dll exposes 了两个函数,WSAPreApiNotify 和 WSAPostApiNotify。在 LSP 中实现 Winsock 2 的基本思想就是当进入一个 WSP 函数时,dt_dll.dll 的 WSAPreApiNotify 函数首先被调用,当 WSP 函数退出时, WSAPostApiNotify 最后被调用。关于 Winsock 2 debug 和 trace 的详细描述可以在 Platform SDK 的 DT_DLL 样例的 dbgspec.doc 中找到。

Installing a Layered Transport Service Provider

现在我们已经讲了 LSP 的实现方式,我们来看一下如何编写一个 LSP 的安装程序。安装程序只是配置 Winsock 2 系统配置数据库(Winsock 2 system configuration database)中的 LSP ,这个数据库是所有已安装的 service providers 的目录。Winsock 2 可以通过数据库知道 service provider 的存在并定义所提供的服务的类型。当 Winsock 应用程序创建 socket 时,Winsock 2 用数据库来确定所需加载的 transport service providers。ws2_32.dll 搜索数据库以找到与 socket 或 WSASocket API 调用的输入参数(如地址族、socket 类型和协议)相匹配的第一个 provider。一旦找到,ws2_32.dll 就加载目录中指定的相应的 service provider DLL。

要成功安装并管理 database 中的 service provider entry 需要四个函数。每一个函数都以 WSC 前缀开头:

WSCEnumProtocols
WSCInstallProvider
WSCWriteProviderOrder
WSCDeInstallProvider

这些函数用 WSAPROTOCOL_INFOW 结构体(见 Figure 8)查询并操作数据库。对于 LSP 的安装来说,主要关心的是 ProviderId, dwCatalogEntryId 和 ProtocolChain 域。ProviderId 域是一个全局唯一的标识符,该标识符可用来在任何系统上定义和安装 provider。dwCatalogEntryId 域只是标识了数据库中的每一个 WSAPROTOCOL_INFOW 目录项结构体。ProtocolChain 决定了 WSAPROTOCOL_INFOW 结构体是一个 base provider 的 catalog entry、layered provider 还是 provider protocol chain。ProtocolChain 域是一个 WSAPROTOCOLCHAIN 结构体:

typedef struct {
int ChainLen; /* the length of the chain,
/* length = 0 means layered provider,
/* length = 1 means base provider,
/* length > 1 means protocol chain */
DWORD ChainEntries[MAX_PROTOCOL_CHAIN];
/* a list of dwCatalogEntryIds */

} WSAPROTOCOLCHAIN, FAR * LPWSAPROTOCOLCHAIN;

ChainLen 决定了目录项表示 base provider, layered provider 或是定义一个协议链。协议链式也是目录项,它定义了将作为 Winsock 和其它 service providers 间的 service provider 的我们的 layered provider 放在什么位置(见 Figure 2)。 ChainLen 域为 0 表示是一个 layered provider,为 1 表示是 base service provider 或其它的 service providers,若为比 1 大的值则表示是协议链。对于 layered provider 和 base provider,在数据库中每个 provider 只有一个目录项。

最后的 ChainEntries 域是一个目录 IDs 的数组,这些目录 IDs 是用来描述协议链目录项中 service providers 的加载顺序的。在创建 socket 期间,当 Winsock 在目录中查找相应的 service provider 时,只需找协议链和 base provider catalog 目录项。ws2_32.dll 忽略 layered provider 目录项且 layered provider 目录项只在链接到协议链目录项的协议链时才存在。

当在 service provider 之上安装 LSP 时,需要创建两个 WSAPROTOCOL_INFOW 目录项结构体,一个表示 layered provider 另一个表示将 layered provider 链接到 base provider 的协议链。这两个结构体一般都用已有的 service provider 的 WSAPROTOCOL_INFOW 目录项结构体初始化,这个结构体可由调用 WSCEnumProtocols 得到。在初始化之后,就需要用 WSCInstallProvider 来安装 layered provider 的目录项。

这两个结构体一般都用现有的 service provider 的 WSAPROTOCOL_INFOW 目录项结构体初始化,这个结构体可以通过调用 WSCEnumProtocols 得到。初始化之后,就需要用 WSCInstallProvider 来安装 layered provider 目录项,然后取得初始化后赋给此结构体的目录 ID。之后目录项就可以用来建立将 layered provider 链接到另一个 provider 的协议链目录项。安装链接的 provider 还要调用 WSCInstallProvider(见 Figure 9)。

注意在 Figure 9 中在 layered provider 的 WSAPROTOCOL_INFOW 结构体中指定了 PFL_HIDDEN 标志。这个标志保证了 WSAEnumProtocols 不会在返回的缓冲区中包含 layered provider 的目录。

LSP 现在就安装到系统上了。Winsock 2 是如何在数据库中查找 service providers 的呢?多数 Winsock 应用程序都要通过 socket 或 WSASocket 的调用参数来确定打算使用的协议。例如,如果创建使用 AF_INET 地址族和 SOCK_STREAM 类型的 socket,Winsock 2 会首先在提供此功能的数据库中查找可用的 TCP/IP 协议链目录项或是 base provider 目录项。当用 WSCInstallProvider 为 layered provider 安装协议链时,目录项会自动变为数据库的最后一项。为了让新的链成为默认的 TCP/IP provider,还必须将数据库中的 providers 重新排序并调用 WSCWriteProviderOrder 将协议链目录项放在其它 TCP/IP providers 的前面。可以通过 Platform SDK 中的 sporder.exe 工具来察看 providers 在目录中的安装和排序情况。sporder.dll 一定要在同一目录下,否则 sporder.exe 会失败。Figure 10 所示为在普通计算机上安装 layered sample 后的 Winsock 2 配置情况。这里的 LAYERED_PROVIDER 项表示 layered provider 目录项, Layered MSAFD Tcpip [TCP/IP] 表示将 layered provider 链接到 base provider MSAFD Tcpip [TCP/IP] 上的协议链。

Figure 10 Winsock 2 Configuration

随着加入的 LSPs 数目的增长,一个安装程序可以将一个 LSP 安装在有以前安装过 LSPs 的系统上。安装程序需要选择是将其 LSP 插入到现有的协议链还是在 base providers 上层创建一个新链。我们已经讲过如何在 base providers 之上安装 LSP。要向现有的协议链插入 LSP,安装程序需要使用 WSC 函数来做以下的工作:

安装 layered 的 provider 来得到其 catalog ID.

通过增加 ProtocolChain.ChainLen 来修改 chain provider 的 WSAPROTOCOL_INFOW 并将 catalog ID 插入到 ProtocolChain.ChainEntries 中的目的位置。

移除现有的链并安装修改过的链。

Managing Protocol Chain Order

LSPs 对 value-added 的网络服务来说是有着巨大的潜力的。但是当前的 Winsock 2 规范并没有对一个重要的问题给出答案,这个问题就是:若已经安装了协议链则新的协议链应插入到何处。例如,如果想要向一个已经有一个 URL 过滤 LSP 的系统上再安装一个数据加密的 LSP,很显然数据加密的 LSP 需要插入到现有协议链的过滤 LSP 之下。但问题是 LSP 安装程序不能获知现有 LSP 提供的是什么类型的服务也因此就不知道它在协议链中的正确插入位置。在由 administrators 决定安装什么 LSP 及以何种顺序安装的受控网络环境中这还不算什么问题。但是 LSP 的广泛应用却受到了抑制,因为只有使 LSP 在 base provider 之上并使新的链成为协议默认的 provider 的安装才是安全的。这样的方法保证了新 LSP 的服务,但却使现有默认 provider 的 LSP 被移出链。

另一个没有在 Winsock 2 规范中提到的问题就是现有的 LSPs 如何在链接活动中保护自己本身不被修改或被修改时能得到通知。这个问题与第一个问题一样棘手。在实际中,如果 LSP 协议链没有被修改,LSP 开发者就能够在 LSP 中 hardcode 链的顺序,并在安装程序中将 LSP 作为 base provider 来安装,这需要将 LSP 的 WSAPROTOCOL_INFOW 结构体的 ProtocolChain.ChainLen 成员指定为 1。

A Walk Through the Layered Sample

现在我们将所有这些都放到一起并来探索 Platform SDK 中的 layered sample。尽管这个样例可能看起来有点儿大,但它实现了一套完整的 Winsock 2 LSP,而开发者可以立即着手对其的扩展工作。

要构建这个 layered sample,可以只对 makefile 运行 nmake 工具。生成的 lsp.dll 就是所要的 layered LSP,inst_lsp.exe 是安装用的可执行文件。将 lsp.dll 拷贝到 Windows system(32) 目录并运行 inst_lsp.exe。再次运行 inst_lsp.exe 就会将 layered sample 从 Winsock 2 provider 的目录里移除并因此就卸载了 layered。layered sample 有几种不同的版本。这里讨论的是要移入 Windows 2000 官方版的 Platform SDK 中的最新版本。程序可以运行在带有最新 Winsock 2 的 Windows 95、Windows 98 和 Windows NT 4.0 SP4 上运行,不能在 Windows NT 4.0 SP3 和更早的 Windows NT 上运行,因为这些版本都没有实现 WPUCompleteOverlappedRequest 函数。要在 SP4 之前的 Windows NT 4.0 上使用则需要97年3月的 Platform SDK 中的 layered sample,那个 layered sample 使用 SetEvent 来通知用户程序指定的 overlapped structure 中的事件句柄。Figure 11 列出了 layered sample 中的文件。

layered 中使用了 LIST_ENTRY 和 SINGLE_LIST_ENTRY 两个链表结构体,一个是 LLIST.H 中定义的双向链表,一个是 NTDEF.H 中定义的单链表,两个头文件都在 Platform SDK 中:

typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY,
*RESTRICTED_POINTER PRLIST_ENTRY;

typedef struct _SINGLE_LIST_ENTRY {
struct _SINGLE_LIST_ENTRY *Next;
} SINGLE_LIST_ENTRY, *PSINGLE_LIST_ENTRY;

LIST_ENTRY 用于保存所有协议目录项,某一 socket 上的所有的 outstanding I/O 和所有的 outstanding sockets。SINGLE_LIST_ENTRY 用于保存一个预分配的 INTERNALOVERLAPPEDSTRUCT 结构体链表。为了让结构体能用 LIST_ENTRY 链接起来,结构体必须有以下的形式:

// typedef struct _FOO
// {
// LIST_ENTRY FooListEntry;
?
?
?
//
// } FOO, * PFOO;

给定一个指向成员 FooListEntry 的指针,CONTAINING_RECORD 宏返回一个指向宿主 FOO 结构体的指针。

#define CONTAINING_RECORD(address, type, field)
((type FAR *)( /
(PCHAR)(address) - /
(PCHAR)(&((type *)0)->field)))

只要理解了 CONTAINING_RECORD 宏,再理解 LLIST.H 中的其余部分就应该没有问题了。

DT_DLL SPI Function Tracing

设置了 Winsock 2 debug/trace DLL 后,layered sample 可以完成所有 SPI 函数的 debug tracing。可以将 MSDN Platform SDK 的样例 DT_ DLL 同 layered sample 配套使用。还需要将文件 dt_dll.dll 重命名为 mydt_dll.dll。layered sample 的每一个 WSP 函数都要调用两个特殊的宏 PREAPINOTIFY 和 POSTAPINOTIFY 来钩挂 mydt_dll.dll 中的 WSAPreApiNotify 和 WSAPostApiNotify。makefile 中有一个 DEBUG_TRACING 标志用于使调试功能可用。若 DEBUG_TRACING 已置位,PREAPINOTIFY 宏就映射到了 WSAPreApiNotify,POSTAPINOTIFY 映射到 WSAPostApiNotify。否则,这两个宏就都映射到 no-op。

DT_HOOK.CPP 中的 DTHookInitialize 函数加载 mydt_dll.dll 并将其 WSAPreApiNotify 和 WSAPostApiNotify 函数项。这一层上的(SPI.CPP 中实现)以及base provider 层(DPROVIDE.H 中实现)上的所有 SPI 函数都要调用 DT 钩挂函数。DTHookShutdown 函数用于卸载 mydt_dll.dll。DLLMAIN.CPP 中的 DllMain 函数在 DLL_PROCESS_ATTACH 中调用 DTHookInitialize,在 DLL_PROCESS_DETACH 中调用 DTHookShutdown。

PROTO_CATALOG_ITEM and DCATALOG

SPI.CPP 中的第一次 WSPStartup 调用会创建一个全局的 DCATALOG 对象,gProviderCatalog,并调用 DCATALOG.CPP 中的 Initialize,这个 Initialize 使用 WSCEnumProtocols 将一个所有已安装的 providers 的 PROTO_CATALOG_ITEM 对象们的链表填入 gProviderCatalog->m_protocol_list。gProviderCatalog->m_local_item 指向一个 layered 自己的协议目录项。gProviderCatalog->FindNextProviderInChain 函数负责加载链中的下一个 provider。如果下一个 provider 是一个 base provider,FindNextProviderInChain 还要返回 base provider 的 PPROTO_CATALOG_ITEM。每次调用 WSPStartup 时变量 gStartupCount 都要增一;每次调用 SPI.CPP 中的 WSPCleanUp,gStartupCount 要减一。当 gStartupCount 变为 0 时,WSPCleanUp 就删除掉 gProviderCatalog 对象。

DSOCKET

DSOCKET.CPP 中的 DSOCKET 对象负责保存 socket 的操作模式并在 base provider 的 socket 句柄与提交给 ws2_32.dll 的句柄之间建立关联。DSOCKET 类中的静态成员 m_socket_list 包含一个所有 DSOCKET 对象的全局链表。它是在静态函数 DSOCKET::DSocketClassInitialize,这个函数在第一次调用 WSPStartup 时被调用。DSOCKET 对象中的 m_provider_socket 是由 base provider 给出的 socket 句柄。m_socket_handle 成员是提交给 ws2_32.dll 的 socket 句柄,是由 WPUCreateSocketHandle 调用创建的。DSOCKET 对象其余的成员保存了不同 I/O 模型的上下文。对于 Windows NT 上的 overlapped I/O,m_completion_context 为 IOCP 的 Completion key。对于 WSPAsyncSelect,m_ async_events 为一个用户程序所用的网络事件的位掩码。m_async_window 是 lsp.dll 的隐藏窗口的窗口句柄,用于接收网络通知窗口消息。

DSOCKET 中没有任何反映 WSPEventSelect 的东西,因为该调用只是被向下传递给 base provider 的 WSPEventSelect。

当 layered sample 中的 SPI 函数被 ws2_32.dll 调用且向函数传递了一个 socket 句柄时,该句柄就是前面 WPUCreateSocketHandle 返回给 ws2_32.dll 的那个句柄。当调用了 WPUCreateSocketHandle 函数时,函数将相应的 DSOCKET 对象指针保存为上下文。因此,当得到一个 ws2_32.dll 中的 socket 句柄时,layered sample 就调用 WPUQuerySocketHandleContext 来取得原始的 DSOCKET 对象。从 DSOCKET 对象得到的 base provider 的句柄用于调用相应的 base SPI 函数。

DPROVIDER

DPROVIDER 对象保存了一个 provider 的所有 SPI 函数入口。layered sample 为 DPROVIDE.H 和 DPROVIDE.CPP 里实现的 base provider 创建了一个 DPROVIDER 对象。DCATALOG 中的 indNextProviderInChain 和 LoadProvider 函数可以为 base provider 加载一个 DPROVIDER 对象。第一次调用以下函数时,base provider 对象就会被加载:WSPSocket, WSPAddressToString 或 SPI.CPP 中的 WSPStringToAddress。

DPROVIDE.CPP 文件还实现了一种支持 Microsoft 对 Winsock 2 进行扩展的机制。当以 SIO_GET_EXTENSION_ FUNCTION_POINTER 标志调用 SPI.CPP 中的 WSPIoctl 时,DPROVIDER 的 InterceptExtensions 函数就会被调用。所返回的是 SPI.CPP 中的 WSPTransmitFile 和 WSPAcceptEx 函数指针而不是 base TransmitFile 和 AcceptEx 函数指针。SPI.CPP 中的 WSPTransmitFile 和 WSPAcceptEx 转换 socket 句柄并调用 base provider 的 TransmitFile 和 AcceptEx 函数。

扩展函数 GetAcceptExSockAddrs 的实现方式有点不同。因为 GetAcceptExSockAddrs 不需要 socket 句柄,因此就不用进行 socket 句柄的转换,layered sample 也就没有截获此调用。在 InterceptExtensions 中 base provider 的 GetAcceptExSockAddrs 函数指针未被修改,并在 WSPIoctl(SIO_ GET_EXTENSION_FUNCTION_ POINTER)被直接向上传递。最后的 Microsoft Winsock 2 扩展函数,WSARecvEx,被 mswsock.dll 映射到了 WSARecv,因此就未被 InterceptExtensions 处理。

只有 Windows NT 实现了这些 Microsoft 扩展函数,而 Windows 9x 却没有。要添加自己的扩展函数,只需要修改 SPI.CPP 中的 WSPIoctl 函数来将扩展函数指针返回到 SIO_GET_EXTENSION_FUNCTION_POINTER 上。

DBUFFERMANAGER

第一次调用 SPI.CPP 文件中的 WSPStartup 时,会创建一个名为 DBUFFERMANAGER 的全局变量。每次调用 WSPStartup 时,gStartupCount 就会增一。当最后一次调用 WSPCleanup 时(gStartupCount 变量变为0),gBufferManager 对象就会被删除。当调用了一个 I/O 函数时,gBufferManager->AllocBuffer 就会创建一个基于用户缓冲区的内部缓冲区。然后 layered sample 使用这个内部缓冲区来调用相应的 base provider 的 I/O 函数。完成时,gBufferManager->CopyBuffer 将内部缓冲区中的数据拷回原用户缓冲区。gBufferManager->FreeBuffer 被调用以释放内部的缓冲区。layered sample 中的 AllocateBuffer 和 CopyBuffer 只是使用同一个用户缓冲区指针来作为内部缓冲区指针,而 FreeBuffer 是一个 no-op。自己的 LSP 还可以覆盖这些函数来轻松地截获并修改 SPI I/O 调用中的数据流。

DASYNCWINDOW

当 ws2_32.dll 调用 SPI.CPP 中的 WSPAsyncSelect 函数时,SPI.CPP 中的 GetAsyncWindow 函数被调用来取得一个名为 gAsyncWindow 的全局 DASYNCWINDOW 对象。如果这是第一次调用 GetAsyncWindow,就会创建 gAsyncWindow,而且会调用 gAsyncWindow 的 Initialize 函数来在 m_async_thread 中创建一个 worker thread。在其 thread procedure,AsyncThreadProc 中会创建一个隐藏的窗口并调用一个 message pump。如果 GetAsyncWindow 被再次调用,则返回被缓存的 gAsyncWindow。当进入 AsyncThreadProc 时,会对 lsp.dll 再次调用 LoadLibrary 来添加一个 lsp.dll 的系统加载计数,而且在退出 thread procedure 时,会调用 FreeLibraryAndExitThread 来使 lsp.dll 的系统加载计数减一。layered 的加载计数 gStartupCount 并不受着两个调用影响。这是因为当 ws2_32.dll 最后一次调用 WSPCleanup 时,它会试着卸载 provider。如果有额外的 lsp.dll 加载计数,就可以避免在线程退出前 DLL 的过早卸载。

在 GetAsyncWindow 返回后,就会调用 Socket->RegisterAsyncOperation 函数来向 Socket 对象保存用户程序的异步窗口(async window)、异步通知消息(async notification message)和网络事件的位掩码。然后调用 gAsyncWindow-> RegisterSocket 函数用隐藏窗口、一个名为 WM_SELECT_MESSAGE 的新消息和网络事件的位掩码来调用 base provider 的 WSPAsyncSelect 函数。Socket->RegisterAsyncOperation 函数完成以下工作:

Socket->m_async_window = hWnd; // user app's async
// window
Socket->m_async_message = wMsg;// user app's async
// message
Socket->m_async_events = lEvent; // user apps' async
// event

gAsyncWindow->RegisterSocket 函数调用 base provider 的 WSPAsyncSelect,参数为 gAsyncWindow->m_ async_window,WM_SELECT_MESSAGE 和 lEvent。

注意 Socket 对象中的 m_async_window 成员变量是用户程序的窗口,Socket 对象中的 m_ async_message 成员变量是用户程序的异步 Winsock 通知消息。gAsyncWindow 中的 m_async_window 就是使用 WM_SELECT_MESSAGE 作为 base provider 的异步 Winsock 通知消息的 async message 的隐藏窗口。

当 gAsyncWindow->m_ async_window 的 window procedure 接收到 WM_SELECT_MESSAGE 消息时,Socket->SignalAsyncEvents 函数就会被调用,该函数会继而调用 WPUPostMessage 来将原用户程序的 Socket->m_async_message 递交给原用户程序的 Socket->m_async_window。

DOVERLAPPEDSTRUCTMGR and INTERNALOVERLAPPEDSTRUCT

扩展的 overlapped INTERNALOVERLAPPEDSTRUCT 结构体包含了 overlapped I/O 操作所有的上下文信息,包括 I/O 类型、缓冲区、completion routine、socket 句柄等等。当第一次调用 overlapped I/O 操作时,会创建一个名为 gWorkerThread 的全局 DWORKERTHREAD 对象。gWorkerThread 的 Initialize 函数创建一个名为 gOverlappedManager 的全局 DOVERLAPPEDSTRUCTMGR 对象。gOverlappedManager 的 Initialize 函数在 gBufferManager->m_overlapped_ struct_block 中预分配一个预定义数目(OUTSTANDINGOVERLAPPEDSTRUCTS)的 INTERNALOVERLAPPEDSTRUCT。在 DOVERLAP.H 中 OUTSTANDINGOVERLAPPEDSTRUCTS 被定义为 1000。换句话说,如果所编的 LSP 希望任何时刻都能有多于1000次的 overlapped I/O 操作,那么就需要增加 DOVERLAP.H 中定义的 OUTSTANDINGOVERLAPPEDSTRUCTS。gWorkerThread 的析构函数会删除 gBufferManager。

DWORKERTHREAD

当第一个 overlapped I/O 被调用时会创建一个全局的 DWORKERTHREAD 对象,gWorkerThread。在其 Initialize 调用中,如果 IOCP 创建成功,一个 worker thread 就会被创建。这就暗示着所用的平台是 Windows NT。如果 IOCP 创建失败,则暗示所用平台为 Windows 9x,这时会创建一个信号量,所创建的线程数就是系统上 CPU 的数目。

在 Windows NT 上,当 ws2_32.dll 以 overlapped 方式调用了 layered sample 的 SPI I/O 函数时,provider 的 socket 句柄就被加到了 IOCP。然后进行 base provider 的 overlapped 调用并且 worker thread 用 GetQueuedCompletionStatus 函数等待其完成。当 GetQueuedCompletionStatus 返回时,会调用 OverlappedCompletionProc 函数来通知 ws2_32.dll 操作已完成。如果客户未提供 completion function,OverlappedCompletionProc 就调用 WPUCompleteOverlappedRequest,否则用 WPUQueueApc。

注意在进入 worker thread 过程 WorkerThreadProc 时,会对 lsp.dll 调用 LoadLibrary 来使 lsp.dll 的系统加载计数增加,在 WorkerThreadProc 的出口处会调用 FreeLibraryAndExitThread 来使 lsp.dll 的系统加载计数减少。gStartupCount 并不受这两个调用的影响。这是为了确保当 worker thread 做退出时的清理工作时 lsp.dll 已被加载,即使 ws2_32.dll 已经调用了最后的 WSPCleanup 并试图卸载 lsp.dll。

Blocking Hook

WSASetBlockingCall 和 WSACancelBlockingCall 调用被移出了 Winsock 2 API 规范。然而,如果应用程序使用 Winsock 1.1 接口的话, ws2_32.dll 仍然可以调用 WSPCancelBlockingHook 函数。

在 layered sample 的 DllMain(DLL_PROCESS_ ATTACH)中,TlsAlloc 函数分配了一个线程本地存储(thread local storage,TLS)的索引。这个 TLS 索引用于保存 base provider 对象,以使 base 的 WSPCancelBlockingHook 能够被分配并能在这一层的 WSPCancelBlockingHook 中被调用。

SPI.CPP 中的两个宏,SetBlockingProvider 和 GetBlockingProvider,一个将 base provider 对象设置为 TLS index,一个从 TLS index 中取得 base provider 对象。以 WSPRecv 函数为例,SPI.CPP 为阻塞的 WSPRecv 发出以下调用:

SetBlockingProvider (Provider);
Provider->WSPRecv(…);
SetBlockingProvider (NULL);

以下的 SPI 函数实现了一个如上所示的阻塞调用:WSPAccept,WSPAcceptEx, WSPConnect, WSPRecv, WSPRecvFrom, WSPSend 和 WSPSendTo。
因为阻塞实际发生在 base provider 上,layered sample 并不需要调用 WPUQueryBlockingCallback。

WSPCleanup

SPI.CPP 中的 WSPCleanup 只是使 gStartupCount 变量减一,而且如果 gStartupCount 大于0,就别的什么也不干。当 gStartupCount 减至0时,如果 gAsyncWindow 或 gWorkerThread 有一个不为 NULL,则其 Destroy 函数被调用,这个 Destroy 函数最终会使相应的 worker thread 退出。记住当进入一个 worker thread 时,要额外调用一次 LoadLibrary,而在退出 worker thread 时,会调用一个与之匹配的 FreeLibraryAndExitThread。在 ws2_32.dll 完成 LSP 最后的 WSPCleanup 时也要调用 FreeLibraryAndExitThread。最后调用谁的 FreeLibraryAndExitThread 并无大碍。当 lsp.dll 仍被加载时,worker thread 的清理代码保证会被执行到。最后的 FreeLibraryAndExitThread 调用会卸载 lsp.dll。最后的 WSPCleanup 还会调用 DSOCKET::DSocketClassCleanup 来清理 socket 列表, 删除目录列表 gProviderCatalog 以及删除 overlapped 结构体列表 gBufferManager。

在 layered sample 的以前的版本中,程序释放 overlapped 结构体却不管下层的 overlapped I/O 是否已经完成,这样就可能使系统崩溃。现在的 layered sample 的 cleanup 的序列如下:

关闭 worker threads.

关闭 socket 句柄,使得不能再提交更多的 I/O。

若相应的 I/O 完成则删除 overlapped 结构体(Windows NT 使用 HasOverlappedIoCompleted 宏)。

Putting It All Together

到此已经学习了 layered sample 中的所有代码,现在该总结一下了。一个典型的 lsp.dll 的生命周期:

在 DllMain 中加载 lsp.dll(DLL_PROCESS_ ATTACH)

调用 WSPSocket。

对 socket 进行多种 SPI 函数调用。

调用 WSPCloseSocket。

调用 WSPCleanup。

在 DllMain 中卸载 lsp.dll(DLL_PROCESS_ DETACH)。

理解了 layered sample 的所有代码后就会发现此样例一点也不难学。读者很可能会赞成我们说 layered sample 中的 C++ 对象清晰简洁地实现了 LSP 所需的每一个特定的 SPI 函数。所以可以轻松的扩展那些对象来实现自己的 LSP 而不用重新编写。如果读者只是扩展这个 layered sample,可能会发现实现一个 LSP 不再是一个吓人而耗时的工作。随着 Windows NT 4.0 SP4 修正了 WPUCreateSocketHandle 函数的 bug,我们可以期待市场上会出现许多的商业 LSPs。以读者们为 Winsock 2 开发 service providers 时的创新思维,读者可以开始探索这些商机了!

抱歉!评论已关闭.