网狐棋牌(五) TCPSocketEnging分析

相关UML:
网络引擎整体结构:


SocketItem细节:



先来看几个底层结构:
// 重叠结构类
class COverLapped
{
     // 变量定义
public:
    WSABUF                            m_WSABuffer;                         // 数据指针
    OVERLAPPED                        m_OverLapped;                         // 重叠结构
     const enOperationType            m_OperationType;                     // 操作类型

    
// 函数定义
public:
     // 构造函数
    COverLapped(enOperationType OperationType);
     // 析构函数
     virtual ~COverLapped();

     // 信息函数
public:
     // 获取类型
    enOperationType GetOperationType() {  return m_OperationType; }
};

// 接收重叠结构
class COverLappedSend :  public COverLapped
{
     // 数据变量
public:
    BYTE                            m_cbBuffer[SOCKET_BUFFER];             // 数据缓冲

    
// 函数定义
public:
     // 构造函数
    COverLappedSend();
     // 析构函数
     virtual ~COverLappedSend();
};

// 重叠结构模板
template <enOperationType OperationType>  class CATLOverLapped :  public COverLapped
{
     // 函数定义
public:
     // 构造函数
    CATLOverLapped() : COverLapped(OperationType) {}
     // 析构函数
     virtual ~CATLOverLapped() {}
};


先复习下基础,Windows下的网络模型有很多种,这里只拿出三种来说:
EventSelect:基于信号机制,以socket为单位绑定信号量,当socket上有指定的事件发生时激发信号,然后查询事件处理事件重设事件,继续在信号量上等待。其实也是在伯克利select模型上的换不换药的加强。
OverLapped:分两种工作模式完成回调,和完成事件。重叠IO监视每次操作,每次IO都绑定一个重叠对象,当操作完成以后激发信号或者调用回调。
IOCP:和overlapped类似,不过结果经过了Windows的预处理以队列的形式挂在完成端口上

根据上面的复习,可以得出一个结论,IOCP环境中每一次IO操作都需要一个重叠结构,那么一个CServerSocketItem至少需要如些这些东东:
他要接受数据,所以必须有一个接受数据的 OverLapped
它要发送数据,说以必须有一个发送数据的 OverLapped
netFox对OverLapped做了使用了类似池的的管理手段,他的Send都是不等待上一次完成就直接投递下一个请求了,,,这是很操蛋的做法,,,

然后继续复习下基础:
在EventSelect模型中获处理件类型流程是这样:
event受信,使用::WSAEnumNetworkEvents查询和这个event关联的socket发生的事件,根据查询到的事件类型去处理事件
在以每一次IO为查询对象重叠IO、IOCP模型中是这样:
使用GetOverlappedResult 或者 GetQueuedCompletionStatus然后根据重叠结构去查询投递的是什么类型的操作,然后找到关联的socket去操作,,,

这样必然要给OverLapped做个扩展,提供一种通过OverLapped查询操作类型和socket的能力。
通过分析代码,netFox关联socket是通过在创建完成端口的时候绑定SocketItem对象指针完成的,操作类型是通过对OverLapped结构加强完成的。
通过GetQueuedCompletionStatus获取到完成OverLapped以后使用一个宏:
(这是COverLapped类型)  pSocketLapped=CONTAINING_RECORD(pOverLapped,COverLapped,m_OverLapped);
来获取包装后的OverLapped,然后获取操作类型,然后执行具体操作。
其实宏的展开如下:
(COverLapped*)((BYTE*)pOverLapped - (COverLapped*)(0)->m_OverLapped);
pOverLapped是获取到的某个COverLapped中的成员变量,(COverLapped*)(0)->m_OverLapped是到在COverLapped中的偏移,((BYTE*)pOverLapped - (COverLapped*)(0)->m_OverLapped) 就是根据pOverLapped推算出来的包含地址为pOverLapped作为成员变量m_OverLapped的COverLapped对象的地址。
然后就分别调用:

//发送完成函数
bool CServerSocketItem::OnSendCompleted(COverLappedSend * pOverLappedSend, DWORD dwThancferred);

//接收完成函数
bool CServerSocketItem::OnRecvCompleted(COverLappedRecv * pOverLappedRecv, DWORD dwThancferred);

为毛要区分Send OverLapped 和 Recv OverLapped呢,,,
应为投递一次Send不一定是瞬间完成的,在处理的过程中存储数据的内存应该是锁定的,也就是不允许修改的,,,所以OverLapped应该自己管理内存。
而recv应该也是需要有一片内存直接接受数据的,很奇怪netFox没有提供,,,

recv居然是在投递接受请求的时候给了一个空的buffer,然后在完成回调中自己再次调用recv方法接受数据。
接受有关的成员变量如下:
     // 状态变量
protected:
     bool                            m_bNotify;                             // 通知标志
     bool                            m_bRecvIng;                             // 接收标志
     bool                            m_bCloseIng;                         // 关闭标志
     bool                            m_bAllowBatch;                         // 接受群发
    WORD                            m_wRecvSize;                         // 接收长度
    BYTE                            m_cbRecvBuf[SOCKET_BUFFER*5];         // 接收缓冲
int iRetCode=recv(m_hSocket,(char *)m_cbRecvBuf+m_wRecvSize,sizeof(m_cbRecvBuf)-m_wRecvSize,0);
难道这么蠢的做法只是为了躲开分包算法?
具体的看看接受代码:
// 接收完成函数
bool CServerSocketItem::OnRecvCompleted(COverLappedRecv * pOverLappedRecv, DWORD dwThancferred)
{
     // 效验数据
    ASSERT(m_bRecvIng== true);

     // 设置变量
    m_bRecvIng= false;
    m_dwRecvTickCount=GetTickCount();

     // 判断关闭
     if (m_hSocket==INVALID_SOCKET)
    {
        CloseSocket(m_wRountID);
         return  true;
    }

     // 接收数据
     int iRetCode=recv(m_hSocket,( char *)m_cbRecvBuf+m_wRecvSize, sizeof(m_cbRecvBuf)-m_wRecvSize,0);
     if (iRetCode<=0)
    {
        CloseSocket(m_wRountID);
         return  true;
    }

     // 接收完成
    m_wRecvSize+=iRetCode;
    BYTE cbBuffer[SOCKET_BUFFER];
    CMD_Head * pHead=(CMD_Head *)m_cbRecvBuf;

     // 处理数据
     try
    {
         while (m_wRecvSize>= sizeof(CMD_Head))
        {
             // 效验数据
            WORD wPacketSize=pHead->CmdInfo.wDataSize;
             if (wPacketSize>SOCKET_BUFFER)  throw TEXT("数据包超长");
             if (wPacketSize< sizeof(CMD_Head))  throw TEXT("数据包非法");
             if (pHead->CmdInfo.cbMessageVer!=SOCKET_VER)  throw TEXT("数据包版本错误");
             if (m_wRecvSize<wPacketSize)  break;

             // 提取数据
            CopyMemory(cbBuffer,m_cbRecvBuf,wPacketSize);
            WORD wRealySize=CrevasseBuffer(cbBuffer,wPacketSize);
            ASSERT(wRealySize>= sizeof(CMD_Head));
            m_dwRecvPacketCount++;

             // 解释数据
            WORD wDataSize=wRealySize- sizeof(CMD_Head);
             void * pDataBuffer=cbBuffer+ sizeof(CMD_Head);
            CMD_Command Command=((CMD_Head *)cbBuffer)->CommandInfo;

             // 内核命令
             if (Command.wMainCmdID==MDM_KN_COMMAND)
            {
                 switch (Command.wSubCmdID)
                {
                 case SUB_KN_DETECT_SOCKET:     // 网络检测
                    {
                         break;
                    }
                 defaultthrow TEXT("非法命令码");
                }
            }
             else 
            {
                 // 消息处理
                m_pIServerSocketItemSink->OnSocketReadEvent(Command,pDataBuffer,wDataSize, this);            
            }

             // 删除缓存数据
            m_wRecvSize-=wPacketSize;
            MoveMemory(m_cbRecvBuf,m_cbRecvBuf+wPacketSize,m_wRecvSize);
        }
    }
     catch ( )
    { 
        CloseSocket(m_wRountID);
         return  false;
    }

     return RecvData();
}

这是还是有分包算法的,总的来说接受流程如下:
直接使用recv把数据接受到SocketItem的缓冲区中,当长度大于CMD_HEAD之后,进入处理阶段,处理head数据各种判断,然后将数据扔出去,再调整缓冲区,,,

简单的说:
Send完全不考虑同步问题,不管一个劲的网队列投递Send请求,,,这边处理队列也是直接Send完事,完全不考虑上一次是否send成功,,,
Recv更是莫名其妙的使用完成端口绕一圈还回到recv直接接受了,,,

很狗血的做法,,,

更正下我自己狗血的不理解:
如果一个服务器提交了非常多的重叠的receive在每一个连接上,那么限制会随着连接数的增长而变化。如果一个服务器能够预先估计可能会产生的最大并发连接数,服务器可以投递一个使用零缓冲区的receive在每一个连接上。因为当你提交操作没有缓冲区时,那么也不会存在内存被锁定了。使用这种办法后,当你的receive操作事件完成返回时,该socket底层缓冲区的数据会原封不动的还在其中而没有被读取到receive操作的缓冲区来。此时,服务器可以简单的调用非阻塞式的recv将存在socket缓冲区中的数据全部读出来,一直到recv返回 WSAEWOULDBLOCK 为止。 这种设计非常适合那些可以牺牲数据吞吐量而换取巨大 并发连接数的服务器。当然,你也需要意识到如何让客户端的行为尽量避免对服务器造成影响。在上一个例子中,当一个零缓冲区的receive操作被返回后使 用一个非阻塞的recv去读取socket缓冲区中的数据,如果服务器此时可预计到将会有爆发的数据流,那么可以考虑此时投递一个或者多个receive 来取代非阻塞的recv来进行数据接收。(这比你使用1个缺省的8K缓冲区来接收要好的多。)

源码中提供了一个简单实用的解决WSAENOBUF错误的办法。我们执行了一个零字节缓冲的异步WSARead(...)(参见 OnZeroByteRead(..))。当这个请求完成,我们知道在TCP/IP栈中有数据,然后我们通过执行几个有MAXIMUMPACKAGESIZE缓冲的异步WSARead(...)去读,解决了WSAENOBUFS问题。但是这种解决方法降低了服务器的吞吐量。

总结: 

解决方法一: 

投递使用空缓冲区的 receive操作,当操作返回后,使用非阻塞的recv来进行真实数据的读取。因此在完成端口的每一个连接中需要使用一个循环的操作来不断的来提交空缓冲区的receive操作。 

解决方法二: 

在投递几个普通含有缓冲区的receive操作后,进接着开始循环投递一个空缓冲区的receive操作。这样保证它们按照投递顺序依次返回,这样我们就总能对被锁定的内存进行解锁。 



///
如果一个服务器同时连接了许多客户端, 对每个客户端又调用了许多 WSARecv, 那么大量的内存将会被锁定到非分页内存池. 锁定这些内存时是按照页面边界来锁定的, 也就是说即使你 WSARecv 的缓存大小是 1 字节, 被锁定的内存也将会是 4k. 非分页内存池是由整个系统共用的, 如果用完的话最坏的情况就是系统崩溃. 一个解决办法是, 使用大小为 0 的缓冲区调用 WSARecv. 等到调用成功时再换用非阻塞的 recv 接收到来的数据, 直到它返回 WSAEWOULDBLOCK 表明数据已经全部读完. 在这个过程中没有任何内存需要被锁定, 但坏处是效率稍低.
最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口(IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据(发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值