[轉]很幽默的講解六種Socket IO模型

本文簡單介紹了當前Windows支持的各種Socket I/O模型,如果你發現其中存在什麼錯誤請務必賜教。

 

一:select模型
二:WSAAsyncSelect模型
三:WSAEventSelect模型
四:Overlapped I/O 事件通知模型
五:Overlapped I/O 完成例程模型
六: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.dwNumberOfProcessors2+2 do
begin
AThread := TRecvSendThread.Create( false );
AThread.CompletPort := FCompletPort;//告訴這個線程,你要去這個IOCP去訪問數據
end;

OK,就這麼簡單,我們要做的就是建立一個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。這一切都是由系統自動調配的,我們無需過問。
深入瞭解epoll
一、 介紹
Epoll是一種高效的管理socket的模型,相對於select和poll來說具有更高的效率和易用性。傳統的select以及poll的效率會因爲 socket數量的線形遞增而導致呈二次乃至三次方的下降,而epoll的性能不會隨socket數量增加而下降。標準的linux-2.4.20內核不支持epoll,需要打patch。本文主要從linux-2.4.32和linux-2.6.10兩個內核版本介紹epoll。
二、 Epoll的使用
epoll用到的所有函數都是在頭文件sys/epoll.h中聲明的,下面簡要說明所用到的數據結構和函數:
所用到的數據結構
typedef union epoll_data {
                  void ptr;
                  int fd;
                  __uint32_t u32;
                  __uint64_t u64;
          } epoll_data_t;

          struct epoll_event {
                  __uint32_t events;       / Epoll events /
                  epoll_data_t data;       / User data variable /
          };
結構體epoll_event 被用於註冊所感興趣的事件和回傳所發生待處理的事件,其中epoll_data 聯合體用來保存觸發事件的某個文件描述符相關的數據,例如一個client連接到服務器,服務器通過調用accept函數可以得到於這個client對應的socket文件描述符,可以把這文件描述符賦給epoll_data的fd字段以便後面的讀寫操作在這個文件描述符上進行。epoll_event 結構體的events字段是表示感興趣的事件和被觸發的事件可能的取值爲:EPOLLIN :表示對應的文件描述符可以讀;
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET:表示對應的文件描述符設定爲edge模式;
所用到的函數:
1、epoll_create函數
      函數聲明:int epoll_create(int size)
      該函數生成一個epoll專用的文件描述符,其中的參數是指定生成描述符的最大範圍。在linux-2.4.32內核中根據size大小初始化哈希表的大小,在linux2.6.10內核中該參數無用,使用紅黑樹管理所有的文件描述符,而不是hash。
2、epoll_ctl函數
      函數聲明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
      該函數用於控制某個文件描述符上的事件,可以註冊事件,修改事件,刪除事件。
      參數:epfd:由 epoll_create 生成的epoll專用的文件描述符;
                  op:要進行的操作例如註冊事件,可能的取值
EPOLL_CTL_ADD 註冊、
EPOLL_CTL_MOD 修改、
EPOLL_CTL_DEL 刪除
    fd:關聯的文件描述符;
    event:指向epoll_event的指針;
    如果調用成功返回0,不成功返回-1
3、epoll_wait函數
函數聲明:int epoll_wait(int epfd,struct epoll_event   events,int maxevents,int timeout)
該函數用於輪詢I/O事件的發生;
參數:
epfd:由epoll_create 生成的epoll專用的文件描述符;
epoll_event:用於回傳代處理事件的數組;
maxevents:每次能處理的事件數;
timeout:等待I/O事件發生的超時值(ms);-1永不超時,直到有事件產生才觸發,0立即返回。
返回發生事件數。-1有錯誤。

舉一個簡單的例子:

C/C++ codeint main()
{
      //聲明epoll_event結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件
      struct epoll_event ev,events[20];

      epfd=epoll_create(10000);     //創建epoll句柄
     
      listenfd = socket(AF_INET, SOCK_STREAM, 0);
      //把socket設置爲非阻塞方式
      setnonblocking(listenfd);
     
      bzero(&serveraddr, sizeof(serveraddr));
      serveraddr.sin_family = AF_INET;
      serveraddr.sin_addr.s_addr = INADDR_ANY;
      serveraddr.sin_port=htons(SERV_PORT);
      bind(listenfd,(struct sockaddr )&serveraddr, sizeof(serveraddr));
      listen(listenfd, 255);

      //設置與要處理的事件相關的文件描述符
      ev.data.fd=listenfd;
      //設置要處理的事件類型
      ev.events=EPOLLIN;
      //註冊epoll事件
      epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

      for ( ; ; )
      {
          //等待epoll事件的發生
          nfds=epoll_wait(epfd,events,20,1000);
          //處理所發生的所有事件
          for(i=0;i<nfds;++i)
          {
              if(events[i].data.fd==listenfd)
              {
                  connfd = accept(listenfd,(struct sockaddr )&clientaddr, &clilen);
                  if(connfd<0)
                  {
                      perror("connfd<0");
                  }
                  setnonblocking(connfd);
                  //設置用於讀操作的文件描述符
                  ev.data.fd=connfd;
                  //設置用於注測的讀操作事件
                  ev.events=EPOLLIN|EPOLLET;
                  //註冊event
                  epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
              }
              else if(events[i].events&EPOLLIN)
              {
                  read_socket(events[i].data.fd);
                  ev.data.fd=events[i].data.fd;
                  ev.events=EPOLLIN|EPOLLOUT|EPOLLET;
                  epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
              }
              else if(events[i].events&EPOLLOUT)
              {
                  write_socket(events[i].data.fd);
                  ev.data.fd=events[i].data.fd;
                  ev.events=EPOLLIN|EPOLLET;     //ET模式
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
             }
              else
              {
                  perror("other event");
              }
          }
      }
}


Epoll的ET模式與LT模式
ET(Edge Triggered)與LT(Level Triggered)的主要區別可以從下面的例子看出
eg:
1. 標示管道讀者的文件句柄註冊到epoll中;
2. 管道寫者向管道中寫入2KB的數據;
3. 調用epoll_wait可以獲得管道讀者爲已就緒的文件句柄;
4. 管道讀者讀取1KB的數據
5. 一次epoll_wait調用完成
如果是ET模式,管道中剩餘的1KB被掛起,再次調用epoll_wait,得不到管道讀者的文件句柄,除非有新的數據寫入管道。如果是LT模式,只要管道中有數據可讀,每次調用epoll_wait都會觸發。

另一點區別就是設爲ET模式的文件句柄必須是非阻塞的。
三、 Epoll的實現
Epoll的源文件在/usr/src/linux/fs/eventpoll.c,在module_init時註冊一個文件系統 eventpoll_fs_type,對該文件系統提供兩種操作poll和release,所以epoll_create返回的文件句柄可以被poll、 select或者被其它epoll epoll_wait。對epoll的操作主要通過三個系統調用實現:
1. sys_epoll_create
2. sys_epoll_ctl
3. sys_epoll_wait
下面結合源碼講述這三個系統調用。
1.1 long sys_epoll_create (int size)
該系統調用主要分配文件句柄、inode以及file結構。在linux-2.4.32內核中,使用hash保存所有註冊到該epoll的文件句柄,在該系統調用中根據size大小分配hash的大小。具體爲不小於size,但小於2size的2的某次方。最小爲2的9次方(512),最大爲2的17次方(128 x 1024)。在linux-2.6.10內核中,使用紅黑樹保存所有註冊到該epoll的文件句柄,size參數未使用。
1.2 long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
1. 註冊句柄 op = EPOLL_CTL_ADD
註冊過程主要包括:
A.將fd插入到hash(或rbtree)中,如果原來已經存在返回-EEXIST,
B.給fd註冊一個回調函數,該函數會在fd有事件時調用,在該函數中將fd加入到epoll的就緒隊列中。
C.檢查fd當前是否已經有期望的事件產生。如果有,將其加入到epoll的就緒隊列中,喚醒epoll_wait。

2. 修改事件 op = EPOLL_CTL_MOD
修改事件只是將新的事件替換舊的事件,然後檢查fd是否有期望的事件。如果有,將其加入到epoll的就緒隊列中,喚醒epoll_wait。

3. 刪除句柄 op = EPOLL_CTL_DEL
將fd從hash(rbtree)中清除。
1.3 long sys_epoll_wait(int epfd, struct epoll_event events, int maxevents,int timeout)
如果epoll的就緒隊列爲空,並且timeout非0,掛起當前進程,引起CPU調度。
如果epoll的就緒隊列不空,遍歷就緒隊列。對隊列中的每一個節點,獲取該文件已觸發的事件,判斷其中是否有我們期待的事件,如果有,將其對應的epoll_event結構copy到用戶events。

revents = epi->file->f_op->poll(epi->file, NULL);
epi->revents = revents & epi->event.events;
if (epi->revents) {
……
copy_to_user;
……
}
需要注意的是,在LT模式下,把符合條件的事件copy到用戶空間後,還會把對應的文件重新掛接到就緒隊列。所以在LT模式下,如果一次epoll_wait某個socket沒有read/write完所有數據,下次epoll_wait還會返回該socket句柄。
四、 使用epoll的注意事項
1. ET模式比LT模式高效,但比較難控制。
2. 如果某個句柄期待的事件不變,不需要EPOLL_CTL_MOD,但每次讀寫後將該句柄modify一次有助於提高穩定性,特別在ET模式。
3. socket關閉後最好將該句柄從epoll中delete(EPOLL_CTL_DEL),雖然epoll自身有處理,但會使epoll的hash的節點數增多,影響搜索hash的速度。
  
  
修改 刪除 舉報 引用 回覆   
  
   加爲好友
發送私信
在線聊天
   aSpace
一個空格
等級:
   發表於:2008-07-02 20:52:123樓 得分:0
Q:網絡服務器的瓶頸在哪?
A:IO效率。

在大家苦苦的爲在線人數的增長而導致的系統資源吃緊上的問題正在發愁的時候,Linux 2.6內核中提供的System Epoll爲我們提供了一套完美的解決方案。傳統的select以及poll的效率會因爲在線人數的線形遞增而導致呈二次乃至三次方的下降,這些直接導致了網絡服務器可以支持的人數有了個比較明顯的限制。

自從Linux提供了/dev/epoll的設備以及後來2.6內核中對/dev/epoll設備的訪問的封裝(System Epoll)之後,這種現象得到了大大的緩解,如果說幾個月前,大家還對epoll不熟悉,那麼現在來說的話,epoll的應用已經得到了大範圍的普及。

那麼究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭文件#include 以及幾個簡單的API將可以大大的提高你的網絡服務器的支持人數。

首先通過create_epoll(int maxfds)來創建一個epoll的句柄,其中maxfds爲你epoll所支持的最大句柄數。這個函數會返回一個新的epoll句柄,之後的所有操作將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。

之後在你的網絡主循環裏面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法爲:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd爲用epoll_create創建之後的句柄,events是一個epoll_event的指針,當epoll_wait這個函數操作成功之後,epoll_events裏面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。最後一個timeout是 epoll_wait的超時,爲0的時候表示馬上返回,爲-1的時候表示一直等下去,直到有事件範圍,爲任意正整數的時候表示等這麼長的時間,如果一直沒有事件,則範圍。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。

epoll_wait範圍之後應該是一個循環,遍利所有的事件:

C/C++ codefor(n = 0; n < nfds; ++n) {
                 if(events[n].data.fd == listener) { //如果是主socket的事件的話,則表示有新連接進入了,進行新連接的處理。
                     client = accept(listener, (struct sockaddr ) &local,
                                     &addrlen);
                     if(client < 0){
                         perror("accept");
                         continue;
                     }
                     setnonblocking(client); // 將新連接置於非阻塞模式
                     ev.events = EPOLLIN | EPOLLET; // 並且將新連接也加入EPOLL的監聽隊列。
注意,這裏的參數EPOLLIN | EPOLLET並沒有設置對寫socket的監聽,如果有寫操作的話,這個時候epoll是不會返回事件的,如果要對寫操作也監聽的話,應該是EPOLLIN | EPOLLOUT | EPOLLET
                     ev.data.fd = client;
                     if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
//   設置好event之後,將這個新的event通過epoll_ctl加入到epoll的監聽隊列裏面,這裏用EPOLL_CTL_ADD來加一個新的 epoll事件,通過EPOLL_CTL_DEL來減少一個epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。
                         fprintf(stderr, "epoll set insertion error: fd=d0,
                                 client);
                         return -1;
                     }
                 }
                 else // 如果不是主socket的事件的話,則代表是一個用戶socket的事件,則來處理這個用戶socket的事情,比如說read(fd,xxx)之類的,或者一些其他的處理。
                     do_use_fd(events[n].data.fd);
}

 

對,epoll的操作就這麼簡單,總共不過4個API:epoll_create, epoll_ctl, epoll_wait和close

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