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
- );
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;
- }
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);
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);
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;
- }
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;}
- };
struct perIoData
{
WSAOVERLAPPED ov;
IOOperation op;
LPVOID data;
perIoData() :data(NULL){}
~perIoData(){if(data)delete data;}
};
5.如何把perIoData交给操作系统
perIoData在完成端口模型中是传递数据的核心,mswsock天生和完成端口结合在了一起,所以在完成端口的网络编程中,传递参数的隐晦性是学习的难点。其实WSARecv、WSASend、AcceptEx都可以传递perIoData。也就是说,只要再进行简单的预处理,完成端口的网络模型就可以工作起来拉。