翻译说明:
完成端口基本上公认为一种在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操作完成的事件通知
放到该队列里,由于是暴露 “操作完成”的事件通知,所以命名为“完成端口”?–Ompletion Ports)。一个socket被创建后,可以在任何时刻和一个完成端口联系起
来。
一般来说,一个应用程序可以创建多个工作线程来处理完成端口上的通知事件。工作线
程的数量依赖于程序的具体需要。但是在理想的情况下,应该对应一个CPU创建一个线
程。因为在完成端口理想模型中,每个线程都可以从系统获得一个“原子”性的时间片,
轮番运行并检查完成端口?
叱痰那谢皇嵌钔獾目 T谑导士 ⒌氖焙颍 挂 悸钦庑┫叱淌欠袂I娴狡渌 氯 僮鞯那榭觥H绻 诚叱探 卸氯 僮鳎 低吃蚪 涔移穑 帽鸬南叱袒竦迷诵惺奔洹R虼耍 绻 姓庋 那榭觯 梢远啻唇 父鱿叱汤淳×坷 檬奔洹?
应用完成端口分两步走:
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缓冲的根本意图(避免缓冲复制)就落空了。关闭接收缓冲
是没有必要的,只要应用?绦蚓 S幸馐兜脑谝桓隽 由系饔弥氐 SARecvs操作,这样就避免了AFD老是要缓冲大
量的到来数据。
到这里,我们应该清楚关闭缓冲的方法对绝大多数应用程序来说没有太多好处的了。
然而,一个高性能的服务程序可以关闭发送缓冲,而不影响性能。这样的程序必须确保
它在同时执行多个Overlapped发送,而不是等待一个Overlapped发送结束之后,才执行
另一个。这样如果一个数据缓冲区数据已经被提交,那么传输层就可以立刻使用该数据
缓冲区。如果程序“串行”的
执行Overlapped发送,就会浪费一个发送提交之后另一个发送执行之前那段时间。
资源约束
鲁棒性是每一个服务程序的一个主要设计目标。就是说,服务程序应该可以对付任何的
突发问题,比如,客户端请求的高峰,可用内存的暂时贫缺,以及其他可靠性问题。为
了平和的解决这些问题,开发人员必须了解典型的WindowsNT和Windows2000平台上的资
源约束。
最基本的问题是网络带宽。使用UDP协议进行发送的服务程序对此要求较高,因为这样的
服务程序要求尽量少的丢包率。即使是使用TCP连接,服务器也必须注意不要滥用网络资
源。否则,TCP连接中将会出现大量重发和连接取消事件。具体的带宽控制是跟具体程序
相关的,超出了本文的讨?鄯段А?
程序所使用的虚拟内存也必须小心。应该保守的执行内存申请和释放,或许可以使用旁?恿斜恚ㄒ桓黾锹忌昵氩⑹褂霉 摹翱障小蹦诖娴幕撼迩 ├粗赜靡丫 昵氲 潜怀绦蚴褂霉 障辛说哪诖妫 庋 梢允狗 癯绦虮苊夤 嗟姆锤瓷昵肽诖妫 ⑶冶Vは低持幸恢庇芯】赡芏嗟目沼嗄诖?。(应用程序还可以使用SetWorkingSetSize这个Win32API函数来向系统请求增加该程序
可用的物理内存。)
有两个winsock程序不会直接面对的资源约束。第一个是页面锁定限制。
无论应用程序发起send还是receive操作,也不管AFD.SYS的缓冲是否被禁止,数据所在
的缓冲都会被锁定在物理内存里。因为内核驱动要访问该内存的数据,在访问期间该内
存区域都不能被解锁。在大部分情况下,这?换岵 魏挝侍狻5 遣僮飨低潮匦肴啡匣褂锌捎玫目煞忠衬诖胬刺峁└ 渌 绦颉U庋 龅哪康氖欠乐挂桓鲇写砦蟛僮鞯某绦蚯肭笏 ㄋ 械奈锢鞷AM,而导致系统崩溃。这意味着,应用程序必须有意识的避免导致过多页面锁定,使该数量达到或超过系统限制。
在WinNT和Win2000中,系统允许的总共的内存锁定的限制大概是物理内存的1/8。这只是
粗略的估计,不能作为一个准确的计算数据。只是需要知道,有时重叠IO操作会发生
ERROR_INSUFFICIENT_RESOURCE失败,这是因为可能同时有太多的send/receives操作在
进行中。程序应该注意避免这?智榭觥?
另一个的资源限制情况是,程序运行时,系统达到非分页内存池的限制。WinNT和
Win2000的驱动从指定的非分页内存池中申请内存。这个区域里分配的内存不会被扇出,
因为它包含了多个不同的内核对象可能需要访问的数据,而有些内核对象是不能访问已
经扇出的内存的。一旦系统创建了
一个socket
(或打开一个文件),一定数目的非分页内存就被分配了。另外,绑定(binding)和连接
socket也会导致额外的非分页内存池的分配。更进一步的说,一个I/O请求,比如send或
receive,也是分配了很少的一点非分页内存池的(为了跟踪I/O操作的进行,包含必须信
息的一个很小的结构体被?峙淞耍 ; 俪啥啵 詈蠡故强赡艿贾挛侍狻R虼瞬僮飨低诚拗屏朔欠忠衬诖娴氖 俊T趙inNT和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提供的D
LL(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完成)之后进行交互,发送更多的数据,那么取消接收
缓冲更是一个很不好的做法?
3 悄隳鼙Vふ庑┦ 荻际窃诿扛隽 拥闹氐蘒O接收里完成的 。
结束语:
重复:开发一个可扩展的Winsock服务器并非十分困难的。仅仅是开始一个监听socket,
接收连接,并且进行重叠发送和接收的IO操作。最大的挑战就是管理系统资源,
限制重叠Io的数量,避免内存危机。遵循这几个原则,就能帮助你开发高性能,
可扩展的服务程序。