Winsock 提供两个专门为文件和内存数据传输进行了优化的函数。其中TransmitFile()这个API函数在Windows NT 4.0 和 Windows 2000上都可以使用,而TransmitPackets()则将在未来版本的Windows中实现。

TransmitFile()用来把文件内容通过Winsock进行传输。通常发送文件的做法是,先调用CreateFile()打开一个文件,然后不断循环调用ReadFile() 和WSASend ()直至数据发送完毕。但是这种方法很没有效率,因为每次调用ReadFile() 和 WSASend ()都会涉及一次从用户模式到内核模式的转换。如果换成TransmitFile(),那么只需要给它一个已打开文件的句柄和要发送的字节数,而所涉及的模式转换操作将只在调用CreateFile()打开文件时发生一次,然后TransmitFile()时再发生一次。这样效率就高多了。

TransmitPackets()比TransmitFile()更进一步,它允许用户只调用一次就可以发送指定的多个文件和内存缓冲区。函数原型如下:
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;
其中各字段是自描述型的(self explanatory)。
dwElFlags字段:指定当前元素是一个文件句柄还是内存缓冲区(分别通过常量TF_ELEMENT_FILE 和TF_ELEMENT_MEMORY指定);
cLength字段:指定将从数据源发送的字节数(如果是文件,这个字段值为0表示发送整个文件);
结构中的无名联合体:包含文件句柄的内存缓冲区(以及可能的偏移量)。

使用这两个API的另一个好处,是可以通过指定TF_REUSE_SOCKET和TF_DISCONNECT标志来重用套接字句柄。每当API完成数据的传输工作后,就会在传输层级别断开连接,这样这个套接字就又可以重新提供给AcceptEx()使用。采用这种优化的方法编程,将减轻那个专门做接受操作的线程创建套接字的压力(前文述及)。

这两个API也都有一个共同的弱点:Windows NT Workstation 或 Windows 2000 专业版中,函数每次只能处理两个调用请求,只有在Windows NT、Windows 2000服务器版、Windows 2000高级服务器版或 Windows 2000 Data Center中才获得完全支持。

放在一起看看

以上各节中,我们讨论了开发高性能的、大响应规模的应用程序所需的函数、方法和可能遇到的资源瓶颈问题。这些对你意味着什么呢?其实,这取决于你如何构造你的服务器和客户端。当你能够在服务器和客户端设计上进行更好地控制时,那么你越能够避开瓶颈问题。

来看一个示范的环境。我们要设计一个服务器来响应客户端的连接、发送请求、接收数据以及断开连接。那么,服务器将需要创建一个监听套接字,把它与某个完成端口进行关联,为每颗CPU创建一个工作线程。再创建一个线程专门用来发出AcceptEx()。我们知道客户端会在发出连接请求后立刻传送数据,所以如果我们准备好接收缓冲区会使事情变得更为容易。当然,不要忘记不时地轮询AcceptEx()调用中使用的套接字(使用SO_CONNECT_TIME选项参数)来确保没有恶意超时的连接。

该设计中有一个重要的问题要考虑,我们应该允许多少个AcceptEx()进行守候。这是因为,每发出一个AcceptEx()时我们都同时需要为它提供一个接收缓冲区,那么内存中将会出现很多被锁定的页面(前文说过了,每个重叠操作都会消耗一小部分未分页内存池,同时还会锁定所有涉及的缓冲区)。这个问题很难回答,没有一个确切的答案。最好的方法是把这个值做成可以调整的,通过反复做性能测试,你就可以得出在典型应用环境中最佳的值。

好了,当你测算清楚后,下面就是发送数据的问题了,考虑的重点是你希望服务器同时处理多少个并发的连接。通常情况下,服务器应该限制并发连接的数量以及等候处理的发送调用。因为并发连接数量越多,所消耗的未分页内存池也越多;等候处理的发送调用越多,被锁定的内存页面也越多(小心别超过了极限)。这同样也需要反复测试才知道答案。

对于上述环境,通常不需要关闭单个套接字的缓冲区,因为只在AcceptEx()中有一次接收数据的操作,而要保证给每个到来的连接提供接收缓冲区并不是太难的事情。但是,如果客户机与服务器交互的方式变一变,客户机在发送了一次数据之后,还需要发送更多的数据,在这种情况下关闭接收缓冲就不太妙了,除非你想办法保证在每个连接上都发出了重叠接收调用来接收更多的数据。

结论

开发大响应规模的Winsock服务器并不是很可怕,其实也就是设置一个监听套接字、接受连接请求和进行重叠收发调用。通过设置合理的进行守候的重叠调用的数量,防止出现未分页内存池被耗尽,这才是最主要的挑战。按照我们前面讨论的一些原则,你就可以开发出大响应规模的服务器应用程序。