網絡通訊模型

老陳有一個在外地工作的女兒,不能經常回來,老陳和她通過信件聯繫。他們的信會被郵遞員投遞到他們的信箱裏。
  這和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。這一切都是由系統自動調配的,我們無需過問。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章