最近在codeproject上面闲逛,看到一篇关于他自己封装的IOCP类的讲解的文章,感觉挺通俗易懂的,特此翻译一下,但是英语是硬伤,遇到问题,希望多多包涵。多多指正。再次感谢文章的贡献者,sleepyrea,以及codeproject上面的地址附上http://www.codeproject.com/Articles/11419/IOCPNet-Ultimate-IOCP

初衷介绍:

        现在基于IOCP (Input/Output Completion Port)的文章其实已经很多了,但是那些文章都不太容易理解,主要是因为IOCP本身的一些不易理解的东西,并且没有相关的能够说明该技术和代码示例的标准文档。因此我决定做一个简单的高并发IOCP的例子(OIOCPNet),并且提供详细文档说明处理iocp的操作,以及相关的关键问题。

目标:

    主要致力于:

  1.   实现超过65000并发连接(在IP4版本下端口号最大值65535)。

  2.   能够通过网络传输超过上千字节的功能 。            

  3.  OIOCPNet类中,提供用户更便于使用的方法。           

实现目标的关键思想:

           IOCP技术

               首先第一个问题是我们为什么要使用IOCP技术呢?如果我们使用的是大家都知道的SELECT模型(用FD_SET,FD_ZERO, ...),用SELECT模型的时候我们不得不循环检测是否有socket的发送接收数据的事件发生。当我们开发一个即时通讯或者一个游戏服务器的时候,一个socket表示一个用户所有操作的一个唯一标识ID。因此,在服务器上找到用户数据,我们采用循环查找或哈希表来查找处理套接字。当服务端用户达到上万的级别的时候,在服务上采用循环是个非常严重的问题。但是,如果使用的是IOCP技术的话,我们就不需要循环。因为IOCP是通过内核检测socket事件的,并且IOCP提供将socket和(即完成端口)用户数据指针直接绑定到一起的一种机制。总而言之,IOCP技术可以避免循环,并且在服务端更快的获取到用户数据。


AcceptEx函数

         使用Accept(或WSAAccept)接受连接,当并发连接数超过大概30000(这取决于系统资源)的时候,容易出现WSAENOBUFS(10055)错误。这种错误主要是因为系统不能及时为新连接进来的客户端分配socket资源。因此我们应该找到一种的使用之前能够分配socket资源的方法。AcceptEx 就是我们寻找的答案,它的主要优势就是在使用socket资源之前就会分分配好资源,它的其他方面的特点就比较麻烦令人费解了。(参见MSDN库。)


Static memory(静态内存)

         在服务端应用程序使用静态内存(或者叫预先分配的内存)是大家普遍得而且是至关重要的做法。当我们发送或者接受数据的时候,必须使用静态内存。在我得OIOCPNet类中,我用我自己的(opreallocator)得到预分配的内。

Sliced data chunk(数据分片)

          当你调用函数(如WriteFile, WSASend orsend函数)发送大数据包(超过千字节)的时候,接收端并没有接受到发送的数据包,或者说数据包中全部的数据。如果你遇到过这种情形, 那么你可能是遇到了关于,网络硬件(路由器,集线器,等等)和缓冲---MTU(最大传输单元)的问题。网络硬件的最小MTU是576字节,所以最好是大数据包切成许多小于最小MTU的小数据包。在oiocpnet,我所定义的单元的数据块大小为buffer_unit_size(512字节)。如果你需要更大的一个,你可以改变它。

Don't spawn many threads(不要创建过多线程)

         如果你的服务器逻辑需要很多的IO操作,创建多个线程是个不错的选择。因为,当环境中需要多个IO操作的时候,线程是有意义重大的。但是不要忘记,越多的线程,CPU需要做的线程之间的调度就会越多。如果有超过1000个线程,并且他们都在运行,操作系统和进程不能保持他们正常的运行状态,因为CPU所有的精力都花费在寻找下一个运行的线程,以及线程的调度或者说上下文切换。作为参考,OIOCPNet每个CPU都会创建两个线程,不在创建其他的线程。

OIOCPNet - 关键所在

      OIOCPNet就是应用了上述思想的一个类。OIOCPNet的操作步骤如下:

  •  准备所需要使用的资源,如预先分配的内存区域,完成端口,其他处理等
  • 设定监听Socket
  • 预先生成Socket(65000个,但是我在IOCPNet.h中把值定义为3000,如果不是WIN 2003 操作系统,可以把MAX_ACCEPTABLE_SOCKET_NUM修改为你自己所需要的值。)以及Socket所需的内存,然后通过使用 AcceptEx函数,使用它们。
  • 当有连接试图连接服务的时候,使用OIOCPNet接受这个请求。
  • 当有一个Socket读取一个数据包的时候,OIOCPNet将数据包放入预分配好的读取槽中,并将一个对应的事件放入逻辑服务中处理。
  • 当逻辑服务需要写入数据包的时候,OIOCPNet将数据放入预分配好的内存块,然后调用PostQueuedCompletionStatus,这样工作线程就能发送这个数据包。
  • 当客户端关闭这个连接,OIOCPNet关闭这个套接字,但是不释放这个套接字相关的内存,只是重置它。

下面这个图片将显示OIOCPNet的整个机制:

                                                

写程序时需要注意的几个关键点:

       LPOVERLAPPED:

GetQueuedCompletionStatus和postqueuedcompletionstatus缺少参展示的IO操作的结果的参数。此外,除了GetQueuedCompletionStatus(或PostQueuedCompletionStatus)的默认参数,OIOCPNet需要更多的参数分类IO操作的类型和一些额外的信息。所以我使用GetQueuedCompletionStatus和PostQueuedCompletionStatus的LPOVERLAPPED参数作为我的自定义参数就像CreateThread的线程参数(LPVOID lpParameter,第四个参数)。OVERLAPPEDExt是OVERLAPPED结构的扩展类型结构,它里面包含更多的信息。         

struct OVERLAPPEDExt
{
  OVERLAPPED OL;
  int IOType;
  OBufferedSocket *pBuffSock;
  OTemporaryWriteData *pTempWriteData;
}; // OVERLAPPEDExt</span>
        异步函数使用的变量的生命周期:
      在OIOCPNet, WSASendWSARecv 中使用了异步操作的方式,因此注意参数传递到异步函数时候的生命周期的使用。

// pTempWriteData will be freed when send IO ends.
pTempWriteData = (OTemporaryWriteData *)
m_SMMTempWriteData.Allocate(sizeof (OTemporaryWriteData));

...

// the size of pData 
// (the second parameter of GetBlockNeedsExternalLock)
// does not be over BUFFER_UNIT_SIZE.
m_pWriteBlock->GetBlockNeedsExternalLock
  (&pBuffSockToWrite, pTempWriteData->Data, 
  &ReadSizeToWrite, &DoesItHaveMoreSequence);

...

try
{
  ResSend = WSASend(pTempWriteData->Socket, 
    &pTempWriteData->DataBuf, 1, 
    &WrittenSizeUseless, Flag, 
    (LPOVERLAPPED)&pTempWriteData->OLExt, 0);
}


在上面的代码片段中,pTempWriteData分配给WSASend来调用发送的WSASend发送之后立马返回,但是
pTempWriteData必须是存在的,直到系统内核调用WSASend发送完成。当发送完成之后,释放pTempWriteData
中的数据,释放方式如下:

if (0 != pOVL)
{ 
 if ((IO_TYPE_WRITE_LAST ==     ((OVERLAPPEDExt *)pOVL)->IOType     || IO_TYPE_WRITE ==     ((OVERLAPPEDExt *)pOVL)->IOType))
 {    if (0 != ((OVERLAPPEDExt *)pOVL)->pTempWriteData)    
{      m_SMMTempWriteData.Free(        ((OVERLAPPEDExt *)pOVL)->pTempWriteData);   
 }        continue; 
 }
}


套接字的唯一性:
  一个正常的Socket值是唯一的。但是,操作系统分配的Socket值是随机分配的,
刚关闭的Socket可以重新分配给同时新连接进来的套接字。流程如下:
 
 
  1. 为新连接分配一个值为3947的Socket(作为一个实例)
  2. 逻辑服务器使用套Socket读取数据包。
  3. 在逻辑服务器不知情的情况下,客户端套接字突然关闭。
  4. 一个不同的Socket被分配相同的Socket值,即该Socket值的复用。
  5. 服务器逻辑将数据包写入Socket对应数据,服务器并没有什么问题,但数据包可能会发送到不同的用户。

为了防止这种棘手的情况,OIOCPNet 管理自己的套接字SocketUnique,OBufferedSocket的一个成员。

如何使用OIOCPNet

         用法:OIOCPNet的简单实例,如下面的代码片段:
int _tmain(int argc, _TCHAR* argv[])
{
  ...

  WSAStartup(MAKEWORD(2,2), &WSAData);

  pIOCPNet = new OIOCPNet(&EL);
  pIOCPNet->Start(TEST_IP, TEST_PORT);
    
  hThread = CreateThread(0, 0, LogicThread, 
    pIOCPNet, 0, 0);

  ...
  
  InterlockedExchange((long *)&g_dRunning, 0);
  WaitForSingleObject(hThread, INFINITE);

  ...

  pIOCPNet->Stop();
  delete pIOCPNet;

  WSACleanup();

  return 0;
} // _tmain()

DWORD WINAPI LogicThread(void *pParam)
{
  ...
  
  while (1 == InterlockedExchange((long *)&g_dRunning, 
    g_dRunning))
  {
    iRes = pIOCPNet->GetSocketEventData(WAIT_TIMEOUT_TEST,
      &EventType, &SocketUnique, &pReadData, 
      &ReadSize, &pBuffSock, &pSlot, &pCustData);
    if ...
    else if (RET_SOCKET_CLOSED == iRes)
    {
      // release pCustData.
      continue;
    }

    // Process main logic.
    MainLogic(pIOCPNet, SocketUnique, pBuffSock, 
      pReadData, ReadSize);
        
    pIOCPNet->ReleaseSocketEvent(pSlot);
  }

  return 0;
} // LogicThread()

void MainLogic(OIOCPNet *pIOCPNet, DWORD SocketUnique,
  OBufferedSocket *pBuffSock, BYTE *pReadData, DWORD ReadSize)
{
  pIOCPNet->WriteData(SocketUnique, pBuffSock, 
    pReadData, ReadSize); // echo.
} // MainLogic()

我们在开始的时候设置IP地址和端口号,准备必要的资源。在逻辑线程中我们可以使用
GetSocketEventData获取的数据包和WriteData发送数据包。在数据使用之后,使用
ReleaseSocketEvent通过pSlot指针释放(pReadData)的数据包。最后,当主线程结束后,
停止调用,OIOCPNet释放资源。

在客户端注意数据的读写:

OIOCPNet将大数据报分片成小数据包,在原来的数据的基础上增加了4个字节数据包长度信息。

分片和组装操作由OIOCPNet 的GetSocketEventData和WriteData 两个函数完成。因此,我们不需要关心这些。

但是,你需要使用TCPWriteTCPRead两个函数。(在NetTestClient项目中的TCPFunc.hTCPFunc.cpp

和OIOCPNet进行交互,当你的客户端需要链接服务端的时候。


其他提示


         当一个客户端不能产生超过5000(2000年~)连接到服务器,检查注册表。 检查步骤包括:
  1. 运行注册表编辑器
  2. 打开HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
  3. 添加“MaxUserPort“类型为DWORD和设置值(最大值是65534十进制数)。
如果你需要增加线程数量的测试客户端超过2 0 xx,修改客户端应用程序的函数堆栈大小使用编译选项/栈:BYTE或CreateThread的参数。 在您运行测试服务器和测试客户端之前,设置TEST_IP和TEST_SERVER_IP与您的服务器的IP地址。 看到连接数量,使用性能监视器或“netstat - s”在命令提示符。

 
 





Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐