翻译说明:
完成端口基本上公认为一种在
windows服务平台上比较成熟和高效的IO方法,理解和编写程序都不是很困难。目前我正在进行这方面的实践,代码还没有完全调试和评价,只有这一篇拙劣的学习翻译文摘,见笑见笑。
翻译这个文章,是因为我近期在学习一些
socket服务程序的编写中发现(注意,只是在学习,我本人在这个领域经验并不充足到可以撰文骗钱的地步:P),如果不是逼着自己把这个文章从头翻译一遍,我怀疑我是否能认真领会本文的内容 :PPP. 把这个文章贴出来,不是为了赚人气,而是因为水平确实有限,虽然整体上大差不差的翻译出来了,但是细节和用词上可能还是有很多问题。是希望大家能指出其中的翻译错误和理解谬误,互相交流和帮助。非常感谢。
本文翻译并没有通过原作者同意,仅用来在网络上学习和交流,加之翻译水平拙劣,所以请勿用于做商业用途。
vcbear
2001.8
Windows Sockets 2.0:
使用完成端口高性能,可扩展性Winsock服务程序
原作者:
Anthony Jones 和Amol Deshpande
原文在http://msdn.microsoft.com/msdnmag/issues/1000/winsock/winsock.asp
- APIs
和扩展性
- 完成端口(
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原形,加粗的即为需要传入的socket
BOOL 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的数量,避免内存危机。遵循这几个原则,就能帮助你开发高性能,可扩展的服务程序。