理解完成端口(IO completion port)

 

    关于完成端口网上有很多文章,不过我个人觉得大多都讲得不够清楚。给的例子要不就是给一个复杂的封装,要不就是给一个简单的收发数据。注意,完成端口不仅仅用于网络数据的收发,它可以用于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:

  • Using events - A signal is set as soon as the asynchronous call is finished. The disadvantage of this approach is that the thread has to check or wait for the event to be set.
  • Using the GetOverlappedResult function - This approach has the same disadvantage as the approach above.
  • Using Asynchronous Procedure Calls (or APC) - There are several disadvantages associated with this approach. First, the APC is always called in the context of the calling thread, and second, in order to be able to execute the APCs, the calling thread has to be suspended in the so called alterable wait state.
  • Using IOCP - The disadvantage of this approach is that there are many practical thorny programming problems that must be solved. Coding IOCP can be a bit of a hassle.

 

这里我要特别强调一下异步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_typeSEND(自己定义的常量),执行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的处理尤其多。这些无疑又给初学者带来了迷惑。我觉得只要把握住几个要点就行了:异步操作,结果队列,数据的传送(提交操作时传进去,查询时取出来),工作者线程。

                        

 

你可能感兴趣的:(数据结构,工作,windows,IO,asynchronous,multithreading)