您的位置:首页 > 理论基础 > 计算机网络

IOCP 网络通讯模型源码解读

2015-01-23 23:04 471 查看
From: http://hi.baidu.com/tsingsing/item/1aa5062fa27791fa50fd87b7
以前写服务器的时候用的是iocp,最近偶然发现windows的 网络通讯模型除了Iocp,还有好几种其他类型。这里总结下,感谢原贴作者。

IOCP 网络通讯模型源码解读

老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
  这和Socket模型非常类似。下面我就以老陈接收信件为例讲解Socket I/O模型。
  一:select模型
  老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信,在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
  select模型和老陈的这种情况非常相似:周而复始地去检查......如果有数据......接收/发送.......
  使用线程来select应该是通用的做法:
procedure TListenThread.Execute;
var
 addr : TSockAddrIn;
 fd_read : TFDSet;
 timeout : TTimeVal;
 ASock,
 MainSock : TSocket;
 len, i : Integer;
begin
 MainSock := socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
 addr.sin_family := AF_INET;
 addr.sin_port := htons(5678);
 addr.sin_addr.S_addr := htonl(INADDR_ANY);
 bind( MainSock, @addr, sizeof(addr) );
 listen( MainSock, 5 );
 while (not Terminated) do
 begin
  FD_ZERO( fd_read );
  FD_SET( MainSock, fd_read );
  timeout.tv_sec := 0;
  timeout.tv_usec := 500;
  if select( 0, @fd_read, nil, nil, @timeout ) > 0 then //至少有1个等待Accept的connection
  begin
   if FD_ISSET( MainSock, fd_read ) then
   begin
   for i:=0 to fd_read.fd_count-1 do //注意,fd_count <= 64,也就是说select只能同时管理最多64个连接
   begin
    len := sizeof(addr);
    ASock := accept( MainSock, addr, len );
    if ASock <> INVALID_SOCKET then
     ....//为ASock创建一个新的线程,在新的线程中再不停地select
    end;
   end;   
  end;
 end; //while (not self.Terminated)
 shutdown( MainSock, SD_BOTH );
 closesocket( MainSock );
end;
  二:WSAAsyncSelect模型
  后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天......不是,微软......
  微软提供的WSAAsyncSelect模型就是这个意思。
  WSAAsyncSelect模型是Windows下最简单易用的一种Socket I/O模型。使用这种模型时,Windows会把网络事件以消息的形势通知应用程序。
  首先定义一个消息标示常量:
const WM_SOCKET = WM_USER + 55;
  再在主Form的private域添加一个处理此消息的函数声明:
private
procedure WMSocket(var Msg: TMessage); message WM_SOCKET;
  然后就可以使用WSAAsyncSelect了:
var
 addr : TSockAddr;
 sock : TSocket;
 sock := socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
 addr.sin_family := AF_INET;
 addr.sin_port := htons(5678);
 addr.sin_addr.S_addr := htonl(INADDR_ANY);
 bind( m_sock, @addr, sizeof(SOCKADDR) );
 WSAAsyncSelect( m_sock, Handle, WM_SOCKET, FD_ACCEPT or FD_CLOSE );
 listen( m_sock, 5 );
 ....
  应用程序可以对收到WM_SOCKET消息进行分析,判断是哪一个socket产生了网络事件以及事件类型:
procedure TfmMain.WMSocket(var Msg: TMessage);
var
 sock : TSocket;
 addr : TSockAddrIn;
 addrlen : Integer;
 buf : Array [0..4095] of Char;
begin
 //Msg的WParam是产生了网络事件的socket句柄,LParam则包含了事件类型
 case WSAGetSelectEvent( Msg.LParam ) of
 FD_ACCEPT :
  begin
   addrlen := sizeof(addr);
   sock := accept( Msg.WParam, addr, addrlen );
   if sock <> INVALID_SOCKET then
    WSAAsyncSelect( sock, Handle, WM_SOCKET, FD_READ or FD_WRITE or FD_CLOSE );
  end;
  FD_CLOSE : closesocket( Msg.WParam );
  FD_READ : recv( Msg.WParam, buf[0], 4096, 0 );
  FD_WRITE : ;
 end;
end;
  三:WSAEventSelect模型
  后来,微软的信箱非常畅销,购买微软信箱的人以百万计数......以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使。微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以睡觉了。
  同样要使用线程:
procedure TListenThread.Execute;
var
 hEvent : WSAEvent;
 ret : Integer;
 ne : TWSANetworkEvents;
 sock : TSocket;
 adr : TSockAddrIn;
 sMsg : String;
 Index,
 EventTotal : DWORD;
 EventArray : Array [0..WSA_MAXIMUM_WAIT_EVENTS-1] of WSAEVENT;
begin
 ...socket...bind...
 hEvent := WSACreateEvent();
 WSAEventSelect( ListenSock, hEvent, FD_ACCEPT or FD_CLOSE );
 ...listen...
 while ( not Terminated ) do
 begin
  Index := WSAWaitForMultipleEvents( EventTotal, @EventArray[0], FALSE, WSA_INFINITE, FALSE );
  FillChar( ne, sizeof(ne), 0 );
  WSAEnumNetworkEvents( SockArray[Index-WSA_WAIT_EVENT_0], EventArray[Index-WSA_WAIT_EVENT_0], @ne );
  if ( ne.lNetworkEvents and FD_ACCEPT ) > 0 then
  begin
   if ne.iErrorCode[FD_ACCEPT_BIT] <> 0 then
    continue;
   ret := sizeof(adr);
   sock := accept( SockArray[Index-WSA_WAIT_EVENT_0], adr, ret );
   if EventTotal > WSA_MAXIMUM_WAIT_EVENTS-1 then//这里WSA_MAXIMUM_WAIT_EVENTS同样是64
   begin
    closesocket( sock );
    continue;
   end;
   hEvent := WSACreateEvent();
   WSAEventSelect( sock, hEvent, FD_READ or FD_WRITE or FD_CLOSE );
   SockArray[EventTotal] := sock;
   EventArray[EventTotal] := hEvent;
   Inc( EventTotal );
  end;
  if ( ne.lNetworkEvents and FD_READ ) > 0 then
  begin
   if ne.iErrorCode[FD_READ_BIT] <> 0 then
    continue;
    FillChar( RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
    ret := recv( SockArray[Index-WSA_WAIT_EVENT_0], RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
    ......
   end;
  end;
end;
  
四:Overlapped I/O 事件通知模型
  后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!
  Overlapped I/O 事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在“Overlapped”,Overlapped模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区~~~~~
Listen线程和WSAEventSelect模型一模一样,Recv/Send线程则完全不同:
procedure TOverlapThread.Execute;
var
 dwTemp : DWORD;
 ret : Integer;
 Index : DWORD;
begin
 ......
 while ( not Terminated ) do
 begin
  Index := WSAWaitForMultipleEvents( FLinks.Count, @FLinks.Events[0], FALSE, RECV_TIME_OUT, FALSE );
  Dec( Index, WSA_WAIT_EVENT_0 );
  if Index > WSA_MAXIMUM_WAIT_EVENTS-1 then //超时或者其他错误
   continue;
  WSAResetEvent( FLinks.Events[Index] );
  WSAGetOverlappedResult( FLinks.Sockets[Index], FLinks.pOverlaps[Index], @dwTemp, FALSE,FLinks.pdwFlags[Index]^ );
  if dwTemp = 0 then //连接已经关闭
  begin
   ......
   continue;
  end else
 begin
  fmMain.ListBox1.Items.Add( FLinks.pBufs[Index]^.buf );
 end;
 //初始化缓冲区
 FLinks.pdwFlags[Index]^ := 0;
 FillChar( FLinks.pOverlaps[Index]^, sizeof(WSAOVERLAPPED), 0 );
 FLinks.pOverlaps[Index]^.hEvent := FLinks.Events[Index];
 FillChar( FLinks.pBufs[Index]^.buf^, BUFFER_SIZE, 0 );
 //递一个接收数据请求
 WSARecv( FLinks.Sockets[Index], FLinks.pBufs[Index], 1, FLinks.pdwRecvd[Index]^, FLinks.pdwFlags[Index]^, FLinks.pOverlaps[Index], nil );
end;
end;
  五:Overlapped I/O 完成例程模型
  老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件......为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读/回复了!老陈终于过上了小资生活!
  Overlapped I/O 完成例程要求用户提供一个回调函数,发生新的网络事件的时候系统将执行这个函数:
procedure WorkerRoutine( const dwError, cbTransferred : DWORD;
const
lpOverlapped : LPWSAOVERLAPPED; const dwFlags : DWORD ); stdcall;
  然后告诉系统用WorkerRoutine函数处理接收到的数据:
WSARecv( m_socket, @FBuf, 1, dwTemp, dwFlag, @m_overlap, WorkerRoutine );
  然后......没有什么然后了,系统什么都给你做了!微软真实体贴!
while ( not Terminated ) do//这就是一个Recv/Send线程要做的事情......什么都不用做啊!!!
begin
 if SleepEx( RECV_TIME_OUT, True ) = WAIT_IO_COMPLETION then //
 begin
  ;
 end else
 begin
  continue;
 end;
end;
  
六:IOCP模型
  微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏......
  微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!
  “Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的[没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢?”-----摘自nonocast的《理解I/O Completion Port》
  先看一下IOCP模型的实现:
//创建一个完成端口
FCompletPort := CreateIoCompletionPort( INVALID_HANDLE_VALUE, 0,0,0 );
//接受远程连接,并把这个连接的socket句柄绑定到刚才创建的IOCP上
AConnect := accept( FListenSock, addr, len);
CreateIoCompletionPort( AConnect, FCompletPort, nil, 0 );
//创建CPU数*2 + 2个线程
for i:=1 to si.dwNumberOfProcessors*2+2 do
begin
 AThread := TRecvSendThread.Create( false );
 AThread.CompletPort := FCompletPort;//告诉这个线程,你要去这个IOCP去访问数据
end;
  就这么简单,我们要做的就是建立一个IOCP,把远程连接的socket句柄绑定到刚才创建的IOCP上,最后创建n个线程,并告诉这n个线程到这个IOCP上去访问数据就可以了。
  再看一下TRecvSendThread线程都干些什么:
procedure TRecvSendThread.Execute;
var
 ......
begin
 while (not self.Terminated) do
 begin
  //查询IOCP状态(数据读写操作是否完成)
  GetQueuedCompletionStatus( CompletPort, BytesTransd, CompletKey, POVERLAPPED(pPerIoDat), TIME_OUT );
  if BytesTransd <> 0 then
   ....;//数据读写操作完成
  
   //再投递一个读数据请求
   WSARecv( CompletKey, @(pPerIoDat^.BufData), 1, BytesRecv, Flags, @(pPerIoDat^.Overlap), nil );
  end;
end;
  
读写线程只是简单地检查IOCP是否完成了我们投递的读写操作,如果完成了则再投递一个新的读写请求。
  应该注意到,我们创建的所有TRecvSendThread都在访问同一个IOCP(因为我们只创建了一个IOCP),并且我们没有使用临界区!难道不会产生冲突吗?不用考虑同步问题吗?
  这正是IOCP的奥妙所在。IOCP不是一个普通的对象,不需要考虑线程安全问题。它会自动调配访问它的线程:如果某个socket上有一个线程A正在访问,那么线程B的访问请求会被分配到另外一个socket。这一切都是由系统自动调配的,我们无需过问。

注:iocp都要使用线程池,其中的线程数目一般为当前电脑中cpu个数的2倍。
guxinyi
2012-04-23, 10:17:10
下面这段也是来自网络,解释得很好:

IOCP这个名词我已经听说好久了,当我第一次接触windows网络编程书时,就看到了书上对IOCP的描述。
当时记忆深处就记得windows有5种网络模型,select最基本,也最通用。但是通用就意味着效率低,因为要通用就必须兼容所有的其他操作系统,各种环境等等,所以效率是相当低的。这个也是网络编程的一个标准模式。然后其他就有异步Select,Event选择,重叠IO,这几个模式都有几个弊端,那就是处理的套接字有限,而且上限还是64个,但是其优点就是配合windows应用程序的消息驱动模式非常的方便。当初自己还用异步选择模式写了一个网络版游戏。但没有测试性能如何,不过反正高不了哪里去。
今天我再次重新拿起IOCP来学习,谈谈对IOCP的认识。
首先IOCP从字面解释就是 端口上输入输入完成。端口其实就是port,也可以说是计算机网络上开放的端口的port,也可以说是一个套接字,其本质就是一条联通了的TCP网络连接。网络编程无非就是在这条联通了的网络链路上进行读写数据,就像我们读写文件一样。不过网络读写数据是读取的远程数据,由于是远程,太远了,我们不可以***,所以我们建立了一条管道,这个管道就相当于是TCP连接。因为这个管道太长,或者这个管道太弯曲,造成了网络读写操作是一件令人头痛的事情。
普通的阻塞情况下,如果你要接收数据,那么你就要一直在这个管道的端口等待数据,因为你不知道什么时候数据会到来。如果一直不到来,你就需要一直等待。如果你要发送数据也一样,你需要一点一点的发送数据,直到发送完毕,你才可以腾出手来去做其他事情。在这种模式下,任何事情你都需要亲力亲为。但是你作为一个天才级的人物,把你派到这里来等待管道送来的数据是不是大材小用了呢?因为以你的聪明才智,你完全就是去科学院研究技术的人才。就像一个军事指挥家,他的职责是指挥军事,洞察敌情,如果你把派上前线去打仗当小卒,那就只有死得硬翘翘的,浪费了你的存在价值。这个时候,windows就在为你设身处地的思考问题,于是乎给大家提供了IOCP网络操作模型。
说了半天,这个是个什么模型呢?
“喂,小蜜AAA吗?你去把端口1593上的数据给我接收了”
“喂,小蜜XXX吗?你去把端口2568上的数据给我接收了”
“喂,小蜜YYY吗?我这里有一份重要数据要送到端口8888上,你去给我送了”
呵呵,够牛X吧,小蜜都是N个。此时,你是主脑级的人物,你只需要将自己的精力专注到公司的逻辑处理上去,那些跑腿的事情,你就叫小蜜去吧。
30分钟、50分钟后,小蜜AAA和YYY回来了。然后报告你说:端口数据已经收到了,你要怎么处理?;8888上的数据我已经发送了一半,另外一半因为前面立交桥断裂,发送失败了,怎么办?
这个时候,你已经吃了早饭,开了3个会,并且回家还洗了一个澡回到公司跟小蜜UU喝杯一个早茶。端口数据收发并没有影响你手上的其他事情完成。这个就是IOCP模式,IOCP就是你的无数个小蜜。
最后,吃中午饭的时候,你的小蜜XXX终于回来报告了,说:吗的,等了半天,终于把2568上的数据收到了,老大,怎么处理啊?
你只需要说一句:那个AAA,XXX把收到的数据给我扔到工作线程处理部门去,他们会处理那些数据,你们别管了,给老子继续去各自端口看看还有没有数据到来。那个YYY你去再发送一次看看呢,是不是真的道路损毁断线了?
OK,现在你可以继续去处理其他事情了,等小蜜做完事情后自然会来通报你如何处理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: