IOCP
- 当应用程序必须一次管理多个套接字时,完成端口模型提供了最好的系统性能,这个模型也提供了最好的伸缩性,非常适合用来出来上百上千的套接字。IOCP技术广泛应用于各种高性能服务器,如Apache等
什么是完成端口
- I/O完成端口是应用程序使用线程池处理异步I/O请求的一种机制,处理多个并发异步I/O请求时,使用I/O完成端口比在I/O请求时创建线程更快更有效。
- I/O完成端口最初的设计是应用程序发出一些异步I/O请求,当这些请求完成时设备驱动将这些工作项目排序到完成端口,这样在完成端口上等待的线程池便可以处理这些I/O。完成端口是一个windowsI/O结构,它可以接收多种对象的句柄。如文件对象套接字对象等
完成端口
- 使用完成端口模型,首先调用CreateIoCompletionPort函数创建了一个完成端口对象,Winsock使用这个对象为任意数量的套接字句柄管理I/O请求
HANDLE CreateIoCompletionPort(HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
- 这个函数有两个不同的功能
- 创建一个完成端口对象
- 将一个或多个文件句柄,关联到I/O完成端口对象
- 创建完成端口对象时,唯一需要设置的参数是NumberOfConcurrentThreads,它定义了允许在完成端口上同时 执行的线程的数量。理想情况下希望每个处理器仅运行一个线程来为完成端口提供服务,以避免在线程上下切换。NumberOfConcurrentThreads为0标识系统允许的线程数量与处理器数量一样多。
HANDLE hCompletion=::CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0);
I/O服务线程和完成端口
- 成功创建完成端口对象之后,便可以向这个对象关联套接字句柄。在关联套接字之前,需要创建一个或者多个工作线程(称为I/O服务线程),在完成端口上执行并处理完成投递到完成端口的I/O请求。前面推荐的线程数量为处理器数量,CreateIoCompletionPort的参数NumberOfConcurrentThreads 明确告诉系统运行在完成端口行运行的线程数量。如果创建的线程多余NumberOfConcurrentThreads ,也就仅有NumberOfConcurrentThreads 个线程在运行。
- 如果某个函数调用了sleep或者WaitForSingleObject,进入了暂停状态,多出来的线程就会有一个运行,占据休眠线程的位置。工作线程如果会遇到阻塞(进入暂停状态),应该创建比CreateIoCompletionPort指定数量还要多的线程。
- 有了足够多的工作线程来处理完成端口上的I/O请求,就该为完成端口关联套接字句柄
- FileHandle:要关联的套接字句柄
- ExistingCompletionPort:上面创建的完成端口对象句柄
- CompletionKey:指定一个句柄唯一数据,它将FileHandle套接字句柄关联在一起,应用程序可以再次存储任意类型的信息,通常是一个指针。
- CompletionKey:参数通常用来描述与套接字相关的信息,所有称它为句柄唯一(per-handle)数据,在后面的例子代码中,可以看到他的作用。
完成端口和重叠I/O
- 完成端口关联套接字句柄之后,便可以在套接字上投递重叠发送和接收请求处理I/O。这些I/O操作完成时,I/O系统会向完成端口对象发送一个完成通知封包。I/O完成端口以先进先出的方式为这些封包排队。应用程序GetQueuedCompletionStatus函数可以取得这些队列的封包。这个函数应该在处理完成对象I/O的服务线程中调用。
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,//完成端口对象句柄
LPDWORD lpNumberOfBytes,//取得I/O操作期间传输的字节数
PULONG_PTR lpCompletionKey,//取得 在关联套接字时指定的句柄唯一数据
LPOVERLAPPED* lpOverlapped,//取得投递I/O操作时指定的OVERLAPPED结构
DWORD dwMilliseconds//如果完成端口没有取得完成封包,此参数指定了等待事件,INFINITE为无穷大。
)
- I/O服务线程调用GetQueuedCompletionStatus函数取得所有事件发生的套接字信息,通过lpNumberOfBytes参数得到传输的字节数量,通过lpCompletionKey参数得到与套接字关联句柄唯一(per-handle)数据,通过lpOverlapped参数得到投递I/O请求时使用重叠对象地址。
- lpCompletionKey参数包含了我们称为per-handle的数据,因为当套接字第一次与完成端口关联时,这个数据就关联到一个套接字句柄。这是传递给CreateIoCompletionPort函数的CompletionKey参数,可以给这个参数传递任意类型的数据。
- lpOverlapped参数指向一个OVERLAPPED结构,结构后面便是我们称为per-I/O的数据,这可以是工作线程处理完成封包时想要知道的任何信息。
扩展函数
- WindowsSocket2规范定义了一种扩展机制,允许windows套接字服务提供者向应用程序设计者导出先进的数据传输功能。Microsoft通过使用这些扩展机制提供了一些扩展函数。如果要使用这些函数,开发的程序就会限制在支持这些函数的winsock提供者上运行。有些扩展函数从Winsocke1.1开始就出现了。从MSWSOCK.dll导出,然而并不建议直接链接到这个DLL,这回将程序绑定在Microsof Winsock提供者上,应用程序应该使用WSAIoctl函数动态加载他们。
- 扩展API函数的特征帮助开发可伸缩的服务器程序
GetAcceptExSockaddrs函数
- GetAcceptExSockaddrs函数粘贴从AcceptEx函数取得的数据,将本地和远程地址传递到sockaddr结构。
void GetAcceptExSockaddrs(
PVOID lpOutputBuffer,//指向传递给AcceptEx函数接收客户第一块数据的缓冲区
DWORD dwReceiveDataLength,//lpOutputBuffer缓冲区大小,必须和传递给AcceptEx函数的一致
DWORD dwLocalAddressLength,//为本地地址预留的空间大小
DWORD dwRemoteAddressLength,//为远程地址预留的空间大小
LPSOCKADDR* LocalSockaddr,//用来返回连接的本地地址
LPINT LocalSockaddrLength,//用来返回本地地址的长度
LPSOCKADDR* RemoteSockaddr,//用来返回远程地址
LPINT RemoteSockaddrLength,//用来返回远程地址的长度
)
- GetAcceptExSockaddrs是专门为AcceptEx函数准备的,它将AcceptEx接收的第一块数据的本地和远程机器的地址返回给用户。
TransmitFile
- TransmitFile在一个已连接的套接字句柄上传输文件数据,该函数使用操作系统的缓存来获取文件数据,在套接字上提供高性能的文件传输。
BOOL TransmitFile(
SOCKET hSocket,//一个连接套接字的句柄,TransmitFile将在这个套接字上传输文件数据
HANDLE hFile,//已打开的文件句柄
DWORD nNumberOfBytesToWrite,//要传输的字节数,设置为0,就传输整个文件
DWORD nNumberOfBytePerSend,//每次发送的数据快的大小,设置为0就选择默认的大小
LPOVERLAPPED lpOverlapped,//如果套接字是重叠方式创建的,指定这个参数进行异步发送,默认是重叠创建
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,//指定在文件数据发送之前和之后要发送的数据
DWORD dwFlags//标志
)
- 如果文件句柄hFile为NULL,lpTransmitBuffers将会被传输,lpOverappend结构是可选的,如果省略了次结构,文件传输会从当前文件指针位置开始,不然的话OVERLAPPED结构中的偏移量值将指定操作从哪开始。
- 最后一个参数可选标志,影响文件的操作行为
- TF_DISCONNECT:所有文件数据排队准备传输,开启一个传输级别的断开
- TF_REUSE_SOCKET:准备重新使用这个套接字句柄,当TransmitFile请求完成时,这个套接字句柄可以作为AcceptEx中客户端的套接字使用,仅当同时设置了TF_DISCONNECT这个标志才有效
- TF_USE_DEFAULT_WORKER:指示文件传是使用系统默认的线程,这对大文件传输有用
- TF_USE_SYSTEM_TREAD:这个选项也指示TransmitFile操作使用系统线程来处理
- TF_USE_KERNEL_APC:指示应该使用内核异步调用,而不使用工作线程来处理TransmitFile请求。内核APC尽在应用程序处于等待状态时才调用
- TF_WRITE_BEHIND:指示TransmitFile请求应该立即返回,即便是数据还没有被远端确认。这个标志不应该和TF_DISCONNECT或者TF_REUSE_SOCKET标志在一起使用。
- 在基于文件I/O的程序TransmtFile函数非常有用,另外还有一个特性是同时指定TF_DISCONNECT和TF_REUSE_SOCKET两个标志。指定它们后,文件或缓冲区的数据要被传输,一旦操作完成套接字断开。传递给此API套接字句柄可以作为客户端套接字在AcceptEx函数中使用,或者作为连接套接字ConnectEx函数中使用。
- 调用TransmitFile时,可以令文件句柄为NULL,lpTransmitBuffers也为NULL,但是仍要指定TF_DISCONNECT和TF_REUSE_SOCKET。这个调用不会发送任何数据,但是允许套接字在AcceptEx调用。
TransmitPackets
- TransmitPackets扩展函数与TransmitFile类似,因为他也用来发送数据。不同之处在于TransmitPackets既可以发送文件又可以发送缓冲区数据
BOOL PASCAL TransmitPackets(
SOCKET hSocket,//已经建立连接的套接字句柄,并不要求是面向连接的套接字
LPTRANSMIT_PACKETS_ELEMENT lpPacketArray,//封包元素数组,描述了要传输的数据
DWORD nElementCount,//lpPacketArray数组中元素的数量
DWORD nSendSize,//每次发送数据的大小
LPOVERLAPPED lpOverlapped,//同TransmitFile中的参数一样,是可选的重叠结构
DWORD dwFlags//标志
)
- lpPacketArray是TRANSMT_PACKETS_ELEMENT结构类型的数组,该结构定义如下
typedef struct _TRANSMIT_PACKETS_ELEMENT{
ULONG dwEIFlags;//指定了此元素包含的缓冲区类型,或者是内存或文件
#define TP_ELEMENT_MEMORY 1
#define TP_ELEMENT_FILE 2
#define TP_ELEMENT_EOP 4
ULONG cLength;//指示从文件的内存缓冲区要传输多少字节,如果元素包含文件之后cLength为0表示传输整个文件
union{
struct{
LARGE_INFTEGER nFileOffset;
HANDLE hFile;
};
PVOID pBuffer;
};
}TRANSMIT_PACKETS_ELEMENT;
- dwElFlags用来描述数组元素包含的缓冲区类型。TP_ELEMENT_EOP标志可以和另外两个标志中的一个按位或,指示在发送操作中这个元素不应该和后面的元素混合起来。嵌在结构中的联合包含一个到内存中缓冲区的指针或者一个打开文件的句柄,以及文件的偏移值。可以在多个元素中使用同一个文件句柄。-1为从当前文件指针位置开始传输。
ConnectEx
- ConnectEx函数为指定的套接字建立连接,连接建立之后也可以发送数据(称为连接数据)。这个函数仅面向连接的套接字。
BOOL PASCAL ConnectEx(
SOCKET s,//一个未连接的、已绑定的套接字句柄
const STRUCT SOCKADDR* name;//要连接的远程地址
int namelen;//远程地址的长度
PVOID lpSendBuffer,//建立连接之后要发送的数据
DWORD dwSendDataLength,//lpSendBuffer中数据的长度,当lpSendBuffer不为NULL时使用
LPDWORD lpdwBytesSend,//返回从lpSendBuffer中发送的字节数
LPOVERLAPPED lpOverlapped//用来处理请求的重叠结构,必须指定
)
- ConnectEx函数允许重叠的连接调用,这是它最大的特性,同AcceptEx函数一样ConnectEx也是为了提高程序的性能而设计的,它将多个函数的功能组合在一个API调用中
DisconnectEx
- DisconnectEx函数关闭套接字上的连接,允许使用套接字句柄,函数定义如下:
BOOL DisconnectEx(
SOCKET hSocket,//已建立连接的,面向连接的套接字句柄
LPOVERLAPPED lpOverlapped,//如果套接字是以重叠方式创建的,指定这个参数以进行重叠I/操作
DWORD dwFlags,//标志,仅有的一个可选标志是TF_REUSE_SOCKET
DWORD reserved//保留
)