完成端口
一、什么是完成端口
从本质上讲,完成端口是一种异步I/O技术,它提供一个内核对象,可以关联多个I/O设备,同时关联一个线程池,线程池中的线程通常处于睡眠状态,当有I/O出现时,完成端口唤醒等待线程队列中的线程进行处理。完成端口有着良好的伸缩性灵活性以及较高的效率,一般用来创建大型的服务器。
我们知道,一个服务器应用程序结构可以分为串行模式和并发模式。在串行模式中,一次只能处理一个请求,第二个请求必须等待第一个请求被处理完毕才能开始处理,适合于客户量比较小的情况;在并发模式中,针对每个请求创建一个线程,使得多个请求可以同时得到处理,因而提高了程序的性能。
但是,我们再进一步思考,如果有多个设备同时发出IO请求,那么在并发模式中也必须创建与之相同个数的线程,但是,CPU的个数是有限的,多于CPU个数的可运行线程就没有意义了,系统不得不在多个线程间进行上下文切换,以使得多个线程并发执行,这必然浪费宝贵的CPU周期。另外,虽然创建线程较进程而言开销要小,但也并不意味着没有开销,尤其当数量比较大的时候。
在完成端口模型中,引入了线程池的概念,在应用程序初始化时创建一个线程池,在没有请求时处于等待状态,当请求完成时唤醒一个线程运行,运行完毕后重新放入线程池中,等待其他请求使用。由于不必为每个请求创建一个线程,从而减少了线程的数量,省去了运行中途创建线程的开销,进一步提高了程序的性能。
二、完成端口的内部结构
由于完成端口也是一个内核对象,故我们看一下它的内部结构。完成端口对象包含五个不同的数据结构:
1、设备列表:
表相:设备句柄、完成键。
当调用CreateIoCompletionPort时将设备与完成端口关联起来,同时在该数据结构中创建一项。每当向完成端口关联一个设备时,系统向该完成端口的设备列表中加入一条信息。
2、I/O完成队列:
表相:传输的字节数、32位完成键、I/O请求的OVERLAPPED结构指针、错误代码
当一个设备的异步I/O请求完成时,系统检测该设备是否关联了一个完成端口,如果是,系统就向该完成端口的I/O完成队列中加入完成的I/O请求项。
3、等待线程队列:
表相:线程句柄
当线程池中的一个线程调用GetQueuedCompletionStatus时,调用线程的ID被放入等待线程队列,I/O完成端口内核对象总是知道哪个线程正在等待处理完成的I/O请求。线程将得到完成的I/O项中的信息(传输的字节数、32位完成键、I/O请求的OVERLAPPED结构指针、错误代码)。该信息通过传递给GetQueuedCompletionStatus的lpdwNumberOfBytesTransferred, lpdwCompletionKey和lpOverlapped参数返回线程。
4、释放线程列表
表相:线程句柄
当完成端口唤醒一个线程时,把该线程ID放入释放线程列表中。
5、暂停线程列表
表相:线程句柄
如果一个释放线程使自己进入等待状态,完成端口将该线程ID放入暂停线程列表。
三、与完成端口模型相关的数据结构
谈到完成端口模型就不能不提到与之相关的两个数据结构单句柄数据和单IO数据。
单句柄数据,是用于与IO设备句柄关联的数据结构,当调用CreateIoCompletionPort函数关联完成端口时与将设备与完成端口关联起来,并作为一项储于设备队列中,在获取数据函数GetQueuedCompletionStatus中参数lpCompletionKey传递了关联函数中所关联的设备相关信息,该参数可以是自定义的与设备相关的任何信息。
单I/O数据,用于记录重叠IO结构以及重叠IO操作中操作的数据。在投递函数中,与特定设备的特定操作关联起来,在获取数据函数GetQueuedCompletionStatus中参数lpOverlapped传递了投递函数中关联的重叠IO结构,在使用中,一般将该参数设置为数据结构,并将重叠IO结构作为该结构的第一项,将投递函数的接受缓冲区和缓冲区大小以及操作类型作为该结构的其他项。在投递函数中取该结构的地址并强制类型转换为重叠IO结构指针,在获取数据函数中,以同样的方式获取该结构,并处理该结构中的数据。
四、完成端口编程步骤
1、创建一个完成端口,并将第四个参数设置为0,它指定在完成端口上每个处理器一次只允许执行一个工作器线程。
2、判断系统有多少个处理器
3、根据处理器个数,创建两倍的工作线程。
4、准备好套接字监听连接的客户请求。
5、运用accept函数,接收连接请求。
6、创建数据结构,并存入刚接收的套接字句柄,以及客户端地址等信息。
7、调用CreateIoCompletionPort函数将该套接字句柄通过CompletionKey参数传递给完成端口,将套接字与完成端口关联在一起。
8、在该连接上投递WSARecv或WSASend请求,并通过单I/O数据传递需要传递的数据。
9、重复5~8操作。
五、简单例子
#include <winsock2.h>
typedef struct _PerHandle
{
SOCKET m_sock;
sockaddr_in m_addr;
}PerHandle, *PtrPerHandle;
typedef struct _PerIO
{
OVERLAPPED m_overlapped;
char buf[512];
int m_operationType;
#define OP_READ 1
#define OP_WRITE 2
#define OP_ACCEPT 3
}PerIO, *PtrPerIO;
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
UINT WINAPI ServerThread(PVOID pvParam);
void main()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
{
cout<<"failed to load winsock !"<<endl;
exit(0);
}
/*创建完成端口对象*/
HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
/*创建服务线程,构建服务线程池*/
_beginthreadex(NULL, 0, ServerThread, (LPVOID)hCompletion, 0, 0);
/*创建监听套接字,并监听连接*/
SOCKET myListen = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in localAddr;
localAddr.sin_family = AF_INET;
localAddr.sin_port = ntohs(5500);
localAddr.sin_addr.S_un.S_addr = inet_addr("59.73.161.221");
bind(myListen, (sockaddr *)&localAddr, sizeof(localAddr));
listen(myListen, 5);
cout << "server is listening......" << endl;
/*接受客户端的连接,并将读写操作投放到重叠IO中*/
while(true)
{
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
SOCKET newClient = accept(myListen, (sockaddr *)&clientAddr, &clientAddrLen);
/*将套接字与完成端口关联*/
PtrPerHandle ptrPerHandle = new PerHandle();
ptrPerHandle->m_sock = newClient;
ptrPerHandle->m_addr = clientAddr;
CreateIoCompletionPort((HANDLE)ptrPerHandle->m_sock, hCompletion, (ULONG_PTR)ptrPerHandle, 0);
/*投放异步重叠IO*/
PtrPerIO ptrPerIO = new PerIO();
ptrPerIO->m_operationType = OP_READ;
WSABUF buf;
buf.buf = ptrPerIO->buf;
buf.len = 512;
DWORD dwRecv;
DWORD dwFlag = 0;
::WSARecv(ptrPerHandle->m_sock, &buf, 1, &dwRecv, &dwFlag, &ptrPerIO->m_overlapped, NULL);
}
}
/********************************************************************************
* 函数介绍:服务线程函数,平常处于等待状态,当IO操作完成后,完成数据处理。
* 输入参数:
PVOID pvParam:完成端口对象。
* 输出参数:无
* 返回值 :用于符合线程函数的返回值要求,成功返回0,否则返回-1。
*********************************************************************************/
UINT WINAPI ServerThread(PVOID pvParam)
{
HANDLE hCompletion = (HANDLE)pvParam;
DWORD dwTrans;
PtrPerHandle ptrPerHandle;
PtrPerIO ptrPerIO;
while(true)
{
/*阻塞线程直到有IO操作完成,并通过参数返回操作结果。*/
bool ret = ::GetQueuedCompletionStatus(hCompletion, &dwTrans, (LPDWORD)&ptrPerHandle, (LPOVERLAPPED *)&ptrPerIO, WSA_INFINITE);
if(!ret)
{
closesocket(ptrPerHandle->m_sock);
delete(ptrPerHandle);
delete(ptrPerIO);
continue;
}
/*读或写数据为空*/
if(dwTrans == 0 &&(ptrPerIO->m_operationType == OP_READ || ptrPerIO->m_operationType == OP_WRITE))
{
closesocket(ptrPerHandle->m_sock);
delete(ptrPerHandle);
delete(ptrPerIO);
continue;
}
switch(ptrPerIO->m_operationType)
{
case OP_READ:
{
ptrPerIO->buf[dwTrans] = '/0';
cout << ptrPerIO->buf << endl;
WSABUF buf;
buf.buf = ptrPerIO->buf;
buf.len = 512;
DWORD dwRecv;
DWORD dwFlag = 0;
::WSARecv(ptrPerHandle->m_sock, &buf, 1, &dwRecv, &dwFlag, &ptrPerIO->m_overlapped, NULL);
}
break;
case OP_WRITE:
case OP_ACCEPT:
break;
}
}
return 0;
}