IOCP知识点及疑惑

原文地址:http://hzdiy.iteye.com/blog/778194

2. IOCP发送大数量的问题 

有A,B两块数据,如AB两块数据,如果A数据比较大,异步只发送了一部分就返回了,B数据已经提交,¬这时候再发A剩下的部分就乱顺序了 ,该如何处理。 
所有重叠操作可确保按照应用程序投递的顺序执行. 然而, 不能确保从完成端口返回的完成通知也按照上述顺序执行". 由此可见, "操作的执行"和"操作结果的通知"这二者的顺序并不能保证完成一致. 在这种情况下, 在这种情况下, 为每个客户端单独作一个发送队列, 有利于进行发送控制. 对于同一个客户端而言,  前一次发送的结果没有返回之前, 针对于同一socket, 不再发后续数据.当WSASend的时候,你的应用需要把应用缓冲提交给 socket缓冲,然后系统会把socket缓冲提交给TCP缓冲。这就算是一次完成的WSASend过程。 当你的应用把应用缓冲提交给 socket缓冲成功后(注意,这个时候,应用缓冲并没有提交到TCP缓冲),这个时候,Get...就成功回收了(10M)。 你Get..回收成功了,并不代表你的所有的数据都发送出去了。可能他们都在TCP缓冲里(TCP缓冲也是有个最大值的,并不是提交任何大都可以,你可以尝试把¬10M提高继续测试下)。  IOCP是要么提交成功要么提交失败,所以不用考虑发送出去半个包的情况。如何知道实际发送的数据量,这个拍拍脑袋就能想到办法, 
接收方收到发送方的包回一个确认包就可以了,发送方收到确认包以后就知道成功发送的数据量。在发送方, 如何知道已经发了多少数据量, 还是依靠GET函数的返回数比较准确. 这个数字, 本身也是来自于底层TCP通信时的协议操作结果.我的意思是制订一个通讯协议以后就可以拆包和组包, 因此在收到一个完整包以后就可以给发送方发包确认。 

2. IOCP:GetQueuedCompletionStatus¬返回后,完成事件处理时间相当长。 

感觉单独用线程处理请求要好一些,但是从我刚才性能分析来看 在通常情况下I/O线程和处理线程合并起来性能要好一些,同时处理耗时操作的线程比单独用来处理耗时操作的线程个数要多,所以性能当然要好一些。 
但是,带来一个负面问题,本来我使用一个队列和一个线程来处理数据包,这样在游戏的逻辑部分不存在线程同步的问题。 如果将处理过程放进GET线程的话,就变成N多个线程处理数据包了,虽然在socket的读取这层性能提高了,但是当数据包到达逻辑层后, 
还是需要频繁的加锁的阿~同样是有消耗的,甚至消耗会更大。 
IOCP有多个get线程,这些线程互不相关,只负责处理socket上的读取,以及读取后针对不同的session进行拼包。 这个过程不需要做任何的同步锁定。 当拼包完成后,即获取到了一个完整的逻辑包时,将其插进处理队列,这里队列需要锁定一次。 然后,有一个单独的处理线程,从队列中取逻辑包出来处理,这个线程即逻辑层的单线程。 这样做的好处是,尽量将没有逻辑意义的处理放进多个线程处理,将最后的结果(即需要逻辑处理的东西),交给单线程处理。 以最大限度的缩短单线程处理数据包的时间。 关于多线程还是单线程, 这个问题的争论由来已久. 总体而言, 我们需要在这两者之间作个权衡, 我比较赞同的观点是: 主逻辑线程只有一个, 如果主逻辑线程中存在耗时较长的逻辑, 则想办法分离, 分离的方法其中之一就是看这部分逻辑能否并行处理, 如果可以, 就在这里作"分阶段"的多线程. 主逻辑线程为多个时,带来的是调试和纠错等相关的时间开销. 这类服务器的业务逻 
辑I/O负载比较大,根本无法单线程处理。在这种环境下开多个GET*** 
就比通过队列单线程来处理业务逻辑要高效的多。 
我认为调用Get***和处理在同一个线程也没关系,只要开个线程足够多,比如10个Thread。 
比你开4个Get**线程,6个处理线程的效率肯定要高,因为线程间要通过队列来传递数据, 
如果10个线程都是Get***,没有这部分内存拷贝(或者内存池分配释放)和数据加锁的开销。 
效率当然要高。1其他请求会排队的。 2,应该是得到事件。 3, WINDOWS会跟踪状态。一个线程阻塞了,其他线程会得到 时间处理。 
现在的做法是每次调用wsarecv时投递下去的缓冲区长度就是下次希望收到的完整数 
据包的长度, 每次GetQueuedCompletionStatus返回后检查缓冲区是否填满,若没填满则继续下次 GetQueuedCompletionStatus调用, 若填满了就调用数据处理流程。 

结论: 同步的操作和不费时的操作,尽量放在多线程中执行,需要异步的,比如数据库操作,可以用单独的线程处理(队列),也可以开多个工作线程来处理。 

3. IOCP投递数据包顺序的问题: 
问题一: 
在一个连接上,我投递一个WSASend发送1K的数据出去,这时候下面的缓冲区里却只剩下512字节的空间,这时候,内核就会把这1K数据里的512字节拷贝¬到缓冲区里发出去,然后在GetQueueCompleteStatus里返回TRUE,并且在参数里指明只发送了512字节,这时候我们需要继续发送剩下的5¬12字节才能保证把整个1K的数据发送完成,如果在GetQueueCompleteStatus返回后,我们继续发送剩下的512字节的这一个间隙,,刚好另¬外一个线程投递了一个WSASend发送1K的数据,而这时候下面的缓冲区空了,这1K的数据将被完全发送出去,然后内核才调度到我们继续发送剩下的512字节¬的这个线程,那么在客户端收到的数据就成了这样:  512字节+1K字节+512字节,而实际上我们希望客户端收到的数据是: 1K(第一次发送的)+1K(第二次WSASend)发送的。 同样,如果多次在一个连接上调用WSARecv,也会出现数据错乱的问题。 使用异步IO的时候,最好能保证上一个IO彻底完成后才继续发出下一个IO。 
答:一次只投递一个。在带宽不够的情况下,完成端口没有完成投递给他的-工作就返回了完成包. 
问题二: 
事情是这样的,网络层使用IOCP,局入网内7千链接,每条链接上每秒平均收发1K数据没问题(每个包的大小小于1K) 当放到外网上,外网服务器是2M带宽独占的,2000个链接,每链接上每秒发送150个字节(已经超过2M的带宽了),IOCP出现数据没有发送完全,却返回了¬完成包的情况,这个时候查看这条链接上有将近100个包还没有发送出去,不知道大家遇到过这样的情况没有,是不是IOCP的正常情况,有什么解决办法 

iocp只是把数据重新拷贝到tcp的缓冲中,返回包中的WSABUF结构中的长度是说明了有多少数据拷贝到tcp的缓冲区中,上层根据这个来决定¬哪些数据需要重新发送。 

答:iocp只是把数据重新拷贝到tcp的缓冲中,返回包中的WSABUF结构中的长度是说明了有多少数据拷贝到tcp的缓冲区中,上层根据这个来决定¬哪些数据需要重新发送。完成端口是一种通知机制,你向系统提交一个发送请求,之后就不用管了,系统会维护好数据发送的状态,然后数据全部发送(tcp缓冲区大于或等于要发¬送的数据)或部分发送之后(tcp缓冲区小于要发送的数据),通知你,这样做的性能提高在于事情是系统去做,系统能够进行最大的优化,比你自己维护要高效多了。 
像你所说的,向iocp提交多次请求,一般很少这样做,一般都是提交一次,完成之后再提交,多次提交可能会出问题的,系统不保证多次提交的操作的序列话。 

关闭缓冲区只是对于一些对实时要求很高的情况才会关闭,而且即使关闭,TCP那边还是有缓冲,因为有可能丢包需要TCP重新发送。解决办法很简单,那就是一发一收,收到上个包的回复以后发送下一个包。我的理解是WSASend调用返回,但是WSABuf提交的buffer被锁住,直到Get***返回,这个时候buffer被解锁定。 而不是底层收到ack以后WSASend返回。 

假设当前一个tcp连接,底层缓冲还剩余100个字节,这个时候你首先提交一个发送操作,发送1000个字节,然后再提交一个操作¬,50个字节, 能是收到两次发送50个字节的完成通知? 
应该是收到一个1000字节返回,和50字节返回的通知,而不是两次50字节的通知。TCP缓冲不够,1000字节发送了100字节出去,Get**不返回,后面提交的100字节请求Get**也不返回,直到socket 
buffer有空,即使socket buffer有空,也会先COPY那1000个字节剩下的900字节再COPY后提交的100字节,而不会先COPY后来提交的100字节,关于这个,WIND¬OWS网络编程上有介绍,但是Get返回的两次提交的顺序是没有保证,不保证前提交的请求完成通知先返回。讨论结果就是Get**返回只说明数据COPY到AFD缓冲区里,至于是否发送出去看运气了。假设你刚get**成功,然后网线断开了那就Get**再次返回出错或者你下次调用WSASend/WSARecv出错,  TCP有多种机制来控制发送的速度不会超过接收方的处理能力导致网络上大量的包涌塞,滑动窗口机制应该是其中之一。客户端的处理能力也会导致发送方的¬阻塞,不单单是网络带宽。 





4. 消息如何分发 
象消息分发,如果使用OO特性的调用,是很消耗时间的,但是我们可以在很消耗时间的地方进行优化,函数调用不用进行寻址,直接运行。可以象下列方式 
class CMsg 

int type(); 
void run(command*); 

class CMsgMan 

CMsg::run runlist[10240]; 
//先注册进来 
AddMsg(Msg* pMsg) 

  runlist[pMsg->type]=pMsg->run; 

//消息分发 
void runCommand(type,command* pCommand) 

  runlist[type](pCommand); 


大规模的开发中需要分很多层次来做,各个模块尽量独立,相互之间的接口尽量简单,在需要效率的地方进行优化。其实模块独立了,互相之间很少依赖,这也是达到了O¬O的要求 


5. 关于完成端口中套接字的关闭问题 
想请教一下,如何主动关闭一个关联到完成端口的连接.非常感谢 

不管在哪个线程里closesocket之后,GET函数都会返回,RECV失败的事件,表示这个socket断开了。 你可以在这个事件里处理释放对应的session资源,而不是closesocket之后立即释放。closesocket后,GetQueuedComplitionStatus会返回错误,你的某个worker thread会被激活,在这个线程中调用GetLastError,系统会告诉你出错的原因。不要在关闭socket的线程释放与这个socket关联的per io data,而是应该在被唤醒的线程中释放。 
我想这里面的焦点问题在于投递的IO请求完成后都是放在一个 completion packet 队列里面等待 GetQueuedCompletionStatus 取出的。你在关闭 socket A 的时候,completion packet 
队列里面可能还有和这个 socket A 有关的 completion packet 。如果你在关闭 socket A 的时候把和 socket A 有关的资源,比如session,都释放了,那以后再处理队列里面和 socket A 有关的completion packet的话肯定会出错了。你在关闭  socket A 之后并不要马上把相关资源都释放了,要等到所有的 socket A有关的completion packet 处理完,其中GetQueuedCompletionStatus 返回的最后一个和 socket A 相关的 completion packet 会提示出错,也就是函数的返回值是 ERROR_SUCCESS ,同时 dwNumberOfBytesTransferred 为0。这个时候就可以放心大胆地释放相关的资源了。 


6. 数据封包、拚包时遇到的一个简单的问题 
基本格式是:前两个字节为逻辑包的大小 + 数据包的类型 + 数据包的内容。 问题是:在正常情况下,包的前两个字节是逻辑包的大小。 但是假如我到了一个数据包,数据包的内容是 "abc". 这个包的前两个字节并不是正常逻辑包的大小信息,在这种情况下,我应该如何判断包的前两个字节是否表示的是逻辑包的大小信息? 

不用再另外加包尾标志, 有逻辑包长度信息, 就已经暗含结尾是在何处. 反过来说, 即使你加了这个结尾标志, 我还是可以给你设计一个非法的结尾标志, 所以, 增加结尾标志没有任何意义. 这种情况下, 只能根据你收到的逻辑包大小来收剩下的内容, 由于TCP流传输的特点, 你没法判定后面还会不会给你发剩下的包,  所以, 接收操作是比较被动的, 尽管这个内容可能并不是你想的合法的内容.  而这个包的具体格式和数据内容到底非法还是合法,  并不由网络层来处理, 交给逻辑层处理会更为合适. 
对待恶意攻击, 你的网络底层应该足够强壮, 主要是一些边界值处理好就行了, 比如:当确定的逻辑包长度小于你指定的逻辑包最小长度时, 这个包明显是错误的, 要丢弃或关闭当前连接; 同理, 如果你定义了逻辑包最大长度, 那这个边界值也要检验. 网络层只负责处理这些边界值并负责把属于边界值以内的逻辑包接收下来即可,  具体的包内容判断交给上层去作. 
逻辑层对每一个解析后的数值进行边界检查是肯定必须的。不过不一定是边界检查,只要是逻辑上正确就行了。 例如client发来一个人物ID(Login时选人),不论这个值是什么,只要这个ID不存在,或这个这个ID不属于这个账号,就是错误的。 

传奇用的是字符串协议,用分隔符, 先把2进制数据用类BASE64算法处理一下,然后加上分隔符, 
这样即使出现错误的包,只要拆包算法写的好,有重新定位功能,就可以继续收下一个包。如果是2进制 
协议,里面的长度字段错误后面,而后面恰好没有发全一个包, 那么整个次序就乱了,因为后面发的半个包有可能和下一个包叠在一起,最 坏情况下完全乱掉。服务器与服务器之间的通讯采用2进制协议,依靠包头的封包长度解包, 而客户端与服务器之间的通讯,采用的是base64编码的方式,通过一个包头字符与一个包尾字符解包。带包对和包尾标志的协议会比只带包长度的协议在重新定位方面表现要好得多, 
只带包长度的, 确实如无忌所言容易造成后面的包也连在一起, 而它的重新定位就可能是错了N个包之后的了, 带包头和包尾的却可以只影响当前包. 
每一个通信包,都设计一个结构,然后通信时直接把结构发送出去就行了,解包则直接根据包类型强制转换成各种相应的结构。 

7. lpCompletionKey问题 
GetQueuedCompletionStatus 函数的 
PULONG_PTR lpCompletionKey 
是用来存放Socket相关的数据的,一般是一个 
指针,里面存放 SOCKET 等数据 
在我现有的一个服务器中,这个值是一个DWORD 整形值,这个DWORD充当索引,SOCKET相关的数据是查 map 得到的,也就是说每次 Send,Recv 都要访问这个map,通过这个DWORD来查到SOCKET 
等数据,然后操作 
Send(DWORD dwPerSockID, memblock block, size_t nLength){ 
    SOCKET socket = 0; 
    m_mapPerSocket.find(dwPerSockID, socket); 
    WSASend(socket, DataBuf, 1, &NumBytes, 0, &PerIOData->Overlapped, 
0); 
    ... 

ThreadProc(void* lpParam){ 
    DWORD nSocketID = 0; 
    GetQueuedCompletionStatus(m_hIOCP, &NumBytes, &nSocketID, 
&lpOverlapped, INFINITE); 
    .... 
    OnSend(nSocketID, PerIoData, NumBytes); 


答:设计的不合理,不需要map查找。 

GetQueuedCompletionStatus函数的原型如下: 
  WINBASEAPI 
  BOOL 
  WINAPI 
  GetQueuedCompletionStatus( 
      IN  HANDLE CompletionPort, 
      OUT LPDWORD lpNumberOfBytesTransferred, 
      OUT PULONG_PTR lpCompletionKey, 
      OUT LPOVERLAPPED *lpOverlapped, 
      IN  DWORD dwMilliseconds 
     ); 
  其中,我们把第三个参数lpCompletionKey称为完成键,由它传递的数据称为单句柄数据。我们把第四个参数lpOverlapped称为重叠结构体,由它传递的数据称为单IO数据。 

  以字面的意思来理解,lpCompletionKey内包容的东西应该是与各个socket一一对应的,而lpOverlapped是与每一次的wsarecv或wsasend操作一一对应的。 

  在网络模型的常见设计中,当一个客户端连接到服务器后,服务器会通过accept或AcceptEx创建一个socket,而应用层为了保存与此socket相关的其它信息(比如:该socket所对应的sockaddr_in结构体数据,该结构体内含客户端IP等信息,以及为便于客户端的逻辑包整理而准备的数据整理缓冲区等),往往需要创建一个与该socket一一对应的客户端底层通信对象,这个对象可以负责保存仅在网络层需要处理的数据成员和方法,然后我们需要将此客户端底层通信对象放入一个类似于list或map的容器中,待到需要使用的时候,使用容器的查找算法根据socket值找到它所对应的对象然后进行我们所需要的操作。 

  让人非常高兴的是,完成端口“体贴入微”,它已经帮我们在每次的完成事件通知时,稍带着把该socket所对应的底层通信对象的指针送给了我们,这个指针就是lpCompletionKey。也就是说,当我们从GetQueuedCompletionStatus函数取得一个数据接收完成的通知,需要将此次收到的数据放到该socket所对应的通信对象整理缓冲区内对数据进行整理时,我们已经不需要去执行list或map等的查找算法,而是可以直接定位这个对象了,当客户端连接量很大时,频繁查表还是很影响效率的。哇哦,太帅了,不是吗?呵呵。 

  基于以上的认识,我们的lpCompletionKey对象可以设计如下: 
  typedef struct PER_HANDLE_DATA 
  { 
    SOCKET socket;             //本结构体对应的socket值 
    sockaddr_in addr;          //用于存放客户端IP等信息 
    char DataBuf[ 2*MAX_BUFFER_SIZE ];  //整理缓冲区,用于存放每次整理时的数据 
  } 

OVERLAPPED是应用层与核心层交互共享的数据单元,如果要执行一个重叠IO操作,必须带有OVERLAPPED结构。在完成端口中,它允许应用层对OVERLAPPED结构进行扩展和自定义,允许应用层根据自己的需要在OVERLAPPED的基础上形成新的扩展OVERLAPPED结构。一般地,扩展的OVERLAPPED结构中,要求放在第一个的数据成员是原OVERLAPPED结构。我们可以形如以下方式定义自己的扩展OVERLAPPED结构: 
  typedef struct PER_IO_DATA 
  { 
    OVERLAPPED ovl; 
    WSABUF           buf; 
    char                    RecvDataBuf[ MAX_BUFFER_SIZE ];   //接收缓冲区 
    char                    SendDataBuf[ MAX_BUFFER_SIZE ];   //发送缓冲区 
    OpType              opType;                                                       //操作类型:发送、接收或关闭等 
  } 
   
  在执行WSASend和WSARecv操作时,应用层会将扩展OVERLAPPED结构的地址传给核心,核心完成相应的操作后,仍然通过原有的这个结构传递操作结果,比如“接收”操作完成后,RecvDataBuf里存放便是此次接收下来的数据。 

个人理解: 其实lpCompletionKey里可以包含socket 和OVERLAPPED信息。这样在投递的时候,绑定的时候, 直接丢指针下去: 
hTemp = CreateIoCompletionPort(pClientContext ->m_hSocket(), hPort, (DWORD) pClientContext, m_dwConcurrency); 

UINT nRetVal = WSASend(pClientContext->m_Socket, 
pClientContext->m_pSendBuffer->GetWSABuffer(), 
1, 
&dwIoSize, 
ulFlags, 
&pClientContext->m_pSendBuffer->m_overlap, 
NULL);
至于用什么客户ID(自定义格式)和逻辑层交互,可以自己定义结构体,并直接保存pClientContext指针就行了。 




8. server中的定时机制(timer)该怎么做 
win下的定时器有下面几种。 
1,消息机制的,也就是你说的。 
2,多媒体定时器 winmm.h中。 
3,队列定时器,queue timer 
4,CreateWaitableTimer定时器。 
这几种我都用过,感觉queue timer最方便。 
CreateTimerQueueTimer() win32 api 


9. WSASend的时候,是每个客户端一个发送列表,还是所有的客¬户端一个发送列表? 
我以前的是逻辑处理完后,就直接WSASend,当然,这样做,需要同步(如果有两次提起WSASend,必须等待第一次WSASend完成),如果发送不成功¬的,需要再次提起WSASend。 我发现这个同步可能是个效率问题。 现在想使用单独的一个发送线程来发送。 
1 每个socket都有一个自己的发送队列。 当逻辑处理完后,把需要发送的数据包提交到每个socket的发送队列。(发送队列有n个,和socket的数目相等) 
2 所有的socket同用一个发送队列。 当逻辑处理完后,把需要发送的数据包提交到所有的socket的共享的发送队列。(发送队列只有一个) 发送线程就不停地遍历,有发送需求的,就提起WSASend,发送不完全的,再次提起WSASend. 

答:完成端口根本没必要维护对列,对与WSASend发出的异步操作操作,由操作系统保持消息队列,由系统保证发送次序。没必要为每个CLIENT的SOCKET维护一个发送队列~ 
你可以在任何地方直接用WSASend发出一个异步操作就行了,不需要担心同时多步WSASend异步引起的乱序问题,因为有M$的内部保存了异步发送的队列。如果你以同步方式调用WSASend你需要自己保证乱序问题了WSASend可以一次提交多个写操作。 
所以,多个提交是没有问题的,但Get...回收的顺序却是不一定的。就是说,你提交的顺序和Get...回收的顺序可能是不同的。 
WSASend异步操作的确可以保证每个异步的完整发送,我曾经也为这个问题困扰,但是后来经过多方查阅资料,和自己高密度的测试表明,没有多个线程序并发的发¬出异步WSASend,发送的WSABUF乱序问题。而对于完成端口接受到回应的顺序是不一定的,这点是肯定的,因为完成端口内部本身就不是一个先进先出的队列¬,而是有种类似先进后出的堆栈格式的,也许还有优先级什么的,这点我没有深究。 
对此我也觉得不会对程序影响的,反正程序本来就是异步的处理过程,与顺序无关的。 

对于完成端口多线程的时候,收包其实也不是乱序的,关键,是线程的执行不确定! 比如线程A收到了包1却没有处理时,线程B收到了包2立即就处理了,这样就会有乱序的问题! 不过也有解决方法,线程A收到了包1后,暂时不投递收包请求, 这样就不会有线程2收到包2了! 不过这个方法串行化了处理, 对于包顺序不敏感的程序,就不要使用这个方法了! 



10.UDP丢包的问题 
我现在做的项目设计到MPEG4、H.264等视频流网络传输。因为要穿越NAT,所以要用UDP。MPEG4、H.264¬码流如果丢包,就会出现马赛克,严重影响观看效果。重传机制只有在网络条件好的情况下才能完全解决丢包问题。 
我测试过在100M局域网内一路3MB/p以上的udp码流会经常丢包,码流越大丢包率越高。 
**********但是用抓包软件查看,其实机器已经收到了所有的udp包!!!!!! 
我用过以下方法接收3MB/s 数据: 
select       平均3minute丢2个包。 
WSAAsyncSelect 同上。 
50MB/s 数据 
select       平均1minute丢10个包。 
WSAAsyncSelect 同上。 
提高线程优先级可以降低丢包率,但是严重影响其他线程工作。 
现在考虑用IOCP是否能显著降低丢包率。 
但是在程序阻塞在GetQueuedCompletionStatus()。能否提供一个UDP 
SOCKET使用IOCP的例程? 
答:你出现的问题是协议底层的问题,和使用什么I/O模型没有关系的。 UDP确实会丢包,你说的3M/s出现丢包是正常的。现在一般的网络视频都是512kbps左右,再高由于网络带宽的问题基本上就没有使用价值。很明显是socket的缓冲暂时满了所以丢包,原因是你的包处理部分太慢了,估计可能做了比较多事情才回来读入下一个包,导致socket缓冲读取不及,如果用¬IOCP,同样会socket缓冲满,唯一不同的是IOCP将要求发送方重发,而UDP则不能。解决办法依据以上,一是提高包处理速度,比如读取包后放入队列由¬别的线程处理,本线程继续读包,一般这样做就已经可解决大多数处理不及,二是设立一个UDP响应机制,比如在UDP包中加入序列号,如果接收的序列号缺少,则发¬送信息到发送方请求重发,这样要改动的地方较多,可以参考rts和rtcp  协议的实现。 


11.程序退出和网络断开的区别(检测死连接) 

一般是下面2种解决办法。根据具体情况灵活使用。 
1,定时向所有用户发送存活包。 
2,定时检查全部用户,如果有用户一定时间没有发送有效请求到服务器就关闭连接。 

int BX_Acceptor::ChangeSocketModeAfterAccept( SOCKET Socket ) 

        BOOL bError; 
        bError = FALSE; 
        if ( SOCKET_ERROR == setsockopt(Socket, SOL_SOCKET, 
SO_UPDATE_ACCEPT_CONTEXT, (char *)&m_ListeningSocket, sizeof 
(m_ListeningSocket))) 
        { 
                NETWORK_REPORT( "setsockopt is failed. Error code =" ); 
                NETWORK_REPORT( WSAGetLastError() ); 
                bError = TRUE; 
                goto ErrHand; 
        } 
        int nKeepAlive = -1; 
    int nOptLen = sizeof(nKeepAlive); 
        // 第一:获取该SOCKET的KeepAlive设置状态 
        if ( getsockopt( Socket, SOL_SOCKET, SO_KEEPALIVE, (char 
*)&nKeepAlive, &nOptLen ) == SOCKET_ERROR) 
        { 
                NETWORK_REPORT( "getsockopt is failed. Error code =" ); 
                NETWORK_REPORT( WSAGetLastError() ); 
                bError = 1; 
                goto ErrHand; 
        } 
        // 第二:设置该SOCKET的KeepAlive设置状态 
        nKeepAlive = 1; 
        if (setsockopt( Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&nKeepAlive, 
nOptLen) == SOCKET_ERROR) 
        { 
                NETWORK_REPORT( "setsockopt is failed. Error code =" ); 
                NETWORK_REPORT( WSAGetLastError() ); 
                bError = 1; 
                goto ErrHand; 
        } 
        // 第三:获取该SOCKET的KeepAlive设置状态 
        nKeepAlive = -1; 
        if (getsockopt( Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&nKeepAlive, 
&nOptLen) == SOCKET_ERROR) 
    { 
        NETWORK_REPORT( "getsockopt is failed. Error code =" ); 
                NETWORK_REPORT( WSAGetLastError() ); 
                bError = 1; 
                goto ErrHand; 
    } 
        // 第四:设置KEEPALIVE的时间和参数 
        if( nKeepAlive == 1 ) 
        { 
                TCP_KEEPALIVE inKeepAlive = {0}; 
                unsigned long ulInLen = sizeof(TCP_KEEPALIVE); 
                TCP_KEEPALIVE outKeepAlive = {0}; 
                unsigned long ulOutLen = sizeof(TCP_KEEPALIVE); 
                unsigned long ulBytesReturn = 0; 
                //设置socket的keep alive为10秒,并且发送次数为3次 
                inKeepAlive.onoff                         = 1; 
                inKeepAlive.keepaliveinterval = 10000; 
                inKeepAlive.keepalivetime     = 3; 
                //为选定的SOCKET设置Keep Alive,成功后SOCKET可通过Keep 
Alive自动检测连接是否断开 
                if ( WSAIoctl( Socket, SIO_KEEPALIVE_VALS, 
                        (LPVOID)&inKeepAlive, ulInLen, 
                        (LPVOID)&outKeepAlive, ulOutLen, 
                        &ulBytesReturn, NULL, NULL) == SOCKET_ERROR) 
                { 
                        NETWORK_REPORT( "WSAIoctl is failed. Error code =" ); 
                        NETWORK_REPORT( WSAGetLastError() ); 
                        bError = 1; 
                        goto ErrHand; 
                } 
        } 
ErrHand: 
        if (TRUE == bError) 
        { 
                return RET_FAIL; 
        } 
        return RET_SUCCESS; 
1. LAN环境,也就是数据不依靠路由或者是使用NAT之类的网络环境。 
    在这种情况下,TCP是不会失效的,因为在其协议实现中,就有保持连接的控制。在客户端和服务器不主动断开的情况下,不会断开,这里指的不会断开,是说在Kee¬palive的有效时间之内,一般情况下,KeepAlive的时间都会很长的。 
2. 使用了地址映射的环境,比如常见的从内网通过NAT访问公网服务器的这种情况。 
    由于为了节约网络端口资源,所以路由器会在一个链路长时间没有有效数据的情况下,断开这条空闲链路的。通常情况下,路由或者防火墙,检测链路有效的时间间隔要远¬比keepalive的时间短。 



12. Server能接收多少TCP连接的问题 
如果大多数连接都在睡觉,不如用短连接,有需要的时候再连接上来,比如HTTP。 
或者用UDP,自己来做流控,支持个10W用户单机问题不大。 
如果需要大量的并发连接,不如换种策略,在应用服务器前面,架上多台网关服务器。 
由多台网关服务器将无数连接的数据转发到应用服务器。如果有10台网关,应用服务器也就只需要维护10个连接,局域网内和网关通信,速度可以不考虑。 而每台网关平均2K个连接,那么就2W个并发连接。 
而且网关服务器,可以做一些简单的数据过滤,将不同内容的数据,转发到不同的应用服务器。 

SOCKET句柄数,一个是port(端口)数,后者65536,代表本机可以在多少个端口上对外部访问进行监听。 

13. IOCP资料释放的问题 
在GetQueuedCompletionStatus处理循环以外关闭, 
你只需要closesocket就行了。 
之后GET函数会发现socket被关闭了,然后返回事件给你,你再释放相应的资源。 
通过closesocket()主动关闭套接字以后,不能立即释放相关的资源,应该在所有的操作都被GetQueuedCompletionStatus()响¬应之后才能释放。 

14. 关于"登陆服务器"的架构设计 
为了同时可容纳很多的人"登陆",我在"登陆服务器"中使用IOCP连接和接受客户端请求,单独开一个线程接受"数据库服务器"的数据,同 时又有几个socket连接着"大厅服务器"。 现在的问题就是:在这个"完成端口"的线程池处理函数中,只需要recv一次客户端的信息。而我每次有客户端连接的时候。都要new全局的"句 柄结构"和"数据结构体",把它和IOCP连接起来。等受到了消息后,就通过和"数据库服务器连接的socket"法给"服务器服务器"。这样子,我感 觉没有合理的运用iocp,并贴会new很多的"结构体",然后只能在返回给客户端消息后delete .这样子程序很快就没空间可分配了,尽管我只用 了"对象池" 

个人感觉登陆服务器有必要用完成端口或者EPOLL, 
1.因为登陆服务器是并发连接多. 
2.单连接通讯数据量小。 
3.用户不介意等个几秒。 
我做过的loginserver并发连接都比较高,经常5k并发连接以上。 
我一般的做法是前面2*CPU个数的iocp线程,后面8~12个db线程。基本上够用了。 

你的问题是没有做拥塞控制,接受连接的吞吐量超过了后端数据库IO的吞吐量,导致一段时间以后累积了很多后端来¬不及处理的连接,你又不控制拥塞导致内存耗尽。并发连接数意义不大,主要看你的每秒最大能处理的登陆请求数,如果你的后端最大每秒只能处理3000个请求,而你接受10000多个连接在用户层就没什么意义,¬还不如缓冲在网络层,网络层缓冲满了系统就开始拒绝连接,这可以防止风暴连接的时候把服务器压垮。我从来都是单线程non-blocking 
select,轮询一遍5000个socket是用不了多少时间的(大约几个ms),,登陆没必要提供很快的响应时间,你可以间隔上百ms才轮询一次,在收到一¬组用户数据后再批量传送给数据库验证,这里可以用线程池来提高IO使用效率。
至于连接则由网络层负责缓冲,你可以把缓冲区设置的大一些改善连接吞吐量,比方说我每隔50ms查询一遍侦听端口,连接缓冲设置为100,这样理论上说我每秒最¬多可以接受2000个新连接,缓冲区满了网络层就不再响应TCP握手消息了,这样最多让用户收到个连接超时的错误,对服务器来说不会造成很大的开销,反之你不加¬控制一股脑全连接进来又来不及处理,时间一长应用层累积了大量未处理连接可能导致资源耗尽,如果遇到DDOS攻击那么情况就更严重了。一旦接收到用户登陆数据就不再需要读取了,当然没必要再select了,一般来说连接建立到select会有一定的间隔时间,这点时间足够用户把登陆数据传¬过来了,因此第一次select的时候大部分情况有数据可读。对于长时间没有数据可读的连接做超时处理就可以了。至于DDOS么,最低要求是在被攻击的时候不应¬该把服务器压的资源耗尽而宕机,至于是否还能够正常服务,那另当别论。 
所以,服务器之间的数据交互,必须都带上给每个客户端分配的唯一标志,当收到另一个服务器传来的针对某一个客户端的数据时,第一件要做的事情,就是查询这个客户¬端是否还在线。 
通常我会将所有在线的用户,按ID、名字、或者临时ID,存进一个hashmap中,下线后就删掉,收到其他服务器传来的消息是,先找在这个map中查询一下,¬user是否在线,再做处理。单线程只该应用在复杂逻辑的服务器应用上,比如游戏的场景服务器,那种情况下,每个客户端连接之间,都存在很多需要共享的数据,多线程处理的话,需要大量的线程¬同步操作。 

15. 有关容器和容器中的数据项的同步问题 
有一个hash表,多个线程从中查找、插入或者删除对象,并对对象进行访问和修改;请问如何对其加锁,使得性能最高呢? 

1.使用一个Mutex 
  线程的流程大致如下: 
  { 
    加锁; 
    从hash表中获取对象; 
    对对象进行访问和修改; 
    解锁. 
   } 
   很明显,这种方案并发性很低。 
2. 使用一个读写锁和一个互斥锁 
  hash表访问: 
  { 
    给hash表加读锁; 
   从hash表中获取对象; 
   加互斥锁; 
   访问对象; 
   解互斥锁; 
   解hash表的读锁; 
  } 
  hash表修改 
  { 
   给hash表加写锁; 
   修改hash表; 
   解锁 
  } 
3.给hash表加读写锁,并且hash表中的每个对象拥有一个互斥锁; 
  但是这样互斥锁的数量会很庞大。 
答:你这么做,会导致需要锁两次。 一个线程,将要删除一个对象,它把整个表锁住了。 另一个线程需要修改一个对象,它单独锁住那个对象是没用的,因为另一个线程将要删除它了,所以这个线程也必须把整个表锁住,这样的效率太低了。 还不如直接只在整个表上加一个锁。 

我们目前的一个项目中也有类似问题,由于容器使用的是BerkeleyDB,本身可以多线程操作,但它的HASH类型只有表级锁。我的附加要求是多个线程不能同¬时操作同一条记录,所以自己做了一个"锁池",要操作某条记录时,先用这个记录的key来获取一个锁,这时候其它线程如果使用同一个key来获取这个锁会阻塞,¬不同的key就不会阻塞,相当于实现了一个记录锁的功能。由于获取锁的过程也要同步,所以性能可能不高,但如果为每条记录创建一个锁根本不可行,因为记录数有上¬亿条呢。 

16. Accept模式调查 

事先投递多个 AcceptEx()然后等线程返回, 我是多个线程调用的,我acceptex返回以后,我接受连接以后立刻再投递一个,因为iocp Get**是多线程里 
调用,所以应该是多线程里调用acceptex 

TransmitFile 和 TransmitPackets 可以通过指定TF_REUSE_SOCKET和TF_DISCONNECT标志来重用套接字句柄。每当API完成数据的传输工作后,就会在传输层级别断开连接,这样这个套接字就又可以重新提供给AcceptEx()使用。 

17. 关于完成端口如何判断与客户端失去连接的一些问题 
在利用完成端口写服务器时遇到了一些关于服务器如何判断与客户端失去连接的问题,描述如下: 服务器在初始化的时候在池中创建足够数量的PerHandleData 和PerIoData. 使用的时候在池中取即可。 
当一个客户端与服务器取得联系的时候在池中取一个PerHandleData. 失去连接的时候把PerHandleData 放回池中,以备其他客户端使用。 当客户端与服务器连接上以后再PerIoData池中取一个PerIoData,用于 提交一个接收操作,提交收的这个PerIoData, 只有在客户端与服务器失去连接的时候才放回PerIoData  池中。服务器在向客户端发送数据的时候也在PerIoData池中取一个PerIoData,用于提交一个WSASend 操作,发送完成以后便将PerIoData放回池中。 不知道这样的结构是否合理,大家指教一下。 
用于投递Recv的OV结构不一定要和客户端端对象绑死~ 如果你的拼包操作没有在GET线程做的话,那么可以收到数据时,直接把这个OV结构,插进拼包队列,避免memcpy。 然后重新取一个OV投递下个recv。 不过如果直接在GET线程做的拼包就无所谓了。这个方面比较灵活。在投递后返回非ERROR_IO_PENDING错误的时候,是直接closesocket的。每当client主动断开的时候,GET函数返回的数据长度是0。 
socket是关闭的情况下,recv或send出错是很正常的呀。 投递send如果没有一次发完,会返回发送了多少字节,这时可以考虑将剩下的重发,但是我是直接做closesocket处理的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值