socket編程之select(),poll(),epoll()

socket編程,通信

    client端  socket() ----->connect() ------->recv() -----> close();

    server端 socket() ----->bind()  ------> listen() ---->accept() ------>send() ------->close();

1. socket(int family,int type,int protocol);

family ->協議類型 

AF_INET    -------------->  IPV4

AF_INET6  ---------------> IPV6

AF_ROUTE ---------------> 路由套接口

type->是指套接字類型

SOCK_STREAM  -------------------->  字節流套接字   TCP

SOCK_DGRAM   -------------------->  數據報套接字  UDP

SOCK_RAW       -------------------->  原始套接字

函數調用成功返回小的非負整數,文件描述符類型,否則返回-1表示調用失敗

2. connect(int sockfd,struct sockaddr*addr,socklen_t addrlen)

sockfd 是從socket的返回值;

addr是指向服務器的套接字的地址結構的指針

addrlen是該套接字的地址結構的大小

struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htonl(1234);
server.sin_addr.s_addr = inte_addr("127.0.0.1");
if(connect(sockfd,(struct sockaddr *)&server,sizeof(server)) == -1)
{
    //強制性地址轉換,轉爲通用套接字結構
}

3. bind(int sockfd,struct sockaddr *server,socklen_len addrlen);

sockfd->socket的返回值

server->表示指向於特定協議的地址結構的指針

addrlen->表示該套接字的地址結構的長度

struct sockaddr_in server;
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htonl(1234);
server.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd,(struct sockaddr *)&server,sizeof(server)) == -1)
{
    ///sockaddr 強制轉換
}

4. listen(int sockfd,int backlog)

sockfd --->socket的返回值

backlog --->規定了請求返回隊列的最大連接數,它對隊列中等待服務請求的數目進行限制,如果一個服務請求到來時,輸入隊列已滿,該套接字將拒絕連接請求。

listen(sockfd,5);

對於一個監聽套接字,內核要維護兩個隊列:未完成連接隊列和已完成連接隊列

未完成連接隊列:爲每個請求建立連接的SYN分節設一個條目,服務器正等待完成三次握手,當前的套接字處在SYN_RECD狀態

已完成連接隊列:爲每個完成TCP三次握手的客戶端開設一個條目,當前的套接字狀態時ESTABLISHED。

5. accept(int sockfd,struct sockaddr *client,socklen_t *addrlen)

 sockfd 是socket()返回的套接字描述符,在調用listen的時候此套接字變成了監聽套接字

client 是返回對方的套接字和地址結構

addrlen 是對應結構的長度

accetp()函數返回,已連接套接字描述符。

  注:一個服務器只能有一個監聽套接字,而且會一直存在,直到服務器關閉,而已連接套接字描述符是內核爲每個被接受的客戶都創建一個,當服務器完成與客戶的數據傳輸時,要關閉連接套接字,所以監聽套接字接受客戶的鏈接請求,已連接套接字描述符負責對應客戶進行數據傳送。
 

int listenfd,connfd;
struct sockaddr_in client;
socklen_t addrlen;
addrlen = sizeof(client);
connfd = accept(listenfd,(struct sockaddr *)&client,&addrlen);
if(connfd == -1)
{
    
}

 

關於select、poll和epoll說明:

(一)select函數

1 int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, const struct timeval *timeout);
各個參數含義如下:

  • int maxfdp:最大描述符值 + 1
  • fd_set *readfds:對可讀感興趣的描述符集
  • fd_set *writefds:對可寫感興趣的描述符集
  • fd_set *errorfds:對出錯感興趣的描述符集
  • struct timeval *timeout:超時時間(注意:對於linux系統,此參數沒有const限制,每次select調用完畢timeout的值都被修改爲剩餘時間,而unix系統則不會改變timeout值)

select函數會在發生以下情況時返回:

  1. readfds集合中有描述符可讀
  2. writefds集合中有描述符可寫
  3. errorfds集合中有描述符遇到錯誤條件
  4. 指定的超時時間timeout到了

當select返回時,描述符集合將被修改以指示哪些個描述符正處於可讀、可寫或有錯誤狀態。可以用FD_ISSET宏對描述符進行測試以找到狀態變化的描述符。如果select因爲超時而返回的話,所有的描述符集合都將被清空。
select函數返回狀態發生變化的描述符總數。返回0意味着超時。失敗則返回-1並設置errno。可能出現的錯誤有:EBADF(無效描述符)、EINTR(因終端而返回)、EINVAL(nfds或timeout取值錯誤)。
設置描述符集合通常用如下幾個宏定義:

FD_ZERO(fd_set *fdset);                /* 將所有位設爲0   */
FD_SET(int fd, fd_set *fdset);         /* 將fd位設爲1     */
FD_CLR(int fd, fd_set *fdset);         /* 將fd位設爲0     */
int FD_ISSET(int fd, fd_set *fdset);   /* 檢測fd位是否爲1 */

如:

fd_set  set;
FD_ZERO(&rset);                        /* i初始化rset */
FD_SET(1, &rset);                      /* 將fd = 1 的描述符設爲1  */
FD_SET(4, &rset);                      /* 將fd = 4 的描述符設爲1   */
FD_SET(5, &rset);                      /* 將fd = 5 的描述符設爲1  */

當select返回的時候,rset位都將被置0,除了那些有變化的fd位。
當發生如下情況時認爲是可讀的:

  1. socket的receive buffer中的字節數大於socket的receive buffer的low-water mark屬性值。(low-water mark值類似於分水嶺,當receive buffer中的字節數小於low-water mark值的時候,認爲socket還不可讀,只有當receive buffer中的字節數達到一定量的時候才認爲socket可讀)
  2. 連接半關閉(讀關閉,即收到對端發來的FIN包)
  3. 發生變化的描述符是被動套接字,而連接的三路握手完成的數量大於0,即有新的TCP連接建立
  4. 描述符發生錯誤,如果調用read系統調用讀套接字的話會返回-1。

當發生如下情況時認爲是可寫的:

  1. socket的send buffer中的字節數大於socket的send buffer的low-water mark屬性值以及socket已經連接或者不需要連接(如UDP)。
  2. 寫半連接關閉,調用write函數將產生SIGPIPE
  3. 描述符發生錯誤,如果調用write系統調用寫套接字的話會返回-1。

注意:
select默認能處理的描述符數量是有上限的,爲FD_SETSIZE的大小。
對於timeout參數,如果置爲NULL,則表示wait forever;若timeout->tv_sec = timeout->tv_usec = 0,則表示do not wait at all;否則指定等待時間。
如果使用select處理多個套接字,那麼需要使用一個數組(也可以是其他結構)來記錄各個描述符的狀態。而使用poll則不需要,下面看poll函數。

(二)poll()函數
 

原型如下:

1 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
各參數含義如下:

  • struct pollfd *fdarray:一個結構體,用來保存各個描述符的相關狀態。
  • unsigned long nfds:fdarray數組的大小,即裏面包含有效成員的數量。
  • int timeout:設定的超時時間。(以毫秒爲單位)

poll函數返回值及含義如下:

  • -1:有錯誤產生
  • 0:超時時間到,而且沒有描述符有狀態變化
  • >0:有狀態變化的描述符個數

着重講fdarray數組,因爲這是它和select()函數主要的不同的地方:
pollfd的結構如下:

struct pollfd {
      int fd;                  /* 測試描述符*/
      short events;      /* 測試條件*/
      short revents;     /* 測試結果 */
};

其實poll()和select()函數要處理的問題是相同的,只不過是不同組織在幾乎相同時刻同時推出的,因此才同時保留了下來。select()函數把可讀描述符、可寫描述符、錯誤描述符分在了三個集合裏,這三個集合都是用bit位來標記一個描述符,一旦有若干個描述符狀態發生變化,那麼它將被置位,而其他沒有發生變化的描述符的bit位將被clear,也就是說select()的readset、writeset、errorset是一個value-result類型,通過它們傳值,而也通過它們返回結果。這樣的一個壞處是每次重新select 的時候對集合必須重新賦值。而poll()函數則與select()採用的方式不同,它通過一個結構數組保存各個描述符的狀態,每個結構體第一項fd代表描述符,第二項代表要監聽的事件,也就是感興趣的事件,而第三項代表poll()返回時描述符的返回狀態。合法狀態如下:

  • POLLIN:                有普通數據或者優先數據可讀
  • POLLRDNORM:    有普通數據可讀
  • POLLRDBAND:    有優先數據可讀
  • POLLPRI:              有緊急數據可讀
  • POLLOUT:            有普通數據可寫
  • POLLWRNORM:   有普通數據可寫
  • POLLWRBAND:    有緊急數據可寫
  • POLLERR:            有錯誤發生
  • POLLHUP:            有描述符掛起事件發生
  • POLLNVAL:          描述符非法

對於POLLIN | POLLPRI等價與select()的可讀事件;POLLOUT | POLLWRBAND等價與select()的可寫事件;POLLIN 等價與POLLRDNORM | POLLRDBAND,而POLLOUT等價於POLLWRBAND。如果你對一個描述符的可讀事件和可寫事件以及錯誤等事件均感興趣那麼你應該都進行相應的設置。
對於timeout的設置如下:

    INFTIME:wait forever

    0    :不等待,輪訓

    >0    :等待特定的時間。

(三)epoll() 函數

epoll()函數是在LINUX2.6中被提到的。

epoll()函數有三個接口    

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_create()函數中size告訴內核監聽數目有多少。

epoll_ctl()表第一個函數是epoll_create()的返回值,op表示要進行的操作,fd表示需要監聽的fd,event告訴內核需要監聽什麼事

op有三種宏定義:

EPOLL_CTL_ADD://註冊新的fd到epfd中;
EPOLL_CTL_MOD://修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL://從epfd中刪除一個fd;

struct epoll_event *event函數的結構體特徵是

struct epoll_event
{
    __uint32_t events;/* epoll事件 */
    epoll_data_t data;/*用戶數據變量 */
};

其中epoll_event結構體中events事件:

 

events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏

 

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epfd:是epoll_create的返回值

events:是從內核得到的事件集合

maxevents:告訴內核events的最大數目

timeout:表示時間(0表示不等待,-1不確定,有的說法是阻塞)

epoll的工作模式:

epoll對文件描述符的操作有兩種操作:LT(水平觸發)&ET(邊緣觸發)

  水平觸發:當epoll_wait檢測到描述符事件發生並將事件通知應用程序,應用程序可以不立刻處理事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。

  邊緣觸發:當epoll_wait檢測到描述符事件發生並將時間通知應用程序,應用程序必須立刻處理事件。如果不處理下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

  邊緣觸發事件在很大程度上減少了epoll事件被重複觸發的次數,因此效率被水平觸發高。epoll在邊緣觸發的時候,必須使用非阻塞接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把多個文件描述符的任務餓死。

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