cookbook只讲step by step创建一个完成端口模块(网络)。
意识到线程切换的巨大代价,NT小组开发了完成端口这个内核级的东西。我们平时使用比较多的是如下三个API:
WINBASEAPI __out HANDLE WINAPI CreateIoCompletionPort( __in HANDLE FileHandle, __in_opt HANDLE ExistingCompletionPort, __in ULONG_PTR CompletionKey, __in DWORD NumberOfConcurrentThreads ); WINBASEAPI BOOL WINAPI GetQueuedCompletionStatus( __in HANDLE CompletionPort, __out LPDWORD lpNumberOfBytesTransferred, __out PULONG_PTR lpCompletionKey, __out LPOVERLAPPED *lpOverlapped, __in DWORD dwMilliseconds ); WINBASEAPI BOOL WINAPI PostQueuedCompletionStatus( __in HANDLE CompletionPort, __in DWORD dwNumberOfBytesTransferred, __in ULONG_PTR dwCompletionKey, __in_opt LPOVERLAPPED lpOverlapped );
时间顺序上,完成端口可以理解为,首先我们告诉操作系统要做一件事情( CreateIoCompletionPort),为了获取该事情的处理结果或者结果数据,我们使用GetQueuedCompletionStatus阻塞等待操作的完成,操作系统做完指定的任务后,通过PostQueuedCompletionStatus方法返回给我们定制的信息。
这只是时间顺序上的逻辑模拟解释,操作系统做些事情的时候要做的东西我就不知道了。既然如此,那开发一个基于完成端口的网络模块应该有如下步骤:
1.初始化winsock2库。
这个老生常谈了,但是我在开发的时候还是往往忘了这个,调试异常的时候才意识到忘了初始化环境了。在开发网络程序的时候,我们总要使用这样的函数对:
int _tmain(int argc, _TCHAR* argv[]) { WSAData wd; WSAStartup(MAKEWORD(2,2),&wd); //初始化 //your code WSACleanup(); //清理 return 0; }
2.初始化监听socket相关(初始化服务器环境)
网络编程非常常见的东西:
m_ssock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); sockaddr_in saddr; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_family = AF_INET; saddr.sin_port = htons(2350); bind(m_ssock,(sockaddr*)&saddr,sizeof(saddr)); listen(m_ssock,5);
3.把服务器socket绑定在完成端口上
这涉及完成端口句柄的创建以及socket句柄的绑定。当把一个完成句柄绑定在完成端口句柄上的时候,我们可以传递一个CompletionKey参数给CreateIoCompletionPort函数,以后GetQueuedCompletionStatus方法可以原样获取这个参数,也就是说,我们可以给特定句柄绑定一份数据,以后该句柄被GetQueuedCompletionStatus方法查询到的时候都可以获取这份数据,我看过不少代码把这个东西叫做perHandleData。
基于传递perHandleData的需求,我们可以定义一个结构。
struct perHandleData { union { SOCKET sock; HANDLE handle; }; int ty; //sock=1,handle=2 }; //开辟完成端口和线程 m_hiocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,0); perHandleData* svrhandle = m_handleset.getnew(); svrhandle->sock = m_ssock; svrhandle->ty = 1; CreateIoCompletionPort(m_ssock,m_hiocp,ULONG_PTR(svrhandle),0);
3.创建工作者线程
在异步模型中,总有线程是在等待的,完成端口模型提高效率的方式是操作系统管理等待,对于我们的程序而言只是简单的调用GetQueuedCompletionStatus方法等待,在有消息的时候操作系统会告诉我们。
for ( int i = 0;i < MAX_WORKER; ++i ) { m_hworkerset[i] = CreateThread(NULL,0,worker,(LPVOID)this,0,NULL); } DWORD WINAPI iocp::worker( LPVOID lpthis ) { iocp* pthis = static_cast<iocp*>(lpthis); BOOL bgqcp; DWORD transed; perIoData* piodata; perHandleData* phddata; while (true) { transed = 0; piodata = NULL; bgqcp = GetQueuedCompletionStatus(pthis->m_hiocp,&transed,(PULONG_PTR)&phddata,(LPOVERLAPPED*)&piodata,INFINITE); if (!bgqcp) { continue; } //your process } return 0L; }
4.GetQueuedCompletionStatus得到信息并处理
从GetQueuedCompletionStatus中,我们得到了先前交给CreateIoCompletionPort的perHandleData数据,通同时我们还可以通过out指针得到一个指向OVERLAPPED结构的指针,利用struct的内存布局,我们可以使用这个指针传递自定义信息。完成端口每次io操作完毕后我们都可以获得信息,很多代码里把这份信息称为perIoData。
struct perIoData { WSAOVERLAPPED ov; IOOperation op; LPVOID data; perIoData() :data(NULL){} ~perIoData(){if(data)delete data;} };
5.如何把perIoData交给操作系统
perIoData在完成端口模型中是传递数据的核心,mswsock天生和完成端口结合在了一起,所以在完成端口的网络编程中,传递参数的隐晦性是学习的难点。其实WSARecv、WSASend、AcceptEx都可以传递perIoData。也就是说,只要再进行简单的预处理,完成端口的网络模型就可以工作起来拉。