关于完成端口网上有很多文章,不过我个人觉得大多都讲得不够清楚。给的例子要不就是给一个复杂的封装,要不就是给一个简单的收发数据。注意,完成端口不仅仅用于网络数据的收发,它可以用于windows 平台的各种IO操作。不过我这里只关注在winsock编程中的应用。
要写出一篇真的让人能够明白的文章,不那么容易。这里我只暂时贴些我的理解。迟些时候如果有空的话,我倒有兴趣写个详细的入门文章。
1.26.2008
Kevin Lynx
理解完成端口:
就目前所了解的信息来说,完成端口通常都会与重叠IO有关联。完成端口可被看作是一个队列。各种操作都会被放到该队列里,程序在迟些时候查询此队列获取之前提交的IO操作结果。
注意,无论是重叠IO还是完成端口,都不仅仅用于socket的操作,他们是用于各种IO的操作。
IOCP不是为每一个客户端连接建立一个线程。
要区分IOCP和事件通知模型的区别,事件通知模型是先得到事件,然后根据事件类型去获取或者发送数据;而IOCP则是先提交动作(发送或接收),后得到通知,当得到通知时,通常就意味着之前提交的动作已经完成了。
When you use IOCP, you spawn a pool of threads once - and they are used to handle the network I/O in your application. Technically, in Windows 2000, you don't even have to spawn the pool yourself - you can let Windows take care of the spawning and management of the threads in the pool。
IOCP其实也属于一种同步对象,就像Windows里的Event对象一样。IOCP用于同步IO操作。在异步IO操作中,提交了一个异步操作后,某些时候就需要得知操作的结果,也就是同步一下。
http://www.codeproject.com/KB/IP/iocp_server_client.aspx
A server application is fairly meaningless if it cannot service multiple clients at the same time, usually asynchronous I/O calls and multithreading is used for this purpose. By definition, an asynchronous I/O call returns immediately, leaving the I/O call pending. At some point of time, the result of the I/O asynchronous call must be synchronized with the main thread. This can be done in different ways. The synchronization can be performed by:
这里我要特别强调一下异步IO和非阻塞IO的区别,异步IO就是把IO提交给系统,让系统替你做,做完了再用某种方式通知你;非阻塞IO就是你要通过某种方式不定时地向系统询问你是否可以开始做某个IO,当可以开始后,还是要自己来完成IO。
不可以通过接受数据是否为0来判断客户端是否断开,只有当调用closesocket时才可以通过这个方法判断,如果是意外退出(断电,网络故障),则判断不出。这个时候可以采用定时发送数据(心跳信息)来确认。
创建IOCP程序,一般的步骤:
1. 创建一个单句柄数据结构体,该结构体里一般都包含一个套接字数据。因为IOCP实际上会有固定的几个线程(工作线程),这些线程在IOCP结果队列里查询IO操作结果。这些结果不止是在一个套接字上进行的操作(读或写),而是包括了所有与该IOCP对象关联起来的套接字上的操作结果。因此,为了区分某次操作结果属于哪个套接字,就需要这个单句柄数据结构里包含这个套接字句柄。
2. 创建一个以OVERLAPPED为首个元素的数据结构体。该结构体实际上对应着一个IO操作(例如WSASend)。对于 WSASend, WSARecv以及查询操作结果函数都需要一个OVERLAPPED参数(一般是指针),通常情况下我们需要更多的数据,因此定义的这个结构体里通常包含了更多的数据(例如WSABUF,它可以用来容纳WSARecv接受到的数据)。之所以要把OVERLAPPED 作为这个结构体的第一个元素,是为了在使用查询函数GetQueuedCompletionStatus后,可以通过该函数返回的OVERLAPPED类型的指针得到我们这里定义的结构体对象地址,从而获取更多的数据。
3. 每一次接受到新的连接时(accept),都将这个新的套接字与完成端口相关联。并且创建一个单句柄对象(也就是完成键)。每一个套接字都有一个关联的单句柄对象。而每一个IO操作都有一个关联的OVERLAPPED相关的数据结构(上一步定义的结构体)。
4. 可以在任何时候提交异步IO请求,例如WSASend, WSARecv。这里需要为OVERLAPPED相关的结构体指定操作类型。一个典型的结构体为(即第二步定义的结构体):
struct IOContext
{
/// 很多函数需要此参数
OVERLAPPED ol;
/// 存放接受数据
char buf[MAX_BUF];
///
WSABUF wsabuf;
/// 操作类型,提交IO操作时指定该值,在查询操作结果时,可以重新获取到该值
int op_type;
};
在创建该结构体的变量时,为op_type指定一个值。然后将此结构体的地址给WSASend之类的函数。在工作者线程中执行查询时,实际上得到了该结构体的地址(结构体变量),那么,就可以获取op_type的值。
注意:查询结构只能获取IO操作的字节数(以及IO操作结果数据),不能知道IO操作的类型。所以IO操作的类型实际上是在这里用户自己指定的。
当执行WSASend时,设置op_type为SEND(自己定义的常量),执行WSARecv时,指定READ。然后在查询结果时,可以根据op_type知道这个操作结果是什么类型。如果是SEND,那么就表示之前提交的WSASend操作。
IOCP是一个异步操作机制,之所以是异步,就是因为可以随时提交IO操作。提交之后具体的操作由系统为你完成。完成后就需要某种机制来得知操作结果。IOCP设置的这个结果队列就是一种机制。
5. 可以通过PostQueuedCompletionStatus手动地往结果队列里放置一个操作结果。通常这个函数都用于让工作者线程退出。例如:
PostQueuedCompletionStatus( cp_handle, 0, NULL, NULL );
然后在工作者线程里:
ret = GetQueuedCompletionStatus( cp_handle, &transfer_bytes, (PULONG_PTR) &hc,
(LPOVERLAPPED*)&ic, INFINITE );
if( ic == NULL )
{
printf( "ic == NULL/n" );
/*
使用PostQueuedCompletionStatus传递过来的数据,
这里约定ic==NULL时退出
*/
break;
}
6. 如果不提交任何IO操作,那么结果队列里很有可能一直都是空的。那么GetQueued这个查询函数就会一直得不到数据。
7. 纵观IOCP程序,一个比较复杂的地方在于资源的释放。在接收到一个新的连接时,会为这个连接创建单句柄数据,执行IO操作的话,还要创建OVERLAPPED相关结构体变量。这些变量的地址都会在工作者线程中通过GetQueued..函数获取,并在工作者线程中使用。一个比较直接的做法是在工作者线程中释放这些资源。
推荐些文章:
几种socket模型的代码: http://blog.csdn.net/mlite/archive/2006/04/30/699340.aspx
又一个简单的IOCP代码:http://www.go321.cn/html/app/cpp/20070526/30207.html
另一个例子,那幅图有点意义:http://www.3800hk.com/Article/cxsj/vc/wllbcvc/2005-08-25/Article_54111.html
codeproject上的文章:http://www.codeproject.com/KB/IP/iocp_server_client.aspx
codeproject上的另一篇:http://www.codeproject.com/KB/IP/jbsocketserver1.aspx
MSDN上的,前部分由点意义:http://msdn.microsoft.com/msdnmag/issues/1000/winsock/
其实完成端口的例子在细节上有很多方法,例如accept, AcceptEx之类,对于accept的处理尤其多。这些无疑又给初学者带来了迷惑。我觉得只要把握住几个要点就行了:异步操作,结果队列,数据的传送(提交操作时传进去,查询时取出来),工作者线程。