翻译说明:
完成端口基本上公认为一种在
windows 服务平台上比较成熟和高效的 IO 方法,理解和编写程序都不是很困难。目前我正在进行这方面的实践,代码还没有完全调试和评价,只有这一篇拙劣的学习翻译文摘,见笑见笑。翻译这个文章,是因为我近期在学习一些
socket 服务程序的编写中发现(注意,只是在学习,我本人在这个领域经验并不充足到可以撰文骗钱的地步 :P ),如果不是逼着自己把这个文章从头翻译一遍,我怀疑我是否能认真领会本文的内容 :PPP. 把这个文章贴出来,不是为了赚人气,而是因为水平确实有限,虽然整体上大差不差的翻译出来了,但是细节和用词上可能还是有很多问题。是希望大家能指出其中的翻译错误和理解谬误,互相交流和帮助。非常感谢。本文翻译并没有通过原作者同意,仅用来在网络上学习和交流,加之翻译水平拙劣,所以请勿用于做商业用途。
vcbear2001.8
Windows Sockets 2.0:
使用完成端口高性能,可扩展性Winsock服务程序原作者:
Anthony Jones 和 Amol Deshpande 原文在 http://msdn.microsoft.com/msdnmag/issues/1000/winsock/winsock.aspAPIs 和扩展性 完成端口( Completion Ports ) 典型的 Worker Thread 结构 Windows NT 和 Windows 2000 的 Sockets 体系结构 缓冲区由谁来管理 资源约束 关于接受连接 TransmitFile 和 TransmitPackets 函数 来实现一个服务方案
本文作者假定你已经熟悉
Winsock API,TCP/IP ,Win32 API摘要
:编写一般的网络应用程序的难点在于程序的“可扩展性”。利用完成端口进行重叠 I/O 的技术在 WindowsNT 和 WIndows2000 上提供了真正的可扩展性。完成端口和 Windows Socket2.0 结合可以开发出支持大量连接的网络服务程序。本文从讨论服务端的实现开始,然后讨论如何处理有系统资源约束和高要求的环境,以及在可扩展的服务程序开发的过程中会遇到的一般问题。
--------------------------------------------------------------------------------
正文:
开发网络程序从来都不是一件容易的事情,尽管只需要遵守很少的一些规则创建socket,发起连接,接受连接,发送和接受数据。真正的困难在于:让你的程序可以适应从单单一个连接到几千个连接。本文主要关注C/S结构的服务器端程序,因为一般来说,开发一个大容量,具可扩展性的winsock程序一般就是指服务程序。我们将讨论基于WindowsNT4.0和Windows 2000的代码,而不包括Windows3.x(什么时候的东西了),因为Winsock2的这一属性只在Windows NT4和最新版本上有效。
APIs
和扩展性win32
重叠 I/O(Overlapped I/O) 机制允许发起一个操作,然后在操作完成之后接受到信息。对于那种需要很长时间才能完成的操作来说,重叠 IO 机制尤其有用,因为发起重叠操作的线程在重叠请求发出后就可以自由的做别的事情了。在
WinNT 和 Win2000 上,提供的真正的可扩展的 I/O 模型就是使用完成端口( Completion Port )的重叠 I/O.其实类似于
WSAAsyncSelect 和 select 函数的机制更容易兼容 Unix ,但是难以实现我们想要的“扩展性”。而且 windows 的完成端口机制在操作系统内部已经作了优化,提供了更高的效率。所以,我们选择完成端口开始我们的服务器程序的开发。完成端口(
Completion Ports )其实可以把完成端口看成系统维护的一个队列,操作系统把重叠
IO 操作完成的事件通知放到该队列里,由于是暴露 “操作完成”的事件通知,所以命名为“完成端口”( COmpletion Ports )。一个 socket 被创建后,可以在任何时刻和一个完成端口联系起来。一般来说,一个应用程序可以创建多个工作线程来处理完成端口上的通知事件。工作线程的数量依赖于程序的具体需要。但是在理想的情况下,应该对应一个
CPU 创建一个线程。因为在完成端口理想模型中,每个线程都可以从系统获得一个“原子”性的时间片,轮番运行并检查完成端口,线程的切换是额外的开销。在实际开发的时候,还要考虑这些线程是否牵涉到其他堵塞操作的情况。如果某线程进行堵塞操作,系统则将其挂起,让别的线程获得运行时间。因此,如果有这样的情况,可以多创建几个线程来尽量利用时间。应用完成端口分两步走:
1
创建完成端口句柄:HANDLE hIocp;
hIocp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if (hIocp == NULL) {
// Error
}
注意在第一个参数(
FileHandle )传入 INVALID_FILE_HANDLE, 第二个参数( ExistingCompletionPort )传入 NULL, 系统将创建一个新的完成端口句柄,没有任何 IO 句柄与其关联。2
. 完成端口创建成功后,在 socket 和完成端口之间建立关联。再次调用 CreateIoCmpletionPort 函数,这一次在第一个参数 FileHandle 传入创建的 socket 句柄,参数 ExistingCompletionPort 为已经创建的完成端口句柄。以下代码创建了一个
socket 并把它和完成端口联系起来。SOCKET s;
s = socket(AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
// Error
if (CreateIoCompletionPort((HANDLE)s,
hIocp,
(ULONG_PTR)0,
0) == NULL)
{
// Error
}
???
}
到此为止
socket 已经成功和完成端口相关联。在此 socket 上进行的重叠 IO 操作结果均使用完成端口发出通知。注意: CreateIoCompletionPort 函数的第三个参数允许开发人员传入一个类型为 ULONG_PTR 的数据成员 , 我们把它称为完成键( Completion key ),此数据成员可以设计为指向包含 socket 信息的一个结构体的一个指针,用来把相关的环境信息和 socket 联系起来,每次完成通知来到的同时,该环境信息也随着通知一起返回给开发人员。完成端口创建以及与
socket 关联之后,就要创建一个或多个工作线程来处理完成通知,每个线程都可以循环的调用 GetQueuedCompletionStatus 函数,检查完成端口上的通知事件。在举例说明一个典型的工作线程的之前,我们先讨论一下重叠
IO 的过程。当一个重叠 IO 被发起,一个 Overlapped 结构体的指针就要作为参数传递给系统。当操作完成, GetQueueCompletionStatus 可以返回指向同一个 Overlapp 结构的指针。为了辨认和定位这个已完成的操作,开发人员最好定义自己的 OVERLAPPED 结构,以包含一些自己定义的关于操作本身的额外信息。比如:typedef struct _OVERLAPPEDPLUS {
OVERLAPPED ol;
SOCKET s, sclient;
int OpCode;
WSABUF wbuf;
DWORD dwBytes, dwFlags;
// other useful information
} OVERLAPPEDPLUS;
此结构的第一个成员为默认的
OVERLAPPED 结构,第二,三个为本地服务 socket 和与该操作相关的客户 socekt, 第 4 个成员为操作类型,对于 socket, 现在定义的有#define OP_READ 0
#define OP_WRITE 1
#define OP_ACCEPT 2
3
种。然后还有应用程序的 socket 缓冲区,操作数据量,标志位以及其他开发人员认为有用的信息。当进行重叠
IO 操作,把 OVERLAPPEDPLUS 结构作为重叠 IO 的参数 lpOverlapp 传递(如 WSASend,WASRecv, 等函数,有一个 lpOverlapped 参数,要求传入一个 OVERLAPP 结构的指针)当操作完成后,
GetQueuedCompletionStatus 函数返回一个 LPOVERLAPPED 类型的指针,这个指针其实是指向开发人员定义的扩展 OVERLAPPEDPLUS 结构 , 包含着开发人员早先传入的全部信息。注意:
OVERLAPPED 成员不一定要求是 OVERLAPPEDPLUS 扩展结构的一个成员,在获得 OVERLAPPED 指针之后,可以用 CONTAINING_RECORD 宏获得相应的扩展结构的指针。
典型的
Worker Thread 结构DWORD WINAPI WorkerThread(LPVOID lpParam)
{
ULONG_PTR *PerHandleKey;
OVERLAPPED *Overlap;
OVERLAPPEDPLUS *OverlapPlus,
*newolp;
DWORD dwBytesXfered;
while (1)
{
ret = GetQueuedCompletionStatus(
hIocp,
&dwBytesXfered,
(PULONG_PTR)&PerHandleKey,
&Overlap,
INFINITE);
if (ret == 0)
{
// Operation failed
continue;
}
OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
switch (OverlapPlus->OpCode)
{
case OP_ACCEPT:
// Client socket is contained in OverlapPlus.sclient
// Add client to completion port
CreateIoCompletionPort(
(HANDLE)OverlapPlus->sclient,
hIocp,
(ULONG_PTR)0,
0);
// Need a new OVERLAPPEDPLUS structure
// for the newly accepted socket. Perhaps
// keep a look aside list of free structures.
newolp = AllocateOverlappedPlus();
if (!newolp)
{
// Error
}
newolp->s = OverlapPlus->sclient;
newolp->OpCode = OP_READ;
// This function prepares the data to be sent
PrepareSendBuffer(&newolp->wbuf);
ret = WSASend(
newolp->s,
&newolp->wbuf,
1,
&newolp->dwBytes,
0,
&newolp.ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
// Put structure in look aside list for later use
FreeOverlappedPlus(OverlapPlus);
// Signal accept thread to issue another AcceptEx
SetEvent(hAcceptThread);
break;
case OP_READ:
// Process the data read
// ???
// Repost the read if necessary, reusing the same
// receive buffer as before
memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
ret = WSARecv(
OverlapPlus->s,
&OverlapPlus->wbuf,
1,
&OverlapPlus->dwBytes,
&OverlapPlus->dwFlags,
&OverlapPlus->ol,
NULL);
if (ret == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
// Error
}
}
break;
case OP_WRITE:
// Process the data sent, etc.
break;
} // switch
} // while
} // WorkerThread
--------------------------------------------------------------------------------
查看以上代码,注意如果
Overlapped 操作立刻失败(比如,返回 SOCKET_ERROR 或其他非 WSA_IO_PENDING 的错误),则没有任何完成通知时间会被放到完成端口队列里。反之,则一定有相应的通知时间被放到完成端口队列。更完善的关于
Winsock 的完成端口机制,可以参考 MSDN 的 Microsoft PlatFormSDK ,那里有完成端口的例子。访问 http://msdn.microsoft.com/library/techart/msdn_servrapp.htm . 可以获得更多信息。
Windows NT
和 Windows 2000 的Sockets体系结构学习一些
WinNT 和 Win2000 基本的 Sockets 体系结构有益与对扩展性规则的理解。下图表示当前版本 Win2000 的 Winsock 实现。应用程序不应该依赖于这里描述的一些底层细节(指 drivers ,Dlls 之类的),因为这些可能会在未来版本的操作系统中被改变。
Socket
体系结构Winsock2.0
规范支持多种协议以及相关的支持服务。这些用户模式服务支持可以基于其他现存服务提供者来扩展他们自己的功能。比如,一个代理层服务支持( LSP )可以把自己安装在现存的 TCP/IP 服务顶层。这样,代理服务就可以截取和重定向一个对底层功能的调用。与其他操作系统不同的是,
WinNT 和 Win2000 的传输协议层并不直接给应用程序提供 socket 风格的接口,不接受应用程序的直接访问。而是实现了更多的通用 API ,称为传输驱动接口 (Transport Driver Interface,TDI). 这些 API 把 WinNT 的子系统从各种各样的网络编程接口中分离出来。然后,通过 Winsock 内核模式驱动提供了 sockets 方法(在 AFD.SYS 里实现)。这个驱动负责连接和缓冲管理,对应用程序提供 socket 风格的编程接口。 AFD.SYS 则通过 TDI 和传输协议驱动层交流数据。缓冲区由谁来管理
如上所说,对于使用
socket 接口和传输协议层交流的应用程序来说, AFD.SYS 负责缓冲区的管理。也就是说,当一个程序调用 send 或 WSASend 函数发送数据的时候,数据被复制到 AFD.SYS 的内部缓冲里(大小根据 SO_SNDBUF 设置),然后 send 和 WSASend 立刻返回。之后数据由 AFD.SYS 负责发送到网络上,与应用程序无关。当然,如果应用程序希望发送比 SO_SNDBUF 设置的缓冲区还大的数据, WSASend 函数将会被堵塞,直到所有数据均被发送完毕为止。同样,当从远地客户端接受数据的时候,如果应用程序没有提交
receive 请求,而且线上数据没有超出 SO_RCVBUF 设置的缓冲大小,那么 AFD.SYS 就把网络上的数据复制到自己的内部缓冲保存。当应用程序调用 recv 或 WSARecv 函数的时候,数据即从 AFD.SYS 的缓冲复制到应用程序提供的缓冲区里。在大多数情况下,这个体系工作的很好。尤其是应用程序使用一般的发送接受例程不牵涉使用
Overlapped 的时候。开发人员可以通过使用 setsockopt API 函数把 SO_SNDBUF 和 SO_RCVBUF 这两个设置的值改为 0 关闭 AFD.SYS 的内部缓冲。但是,这样做会带来一些后果:比如,应用程序把
SO_SNDBUF 设为 0, 关闭了发送缓冲(指 AFD.SYS 里的缓冲),并发出一个同步堵塞式的发送操作,应用程序提供的数据缓冲区就会被内核锁定, send 函数不会返回,直到连接的另一端收到整个缓冲区的数据为止。这貌似一种挺不错的方法,用来判断是否你的数据已经被对方全部收取。但实际上,这是很糟糕的。问题在于:网络层即使收到远端 TCP 的确认,也不能保证数据会被安全交到客户端应用程序那里,因为客户端可能发生“资源不足”等情况,而导致应用程序无法从 AFD.SYS 的内部缓冲复制得到数据。而更重大的问题是:由于堵塞,程序在一个线程里只能进行一次 send 操作,非常的没有效率。如果关闭接受缓冲(设置
SO_RCVBUF 的值为 0 ),也不能真正的提高效率。接受缓冲为 0 迫使接受的数据在比 winsock 内核层更底层的地方被缓冲,同样在调用 recv 的时候进行才进行缓冲复制,这样你关闭 AFD 缓冲的根本意图(避免缓冲复制)就落空了。关闭接收缓冲是没有必要的,只要应用程序经常有意识的在一个连接上调用重叠 WSARecvs 操作,这样就避免了 AFD 老是要缓冲大量的到来数据。到这里,我们应该清楚关闭缓冲的方法对绝大多数应用程序来说没有太多好处的了。
然而,一个高性能的服务程序可以关闭发送缓冲,而不影响性能。这样的程序必须确保它在同时执行多个
Overlapped 发送,而不是等待一个 Overlapped 发送结束之后,才执行另一个。这样如果一个数据缓冲区数据已经被提交,那么传输层就可以立刻使用该数据缓冲区。如果程序“串行”的执行 Overlapped 发送,就会浪费一个发送提交之后另一个发送执行之前那段时间。
资源约束
鲁棒性是每一个服务程序的一个主要设计目标。就是说,服务程序应该可以对付任何的突发问题,比如,客户端请求的高峰,可用内存的暂时贫缺,以及其他可靠性问题。为了平和的解决这些问题,开发人员必须了解典型的
WindowsNT 和 Windows2000 平台上的资源约束。最基本的问题是网络带宽。使用
UDP 协议进行发送的服务程序对此要求较高,因为这样的服务程序要求尽量少的丢包率。即使是使用 TCP 连接,服务器也必须注意不要滥用网络资源。否则, TCP 连接中将会出现大量重发和连接取消事件。具体的带宽控制是跟具体程序相关的,超出了本文的讨论范围。程序所使用的虚拟内存也必须小心。应该保守的执行内存申请和释放,或许可以使用旁视列表(一个记录申请并使用过的“空闲”内存的缓冲区)来重用已经申请但是被程序使用过,空闲了的内存,这样可以使服务程序避免过多的反复申请内存,并且保证系统中一直有尽可能多的空余内存。(应用程序还可以使用
SetWorkingSetSize 这个 Win32API 函数来向系统请求增加该程序可用的物理内存。)有两个
winsock 程序不会直接面对的资源约束。第一个是页面锁定限制。无论应用程序发起 send 还是 receive 操作,也不管 AFD.SYS 的缓冲是否被禁止,数据所在的缓冲都会被锁定在物理内存里。因为内核驱动要访问该内存的数据,在访问期间该内存区域都不能被解锁。在大部分情况下,这不会产生任何问题。但是操作系统必须确认还有可用的可分页内存来提供给其他程序。这样做的目的是防止一个有错误操作的程序请求锁定所有的物理 RAM ,而导致系统崩溃。这意味着,应用程序必须有意识的避免导致过多页面锁定,使该数量达到或超过系统限制。在
WinNT 和 Win2000 中,系统允许的总共的内存锁定的限制大概是物理内存的 1/8 。这只是粗略的估计,不能作为一个准确的计算数据。只是需要知道,有时重叠 IO 操作会发生 ERROR_INSUFFICIENT_RESOURCE 失败,这是因为可能同时有太多的 send/receives 操作在进行中。程序应该注意避免这种情况。另一个的资源限制情况是,程序运行时,系统达到非分页内存池的限制。
WinNT 和 Win2000 的驱动从指定的非分页内存池中申请内存。这个区域里分配的内存不会被扇出,因为它包含了多个不同的内核对象可能需要访问的数据,而有些内核对象是不能访问已经扇出的内存的。一旦系统创建了一个 socket (或打开一个文件),一定数目的非分页内存就被分配了。另外,绑定 (binding) 和连接 socket 也会导致额外的非分页内存池的分配。更进一步的说,一个 I/O 请求,比如 send 或 receive, 也是分配了很少的一点非分页内存池的(为了跟踪 I/O 操作的进行,包含必须信息的一个很小的结构体被分配了)。积少成多,最后还是可能导致问题。因此操作系统限制了非分页内存的数量。在 winNT 和 win2000 平台上,每个连接分配的非分页内存的准确数量是不相同的,在未来的 windows 版本上也可能保持差异。如果你想延长你的程序的寿命,就不要打算在你的程序中精确的计算和控制你的非分页内存的数量。虽然不能准确计算,但是程序在策略上要注意避免冲击非分页限制。当系统的非分页池内存枯竭,一个跟你的程序完全无关的的驱动都有可能出问题,因为它无法正常的申请到非分页内存。最坏的情况下,会导致整个系统崩溃。比如那些第三方设备或系统本身的驱动。切记:在同一台计算机上,可能还有其他的服务程序在运行,同样在消耗非分页内存。开发人员应该用最保守的策略估算资源,并基于此策略开发程序。
资源约束的解决方案是很复杂的,因为事实上,当资源不足的情况发生时,可能不会有特定的错误代码返回到程序。程序在调用函数时可能可以得到类似
WSAENOBUFS 或ERROR_INSUFFICIENT_RESOURCES
的这种一般的返回代码。如何处理这些错误呢,首先,合理的增加程序的工作环境设置( Working set ,如果想获得更多信息,请参考 MSDN 里 John Robbins 关于 Bugslayer 的一章)。如果仍然不能解决问题,那么你可能遇上了非分页内存池限制。那么最好是立刻关闭部分连接,并期待情况恢复正常。
关于接受连接
服务程序最常做的一个事情是接受客户端的连接。
AcceptEx 函数是 Winsock API 中唯一可以使用重叠 IO 方式接受 Socket 连接的函数。 AccpetEx 要求一个传入一个 socket 作为它的参数。普通的同步 accept 函数,新的 SOCKET 是作为返回值得到的。 AcceptEx 函数作为一个重叠操作,接收 Socket 应该提前被创建(但不需要绑定和或连接) , 并传入此 API 。(
AcceptEx 原形,加粗的即为需要传入的 socketBOOL AcceptEx(
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
);
)
使用
AcceptEx 的例程可能是这个样子的:do {
-Wait for a previous AcceptEx to complete //
等待前一个 AcceptEx 完成-Create a new socket and associate it with the completion port //
创建一个新的 Socket 并将其关联//
到完成端口-Allocate context structure etc. //
初始化相关的环境信息结构-Post an AcceptEx request. //
进入 AcceptEx 请求。}
while(TRUE);
一个服务器一直具备足够的
AcceptEx 调用,这样就可以立刻响应客户机的连接。 AcceptEx 操作的数量取决于服务器的策略。如果要满足高连接率(比如大量的短暂连接或爆发性的流量)的话,当然比不常发生连接的程序需要更多的 AcceptEx 入口。聪明的策略就是根据流量改变 AcceptEx 调用的数量,而避免只使用一个确定的数目。在
Win2000 上, Winsock 提供了一些帮助,用来判断 AcceptEx 调用的数量是否跟不上需要。当创建一个监听 Socket 之后,使用 WSAEventSelect 函数把它和一个 FD_ACCEPT 事件关联,如果没有 accept 未决的调用正在进行,一旦有请求到来,该事件( FD_ACCEPT )就会发生。因此此事件可以用来告诉开发人员:还需要进行更多的 AcceptEx 操作,或者由此探测到一个有异常行为的远端实体。注意:此机制在 NT 上是无效的。使用
AcceptEx 的显著好处是:在一次连接中就可以获取客户端的数据,见 AcceptEx 的 lpOutputBuffer 参数。这意味着如果客户端连接并立刻发送数据的话, AcceptEx 将在客户端连接成功和数据发送之后才完成。这个功能同时导致的问题是: AcceptEx 必须等待数据接受完成才能返回。因为一个带 Output 缓冲的 AcceptEx 函数并非一个“原子”操作,而是两步的过程:接受连接和等待数据。所以程序在数据接受之前并不会知道连接成功。当然客户端也可以连接到服务器而不马上发送数据,如果这样的连接过多,服务器将开始拒绝合法的连接,因为没有可用的未决的 Accept 操作入口。这也是一种常用的方法,通过拒绝访问,防止恶意攻击和海量连接。在正在接受连接的线程中,可以检查
AcceptEx 调用传入的 socket ,调用 getsockopt 检查其 SO_CONNECT_TIME, 该值返回的是 socket 连接的时间,没有连接的时候返回 -1 。根据
WSAEventSelect 机制所带来的特性,我们可以很容易的判断是否应该检查传到 AcceptEx 函数的 socket 句柄的连接时间。如果在一定时间里, AcceptEx 没有从某个连接中收到数据, AcceptEx 可以通过关闭该 socket 来断开连接。在不紧急的情况下,程序不应该关闭一个 AcceptEx 里处于未连接状态的 socket ,因为系统考虑到性能问题,关联在 AcceptEx 上的内核态数据结构不会被释放,直到一个新的连接到来或监听 socket 本身都关闭了。乍看起来,一个发出
AcceptEx 请求的线程同时也可以是一个关联在完成端口上,并且处理其他完成 IO 事件的工作线程。然而,最好不要设计这样一个线程。在 winsocket2 的层次结构上有一个副作用,那就是一个 socket/WSASocket API 的开销是相当可观的,每个 AccepEx 都需要创建一个新的 socket, 所以最好创建一个单独的跟其他 IO 处理无关的线程来调用 AcceptEx 。当然,你还可以利用这个线程来进行其他的工作如创建事件 log关于
AcceptEx 要注意的最后一个事情是: Winsock2 的其他供应商不一定会实现 AcceptEx 函数。同样情况也包括的其他 Microsoft 的特定 APIs 如 TransmitFile,GetAcceptExSockAddrs 以及其他 Microsoft 将在以后版本的 windows 里。在运行 WinNT 和 Win2000 的系统上,这些 APIs 在 Microsoft 提供的 DLL(mswsock.dll) 里实现,可以通过链接 mswsock.lib 或者通过 WSAioctl 的 SIO_GET_EXTENSION_FUNCTION_POINTER 操作动态调用这些扩展 APIs.
未获取函数指针就调用函数(如直接连接
mswsock..lib 并直接调用 AcceptEx )的消耗是很大的,因为 AcceptEx 实际上是存在于 Winsock2 结构体系之外的。每次应用程序常试在服务提供层上( mswsock 之上)调用 AcceptEx 时,都要先通过 WSAIoctl 获取该函数指针。如果要避免这个很影响性能的操作,应用程序最好是直接从服务提供层通过 WSAIoctl 先获取这些 APIs 的指针。TransmitFile
和TransmitPackets函数Winsock
提供了两个专为文件和内存数据传输而优化过的函数。 TransmitFile API 在 WinNT 和 Win2000 均有效,而 TransmitPackets 作为一个新的扩展函数,将在未来版本的 windows 里实现。TransmitFile
可以把文件的内容通过 socket 传输。一般情况下,如果应用程序通过 socket 传输文件,首先要用 CreateFile 打开文件,并循环调用 ReadFile 和 WSASend 函数,读取一段数据然后发送,直到整个文件发送完毕。这样的效率很低,因为 ReadFile 和 WSASend 调用都需要系统在用户态和核心态之间进行转换。 TransmitFile 则只需要知道需要传输的文件句柄和要传输的字节数,只有 CreateFile 打开文件获得句柄这个向核心态跃迁的这一个额外开销。如果你的程序需要通过 socket 发送大量文件,建议使用此函数。函数的原形如下:
BOOL TransmitFile(
SOCKET hSocket,
HANDLE hFile,
DWORD nNumberOfBytesToWrite,
DWORD nNumberOfBytesPerSend,
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
DWORD dwFlags
);
TransmitPackets API
比 TransmitFile API 更进一步,允许调用者一次指定多个文件句柄和内存缓冲区,并进行传输。原形如下:BOOL TransmitPackets(
SOCKET hSocket,
LPTRANSMIT_PACKET_ELEMENT lpPacketArray,
DWORD nElementCount,
DWORD nSendSize,
LPOVERLAPPED lpOverlapped,
DWORD dwFlags
);
lpPacketArray
包含结构体的数组。每个入口点指定一个需要被传输的文件句柄或内存缓冲,该结构的成员如下:typedef struct _TRANSMIT_PACKETS_ELEMENT {
DWORD dwElFlags;
DWORD cLength;
union {
struct {
LARGE_INTEGER nFileOffset;
HANDLE hFile;
};
PVOID pBuffer;
};
} TRANSMIT_FILE_BUFFERS;
各成员的名字都是自解释的。
dwEIFlags 成员指明结构体里的元素是一个文件句柄( TF_ELEMENT_FILE )还是一个内存缓冲 (TF_ELEMENT_MEMORY) 。 cLength 成员表示要传输的字节数(对于文件句柄, 0 则表示文件内所有的数据)。一个未命名的联合体 (union) 包含内存缓冲指针或文件句柄(以及指定的偏移量)。使用这两个函数的其他好处是,你可以通过指定
TF_REUSE_SOCKET 标志(必须同时指定 TF_DISCONNECT 标志)重用 socket 句柄。一旦 API 函数完成数据传输,连接就会在传输点的层次上断开,然后该 socket 就可以被 AcceptEx 重新使用。这样就可以减少反复创建 socket 和的次数,优化了效率。使用这两个函数要注意的是,在
WinNT Workstation 版本或 win2000 Professional 版本上,并不能实现优化性能。必须在 winNT,win2000 Server ,Win2000 Advanced Server 或 Win2000 Data Center 版本上才能实现。
来实现一个服务方案
在前几章里,我们介绍了一些有益于改善性能提高扩展性的
APIs 和方法,以及可能遇到的资源瓶颈。这对你有用吗?当然,首先取决于你的服务端和客户端的设计。在设计时,你对服务端和客户端的控制越得力,你就能更好的避免瓶颈让我们来看一种简单的用例,在这个用例里我们设计一个服务器,这个服务器处理客户端的连接,然后客户端发送一次数据,并期待服务端的回应,然后客户端断开连接。
我们的设计是:服务器创建一个监听
socket ,并和一个完成端口相关联,然后创建和 CPU 同等数量的工作线程,以及一个专门用来进行 AcceptEx 调用的线程。既然我们知道客户端一旦连接马上就会发送数据 ,那么准备一个接受缓冲区会有利于工作的进行。当然,不要忘记经常的检查正在连接的 socket 的 SO_CONNECT_TIME 值,避免死连接。本设计中重要的一项是决定需要显形调用多少个
AcceptEx 。因为每个 AcceptEx 操作都需要一个接收缓冲区,大量的页面将被锁定(还记得每个重叠操作都会消耗一些非分页内存,并且会将一些数据缓冲锁定到内存里吗)。没有公式和具体的准则指导如何确定究竟允许多少个 AcceptEx 操作。最好的方案就是使这个数目成为可调的,通过性能测试,寻找一个在典型的环境下最好的值现在已经确定服务器是如何处理连接的了,下一步就是发送数据。影响发送数据的重要因素就是你期望服务器能够并发的处理连接数。一般来说,服务器应该限制并发的连接数量,以及显式的
send 调用。越多的连接数意味着越多的非分页内存的使用,并发的 send 调用也应该被限制,避免冲击系统的可分页内存锁定极限。连接数和并发的 send 调用限制也都应该是程序可调节的。在本例的情况里,不必要去取消每个
socket 的接收缓冲,因为接收事件仅仅在 AcceptEx 调用中发生。保证每个 socket 都有一个接收缓冲不会造成什么危害。一旦客户端 / 服务器在最初的一次请求(由 AcceptEx 完成)之后进行交互,发送更多的数据,那么取消接收缓冲更是一个很不好的做法。除非你能保证这些数据都是在每个连接的重叠 IO 接收里完成的 。
结束语:
重复:开发一个可扩展的
Winsock 服务器并非十分困难的。仅仅是开始一个监听 socket, 接收连接,并且进行重叠发送和接收的 IO 操作。最大的挑战就是管理系统资源,限制重叠 Io 的数量,避免内存危机。遵循这几个原则,就能帮助你开发高性能,可扩展的服务程序。--------------------------------------------------------------------------------