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

来源:互联网 发布:淘宝店新店如何推广 编辑:程序博客网 时间:2024/06/08 07:09

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

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 时的创新思维,读者可以开始探索这些商机了!

原创粉丝点击