假设现在无数客户端连接上来下载一大文件,那么读此文件显然也必须异步进行,想采用完成端口模式,读完一块发送一块,有没有代码示例?

解决方案 »

  1.   

    非要用完成端口吗?
    Select+异步不行吗?
    完成端口有一定的难度,为什么不先看看书?
      

  2.   

    我想用TransmitFile会很快的,它无须再用户模式和内核模式之间进行切换!
    BOOL TransmitFile(
      SOCKET hSocket,                             
      HANDLE hFile,                               
      DWORD nNumberOfBytesToWrite,                
      DWORD nNumberOfBytesPerSend,                
      LPOVERLAPPED lpOverlapped,                  
      LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,  
      DWORD dwFlags                               
    );
      

  3.   

    用完成端口开发大响应规模的Winsock应用程序通常要开发网络应用程序并不是一件轻松的事情,不过,实际上只要掌握几个关键的原则也就可以了——创建和连接一个套接字,尝试进行连接,然后收发数据。真正难的是要写出一个可以接纳少则一个,多则数千个连接的网络应用程序。本文将讨论如何通过Winsock2在Windows NT? 和 Windows 2000上开发高扩展能力的Winsock应用程序。文章主要的焦点在客户机/服务器模型的服务器这一方,当然,其中的许多要点对模型的双方都适用。API与响应规模通过Win32的重叠I/O机制,应用程序可以提请一项I/O操作,重叠的操作请求在后台完成,而同一时间提请操作的线程去做其他的事情。等重叠操作完成后线程收到有关的通知。这种机制对那些耗时的操作而言特别有用。不过,像Windows 3.1上的WSAAsyncSelect()及Unix下的select()那样的函数虽然易于使用,但是它们不能满足响应规模的需要。而完成端口机制是针对操作系统内部进行了优化,在Windows NT 和 Windows 2000上,使用了完成端口的重叠I/O机制才能够真正扩大系统的响应规模。完成端口一个完成端口其实就是一个通知队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O操作一旦完成,某个可以对该操作结果进行处理的工作者线程就会收到一则通知。而套接字在被创建后,可以在任何时候与某个完成端口进行关联。通常情况下,我们会在应用程序中创建一定数量的工作者线程来处理这些通知。线程数量取决于应用程序的特定需要。理想的情况是,线程数量等于处理器的数量,不过这也要求任何线程都不应该执行诸如同步读写、等待事件通知等阻塞型的操作,以免线程阻塞。每个线程都将分到一定的CPU时间,在此期间该线程可以运行,然后另一个线程将分到一个时间片并开始执行。如果某个线程执行了阻塞型的操作,操作系统将剥夺其未使用的剩余时间片并让其它线程开始执行。也就是说,前一个线程没有充分使用其时间片,当发生这样的情况时,应用程序应该准备其它线程来充分利用这些时间片。完成端口的使用分为两步。首先创建完成端口,如以下代码所示:HANDLE  hIocp;hIocp = CreateIoCompletionPort(
      INVALID_HANDLE_VALUE,
      NULL,
      (ULONG_PTR)0,
      0);
    if (hIocp == NULL) {
      // Error
    }
    完成端口创建后,要把将使用该完成端口的套接字与之关联起来。方法是再次调用CreateIoCompletionPort ()函数,第一个参数FileHandle设为套接字的句柄,第二个参数ExistingCompletionPort 设为刚刚创建的那个完成端口的句柄。
    以下代码创建了一个套接字,并把它和前面创建的完成端口关联起来:SOCKET  s;s = socket(AF_INET, SOCK_STREAM, 0);
    if (s == INVALID_SOCKET) {
      // Error
    if (CreateIoCompletionPort((HANDLE)s,
                  hIocp,
                  (ULONG_PTR)0,
                  0) == NULL)
    {
    // Error
    }
    ???
    }
      

  4.   

    这时就完成了套接字与完成端口的关联操作。在这个套接字上进行的任何重叠操作都将通过完成端口发出完成通知。注意,CreateIoCompletionPort()函数中的第三个参数用来设置一个与该套接字相关的“完成键(completion key)”(译者注:完成键可以是任何数据类型)。每当完成通知到来时,应用程序可以读取相应的完成键,因此,完成键可用来给套接字传递一些背景信息。在创建了完成端口、将一个或多个套接字与之相关联之后,我们就要创建若干个线程来处理完成通知。这些线程不断循环调用GetQueuedCompletionStatus ()函数并返回完成通知。下面,我们先来看看应用程序如何跟踪这些重叠操作。当应用程序调用一个重叠操作函数时,要把指向一个overlapped结构的指针包括在其参数中。当操作完成后,我们可以通过GetQueuedCompletionStatus()函数中拿回这个指针。不过,单是根据这个指针所指向的overlapped结构,应用程序并不能分辨究竟完成的是哪个操作。要实现对操作的跟踪,你可以自己定义一个OVERLAPPED结构,在其中加入所需的跟踪信息。无论何时调用重叠操作函数时,总是会通过其lpOverlapped参数传递一个OVERLAPPEDPLUS结构(例如WSASend、 WSARecv等函数)。这就允许你为每一个重叠调用操作设置某些操作状态信息,当操作结束后,你可以通过GetQueuedCompletionStatus()函数获得你自定义结构的指针。注意OVERLAPPED字段不要求一定是这个扩展后的结构的第一个字段。当得到了指向OVERLAPPED结构的指针以后,可以用CONTAINING_RECORD宏取出其中指向扩展结构的指针(译者注:以上两小段一会是OVERLAPPEDPLUS结构,一会是OVERLAPPED结构,本人也看不太懂,请高手赐教)。OVERLAPPED 结构的定义如下:typedef struct _OVERLAPPEDPLUS {
      OVERLAPPED    ol;
      SOCKET      s, sclient;
      int        OpCode;
      WSABUF      wbuf;
      DWORD       dwBytes, dwFlags;
      // other useful information
    } OVERLAPPEDPLUS;#define OP_READ   0
    #define OP_WRITE  1
    #define OP_ACCEPT  2
    下面让我们来看看Figure2里工作者线程的情况。
    Figure 2 Worker Thread DWORD WINAPI WorkerThread(LPVOID lpParam)
    {  
      ULONG_PTR    *PerHandleKey;
      OVERLAPPED   *Overlap;
      OVERLAPPEDPLUS *OverlapPlus,
              *newolp;
      DWORD      dwBytesXfered;  while (1)
      {
        ret = GetQueuedCompletionStatus(
          hIocp,
          &dwBytesXfered,
          (PULONG_PTR)&PerHandleKey,
          &Overlap,
          INFINITE);
        if (ret == 0)
        {
          // Operation failed
          continue;
        }
        OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
      
      switch (OverlapPlus->OpCode)
      {
      case OP_ACCEPT:
        // Client socket is contained in OverlapPlus.sclient
        // Add client to completion port
          CreateIoCompletionPort(
            (HANDLE)OverlapPlus->sclient,
            hIocp,
            (ULONG_PTR)0,
            0);    // Need a new OVERLAPPEDPLUS structure
        // for the newly accepted socket. Perhaps
        // keep a look aside list of free structures.
        newolp = AllocateOverlappedPlus();
        if (!newolp)
        {
          // Error
        }
        newolp->s = OverlapPlus->sclient;
        newolp->OpCode = OP_READ;    // This function prepares the data to be sent
        PrepareSendBuffer(&newolp->wbuf);
     
        ret = WSASend(
            newolp->s,
            &newolp->wbuf,
            1,
            &newolp->dwBytes,
            0,
            &newolp.ol,
            NULL);
        
        if (ret == SOCKET_ERROR)
        {
          if (WSAGetLastError() != WSA_IO_PENDING)
          {
          // Error
          }
        }    // Put structure in look aside list for later use
        FreeOverlappedPlus(OverlapPlus);    // Signal accept thread to issue another AcceptEx
        SetEvent(hAcceptThread);
        break;  case OP_READ:
        // Process the data read  
        // •••    // Repost the read if necessary, reusing the same
        // receive buffer as before
        memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
        ret = WSARecv(
           OverlapPlus->s,
           &OverlapPlus->wbuf,
           1,
           &OverlapPlus->dwBytes,
           &OverlapPlus->dwFlags,
           &OverlapPlus->ol,
           NULL);    if (ret == SOCKET_ERROR)
        {
          if (WSAGetLastError() != WSA_IO_PENDING)
          {
            // Error
          }
        }
        break;  case OP_WRITE:
        // Process the data sent, etc.
        break;
      } // switch
      } // while
    } // WorkerThread
      

  5.   

    其中每句柄键(PerHandleKey)变量的内容,是在把完成端口与套接字进行关联时所设置的完成键参数;Overlap参数返回的是一个指向发出重叠操作时所使用的那个OVERLAPPEDPLUS结构的指针。要记住,如果重叠操作调用失败时(也就是说,返回值是SOCKET_ERROR,并且错误原因不是WSA_IO_PENDING),那么完成端口将不会收到任何完成通知。如果重叠操作调用成功,或者发生原因是WSA_IO_PENDING的错误时,完成端口将总是能够收到完成通知。Windows NT和Windows 2000的套接字架构对于开发大响应规模的Winsock应用程序而言,对Windows NT和Windows 2000的套接字架构有基本的了解是很有帮助的。与其它类型操作系统不同,Windows NT和Windows 2000的传输协议没有一种风格像套接字那样的、可以和应用程序直接交谈的界面,而是采用了一种更为底层的API,叫做传输驱动程序界面(Transport Driver Interface,TDI)。Winsock的核心模式驱动程序负责连接和缓冲区管理,以便向应用程序提供套接字仿真(在AFD.SYS文件中实现),同时负责与底层传输驱动程序对话。谁来负责管理缓冲区?正如上面所说的,应用程序通过Winsock来和传输协议驱动程序交谈,而AFD.SYS负责为应用程序进行缓冲区管理。也就是说,当应用程序调用send()或WSASend()函数来发送数据时,AFD.SYS将把数据拷贝进它自己的内部缓冲区(取决于SO_SNDBUF设定值),然后send()或WSASend()函数立即返回。也可以这么说,AFD.SYS在后台负责把数据发送出去。不过,如果应用程序要求发出的数据超过了SO_SNDBUF设定的缓冲区大小,那么WSASend()函数会阻塞,直至所有数据发送完毕。从远程客户端接收数据的情况也类似。只要不用从应用程序那里接收大量的数据,而且没有超出SO_RCVBUF设定的值,AFD.SYS将把数据先拷贝到其内部缓冲区中。当应用程序调用recv()或WSARecv()函数时,数据将从内部缓冲拷贝到应用程序提供的缓冲区。多数情况下,这样的架构运行良好,特别在是应用程序采用传统的套接字下非重叠的send()和receive()模式编写的时候。不过程序员要小心的是,尽管可以通过setsockopt()这个API来把SO_SNDBUF和SO_RCVBUF选项值设成0(关闭内部缓冲区),但是程序员必须十分清楚把AFD.SYS的内部缓冲区关掉会造成什么后果,避免收发数据时有关的缓冲区拷贝可能引起的系统崩溃。举例来说,一个应用程序通过设定SO_SNDBUF为0把缓冲区关闭,然后发出一个阻塞send()调用。在这样的情况下,系统内核会把应用程序的缓冲区锁定,直到接收方确认收到了整个缓冲区后send()调用才返回。似乎这是一种判定你的数据是否已经为对方全部收到的简洁的方法,实际上却并非如此。想想看,即使远端TCP通知数据已经收到,其实也根本不代表数据已经成功送给客户端应用程序,比如对方可能发生资源不足的情况,导致AFD.SYS不能把数据拷贝给应用程序。另一个更要紧的问题是,在每个线程中每次只能进行一次发送调用,效率极其低下。把SO_RCVBUF设为0,关闭AFD.SYS的接收缓冲区也不能让性能得到提升,这只会迫使接收到的数据在比Winsock更低的层次进行缓冲,当你发出receive调用时,同样要进行缓冲区拷贝,因此你本来想避免缓冲区拷贝的阴谋不会得逞。现在我们应该清楚了,关闭缓冲区对于多数应用程序而言并不是什么好主意。只要要应用程序注意随时在某个连接上保持几个WSARecvs重叠调用,那么通常没有必要关闭接收缓冲区。如果AFD.SYS总是有由应用程序提供的缓冲区可用,那么它将没有必要使用内部缓冲区。高性能的服务器应用程序可以关闭发送缓冲区,同时不会损失性能。不过,这样的应用程序必须十分小心,保证它总是发出多个重叠发送调用,而不是等待某个重叠发送结束了才发出下一个。如果应用程序是按一个发完再发下一个的顺序来操作,那浪费掉两次发送中间的空档时间,总之是要保证传输驱动程序在发送完一个缓冲区后,立刻可以转向另一个缓冲区。
    资源的限制条件 
    在设计任何服务器应用程序时,其强健性是主要的目标。也就是说,你的应用程序要能够应对任何突发的问题,例如并发客户请求数达到峰值、可用内存临时出现不足、以及其它短时间的现象。这就要求程序的设计者注意Windows NT和2000系统下的资源限制条件的问题,从容地处理突发性事件。你可以直接控制的、最基本的资源就是网络带宽。通常,使用用户数据报协议(UDP)的应用程序都可能会比较注意带宽方面的限制,以最大限度地减少包的丢失。然而,在使用TCP连接时,服务器必须十分小心地控制好,防止网络带宽过载超过一定的时间,否则将需要重发大量的包或造成大量连接中断。关于带宽管理的方法应根据不同的应用程序而定,这超出了本文讨论的范围。虚拟内存的使用也必须很小心地管理。通过谨慎地申请和释放内存,或者应用lookaside lists(一种高速缓存)技术来重新使用已分配的内存,将有助于控制服务器应用程序的内存开销(原文为“让服务器应用程序留下的脚印小一点”),避免操作系统频繁地将应用程序申请的物理内存交换到虚拟内存中(原文为“让操作系统能够总是把更多的应用程序地址空间更多地保留在内存中”)。你也可以通过SetWorkingSetSize()这个Win32 API让操作系统分配给你的应用程序更多的物理内存。在使用Winsock时还可能碰到另外两个非直接的资源不足情况。一个是被锁定的内存页面的极限。如果你把AFD.SYS的缓冲关闭,当应用程序收发数据时,应用程序缓冲区的所有页面将被锁定到物理内存中。这是因为内核驱动程序需要访问这些内存,在此期间这些页面不能交换出去。如果操作系统需要给其它应用程序分配一些可分页的物理内存,而又没有足够的内存时就会发生问题。我们的目标是要防止写出一个病态的、锁定所有物理内存、让系统崩溃的程序。也就是说,你的程序锁定内存时,不要超出系统规定的内存分页极限。在Windows NT和2000系统上,所有应用程序总共可以锁定的内存大约是物理内存的1/8(不过这只是一个大概的估计,不是你计算内存的依据)。如果你的应用程序不注意这一点,当你的发出太多的重叠收发调用,而且I/O没来得及完成时,就可能偶尔发生ERROR_INSUFFICIENT_RESOURCES的错误。在这种情况下你要避免过度锁定内存。同时要注意,系统会锁定包含你的缓冲区所在的整个内存页面,因此缓冲区靠近页边界时是有代价的(译者理解,缓冲区如果正好超过页面边界,那怕是1个字节,超出的这个字节所在的页面也会被锁定)。另外一个限制是你的程序可能会遇到系统未分页池资源不足的情况。所谓未分页池是一块永远不被交换出去的内存区域,这块内存用来存储一些供各种内核组件访问的数据,其中有的内核组件是不能访问那些被交换出去的页面空间的。Windows NT和2000的驱动程序能够从这个特定的未分页池分配内存。<<回复3次,可还没结束。谁来顶一下,我好继续贴>>
      

  6.   

    啊塘:
    其实你这样考虑应该没问题了(相对于优化条件来说)。
    但我不知道你的东西能不能优化的余地。如果是不无数个客户端读不同的或同一文件不同的区域时,必然要为它们开线程,主要原因有二:1硬盘速度相对来说比较慢,在某程度上会阻塞完成线程的。2:单线程统一对所有磁盘的操作估计很复杂,一般不值得你这样做。如果你承认上边的方案的话,那继续看下去。你要知道线程数,每个线程对磁盘的操作,网络带宽,这三个主要问题必然是影响你服务端的问题。那么就要监视开对“磁盘操作的线程”了。这样一来,就不能实现你的对无数客户端进行磁盘的操作了,其实这样做也没错,因为“无数磁盘的操作”必然使你的服务器down掉的。更不用谈这“无数”在执行中的线程了,cpu负荷得起?
      

  7.   

    to lostgdi731(O_O):
       如果不采取完成端口的模式,势必要为每个读写文件开一个线程,这样就不能为大批量客户端服务,线程数100以上估计CPU切换比较愚钝,更多就更不要说了。
       其实我认为,readfile和wsarecv对于完成端口来说没有什么区别,反正都是等待完成,微软在后面去玩什么鬼咱不管,我只需要知道什么时候完成了,然后操作的内容在哪里就行了
       微软真他妈的弱,GetQueuedCompletionStatus函数第4个参数LPOVERLAPPED是一个指向 Overlapped结构的指针,然后它说,可以把这个结构改一下(后面加上内容),让它变成一个伪Overlapped结构, 调用这个函数时把它的指针传进去,然后完成的内容就会在这个结构体里搞出来,的确没有错,是可以出来,但是msdn上对如何定义这个结构体也没有统一的说法,大家随便定义,总之只要保证第一项是那个原来的结构体就行。让我不解的是,我定义的伪结构里有WSABUF结构,这个缓冲区里放完成的内容,我的位置不固定,微软是如何知道这个的,假如我不定义这个,它又把完成内容放到哪里去,怪。