网络通讯模型

老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
  这和Socket模型非常类似。下面就以老陈接收信件为例讲解Socket IO模型。

一:select模型
  老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信,在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
  select模型和老陈的这种情况非常相似:周而复始地去检查......如果有数据......接收发送.......
  使用线程来select应该是通用的做法:

   1.
   2.
   3. procedure TListenThread.Execute;
   4. var
   5.  addr  TSockAddrIn;
   6.  fd_read  TFDSet;
   7.  timeout  TTimeVal;
   8.  ASock,
   9.  MainSock  TSocket;
  10.  len, i  Integer;
  11. begin
  12.  MainSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
  13.  addr.sin_family = AF_INET;
  14.  addr.sin_port = htons(5678);
  15.  addr.sin_addr.S_addr = htonl(INADDR_ANY);
  16.  bind( MainSock, @addr, sizeof(addr) );
  17.  listen( MainSock, 5 );
  18.  while (not Terminated) do
  19.  begin
  20.   FD_ZERO( fd_read );
  21.   FD_SET( MainSock, fd_read );
  22.   timeout.tv_sec = 0;
  23.   timeout.tv_usec = 500;
  24.   if select( 0, @fd_read, nil, nil, @timeout )  0 then 至少有1个等待Accept的connection
  25.   begin
  26.    if FD_ISSET( MainSock, fd_read ) then
  27.    begin
  28.    for i=0 to fd_read.fd_count-1 do 注意,fd_count = 64,也就是说select只能同时管理最多64个连接
  29.    begin
  30.     len = sizeof(addr);
  31.     ASock = accept( MainSock, addr, len );
  32.     if ASock  INVALID_SOCKET then
  33.      ....为ASock创建一个新的线程,在新的线程中再不停地select
  34.     end;
  35.    end;   
  36.   end;
  37.  end; while (not self.Terminated)
  38.  shutdown( MainSock, SD_BOTH );
  39.  closesocket( MainSock );
  40. end;


二:WSAAsyncSelect模型
  后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天......不是,微软......
  微软提供的WSAAsyncSelect模型就是这个意思。
  WSAAsyncSelect模型是Windows下最简单易用的一种Socket IO模型。使用这种模型时,Windows会把网络事件以消息的形势通知应用程序。
  首先定义一个消息标示常量:

   1.
   2.
   3. const WM_SOCKET = WM_USER + 55;
   4.   再在主Form的private域添加一个处理此消息的函数声明:
   5. private
   6. procedure WMSocket(var Msg TMessage); message WM_SOCKET;
   7.   然后就可以使用WSAAsyncSelect了:
   8. var
   9.  addr  TSockAddr;
  10.  sock  TSocket;
  11.  sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
  12.  addr.sin_family = AF_INET;
  13.  addr.sin_port = htons(5678);
  14.  addr.sin_addr.S_addr = htonl(INADDR_ANY);
  15.  bind( m_sock, @addr, sizeof(SOCKADDR) );
  16.  WSAAsyncSelect( m_sock, Handle, WM_SOCKET, FD_ACCEPT or FD_CLOSE );
  17.  listen( m_sock, 5 );
  18.  ....
  19.   应用程序可以对收到WM_SOCKET消息进行分析,判断是哪一个socket产生了网络事件以及事件类型:
  20. procedure TfmMain.WMSocket(var Msg TMessage);
  21. var
  22.  sock  TSocket;
  23.  addr  TSockAddrIn;
  24.  addrlen  Integer;
  25.  buf  Array [0..4095] of Char;
  26. begin
  27.  Msg的WParam是产生了网络事件的socket句柄,LParam则包含了事件类型
  28.  case WSAGetSelectEvent( Msg.LParam ) of
  29.  FD_ACCEPT
  30.   begin
  31.    addrlen = sizeof(addr);
  32.    sock = accept( Msg.WParam, addr, addrlen );
  33.    if sock  INVALID_SOCKET then
  34.     WSAAsyncSelect( sock, Handle, WM_SOCKET, FD_READ or FD_WRITE or FD_CLOSE );
  35.   end;
  36.   FD_CLOSE  closesocket( Msg.WParam );
  37.   FD_READ  recv( Msg.WParam, buf[0], 4096, 0 );
  38.   FD_WRITE  ;
  39.  end;
  40. end;


三:WSAEventSelect模型
   后来,微软的信箱非常畅销,购买微软信箱的人以百万计数......以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使。微软改进了 他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以 睡觉了。
  同样要使用线程:

   1.
   2.
   3. procedure TListenThread.Execute;
   4. var
   5.  hEvent  WSAEvent;
   6.  ret  Integer;
   7.  ne  TWSANetworkEvents;
   8.  sock  TSocket;
   9.  adr  TSockAddrIn;
  10.  sMsg  String;
  11.  Index,
  12.  EventTotal  DWORD;
  13.  EventArray  Array [0..WSA_MAXIMUM_WAIT_EVENTS-1] of WSAEVENT;
  14. begin
  15.  ...socket...bind...
  16.  hEvent = WSACreateEvent();
  17.  WSAEventSelect( ListenSock, hEvent, FD_ACCEPT or FD_CLOSE );
  18.  ...listen...
  19.  while ( not Terminated ) do
  20.  begin
  21.   Index = WSAWaitForMultipleEvents( EventTotal, @EventArray[0], FALSE, WSA_INFINITE, FALSE );
  22.   FillChar( ne, sizeof(ne), 0 );
  23.   WSAEnumNetworkEvents( SockArray[Index-WSA_WAIT_EVENT_0], EventArray[Index-WSA_WAIT_EVENT_0], @ne );
  24.   if ( ne.lNetworkEvents and FD_ACCEPT )  0 then
  25.   begin
  26.    if ne.iErrorCode[FD_ACCEPT_BIT]  0 then
  27.     continue;
  28.    ret = sizeof(adr);
  29.    sock = accept( SockArray[Index-WSA_WAIT_EVENT_0], adr, ret );
  30.    if EventTotal  WSA_MAXIMUM_WAIT_EVENTS-1 then这里WSA_MAXIMUM_WAIT_EVENTS同样是64
  31.    begin
  32.     closesocket( sock );
  33.     continue;
  34.    end;
  35.    hEvent = WSACreateEvent();
  36.    WSAEventSelect( sock, hEvent, FD_READ or FD_WRITE or FD_CLOSE );
  37.    SockArray[EventTotal] = sock;
  38.    EventArray[EventTotal] = hEvent;
  39.    Inc( EventTotal );
  40.   end;
  41.   if ( ne.lNetworkEvents and FD_READ )  0 then
  42.   begin
  43.    if ne.iErrorCode[FD_READ_BIT]  0 then
  44.     continue;
  45.     FillChar( RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
  46.     ret = recv( SockArray[Index-WSA_WAIT_EVENT_0], RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
  47.     ......
  48.    end;
  49.   end;
  50. end;
  51.   


四:Overlapped IO 事件通知模型
   后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用 户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信 件了!
  Overlapped IO 事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在“Overlapped”,Overlapped模型是让应用程序使用重叠 数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock IO请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而 你需要做的只是为系统提供一个缓冲区~~~~~
Listen线程和WSAEventSelect模型一模一样,RecvSend线程则完全不同:

   1.
   2.
   3. procedure TOverlapThread.Execute;
   4. var
   5.  dwTemp  DWORD;
   6.  ret  Integer;
   7.  Index  DWORD;
   8. begin
   9.  ......
  10.  while ( not Terminated ) do
  11.  begin
  12.   Index = WSAWaitForMultipleEvents( FLinks.Count, @FLinks.Events[0], FALSE, RECV_TIME_OUT, FALSE );
  13.   Dec( Index, WSA_WAIT_EVENT_0 );
  14.   if Index  WSA_MAXIMUM_WAIT_EVENTS-1 then 超时或者其他错误
  15.    continue;
  16.   WSAResetEvent( FLinks.Events[Index] );
  17.   WSAGetOverlappedResult( FLinks.Sockets[Index], FLinks.pOverlaps[Index], @dwTemp, FALSE,FLinks.pdwFlags[Index]^ );
  18.   if dwTemp = 0 then 连接已经关闭
  19.   begin
  20.    ......
  21.    continue;
  22.   end else
  23.  begin
  24.   fmMain.ListBox1.Items.Add( FLinks.pBufs[Index]^.buf );
  25.  end;
  26.  初始化缓冲区
  27.  FLinks.pdwFlags[Index]^ = 0;
  28.  FillChar( FLinks.pOverlaps[Index]^, sizeof(WSAOVERLAPPED), 0 );
  29.  FLinks.pOverlaps[Index]^.hEvent = FLinks.Events[Index];
  30.  FillChar( FLinks.pBufs[Index]^.buf^, BUFFER_SIZE, 0 );
  31.  递一个接收数据请求
  32.   WSARecv( FLinks.Sockets[Index], FLinks.pBufs[Index], 1, FLinks.pdwRecvd[Index]^, FLinks.pdwFlags[Index]^, FLinks.pOverlaps[Index], nil );
  33. end;
  34. end;


五:Overlapped IO 完成例程模型
   老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件......为了进一步减轻用户负担,微软又开发了一 种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信阅读回复了!老陈终于过上了小资生活!
  Overlapped IO 完成例程要求用户提供一个回调函数,发生新的网络事件的时候系统将执行这个函数:

   1.
   2.
   3. procedure WorkerRoutine( const dwError, cbTransferred  DWORD;
   4. const
   5. lpOverlapped  LPWSAOVERLAPPED; const dwFlags  DWORD ); stdcall;
   6.   然后告诉系统用WorkerRoutine函数处理接收到的数据:
   7. WSARecv( m_socket, @FBuf, 1, dwTemp, dwFlag, @m_overlap, WorkerRoutine );
   8.   然后......没有什么然后了,系统什么都给你做了!微软真实体贴!
   9. while ( not Terminated ) do这就是一个RecvSend线程要做的事情......什么都不用做啊!!!
  10. begin
  11.  if SleepEx( RECV_TIME_OUT, True ) = WAIT_IO_COMPLETION then
  12.  begin
  13.   ;
  14.  end else
  15.  begin
  16.   continue;
  17.  end;
  18. end;
  19.   


六:IOCP模型
  微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏......
  微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!
   “Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的 [没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文 [Context],线程就没有得到很多CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线 程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递 到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程 的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢”-----摘自nonocast的《理解IO Completion Port》
  先看一下IOCP模型的实现:

   1.
   2.
   3. 创建一个完成端口
   4. FCompletPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, 0,0,0 );
   5. 接受远程连接,并把这个连接的socket句柄绑定到刚才创建的IOCP上
   6. AConnect = accept( FListenSock, addr, len);
   7. CreateIoCompletionPort( AConnect, FCompletPort, nil, 0 );
   8. 创建CPU数2 + 2个线程
   9. for i=1 to si.dwNumberOfProcessors2+2 do
  10. begin
  11.  AThread = TRecvSendThread.Create( false );
  12.  AThread.CompletPort = FCompletPort;告诉这个线程,你要去这个IOCP去访问数据
  13. end;
  14.   就这么简单,我们要做的就是建立一个IOCP,把远程连接的socket句柄绑定到刚才创建的IOCP上,最后创建n个线程,并告诉这n个线程到这个IOCP上去访问数据就可以了。
  15.   再看一下TRecvSendThread线程都干些什么:
  16. procedure TRecvSendThread.Execute;
  17. var
  18.  ......
  19. begin
  20.  while (not self.Terminated) do
  21.  begin
  22.   查询IOCP状态(数据读写操作是否完成)
  23.   GetQueuedCompletionStatus( CompletPort, BytesTransd, CompletKey, POVERLAPPED(pPerIoDat), TIME_OUT );
  24.   if BytesTransd  0 then
  25.    ....;数据读写操作完成
  26.   
  27.    再投递一个读数据请求
  28.    WSARecv( CompletKey, @(pPerIoDat^.BufData), 1, BytesRecv, Flags, @(pPerIoDat^.Overlap), nil );
  29.   end;
  30. end;
  31.   


读写线程只是简单地检查IOCP是否完成了我们投递的读写操作,如果完成了则再投递一个新的读写请求。
  应该注意到,我们创建的所有TRecvSendThread都在访问同一个IOCP(因为我们只创建了一个IOCP),并且我们没有使用临界区!难道不会产生冲突吗?不用考虑同步问题吗?
  这正是IOCP的奥妙所在。IOCP不是一个普通的对象,不需要考虑线程安全问题。它会自动调配访问它的线程:如果某个socket上有一个线程A正在访问,那么线程B的访问请求会被分配到另外一个socket。这一切都是由系统自动调配的,我们无需过问。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值