原生API
select
intselect(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函數參數
numfds:文件描述符的最大值+1(為了限制檢測文件描述符的范圍)
readfds:包含所有因為狀態變為可讀而觸發select函數返回文件描述符
writefds:包含所有因為狀態變為可寫而觸發select函數返回文件描述符
exceptfds:包含所有因為狀態發生特殊異常而觸發select函數返回文件描述符
timeout:表示阻塞超時時限
返回值
當為-1的時候表示出錯
當為0的時候表示超時
當大于0則成功
// 新增fd到set中FD_SET(intfd, fd_set *set);// 從set中移除fdFD_CLR(intfd, fd_set *set);// 判斷fd是否在set中FD_ISSET(intfd, fd_set *set);// 將set整個清0FD_ZERO(fd_set *set);
基本思路,把要檢測的文件描述符加載到?fd_set?類型的集合中,然后調用?select?函數檢測加載到集合中的文件描述符;
select?函數監視的文件描述符分為3類,分別是?writefds, readfds, exceptfds,調用之后select函數就會阻塞,直到有文件描述符就緒(有數據可讀,可寫或者except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數返回;當select函數返回之后,可以通過遍歷?fdset來找到就緒的描述符。
#include#include#include#include#include#includeconstintMAXSIZE =1024;intmain(){intsockfd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//sockfd為服務器的套接字sockaddr_insin;sin.sin_family = AF_INET;sin.sin_port = htons(4567);//1024 ~ 49151:普通用戶注冊的端口號sin.sin_addr.s_addr = INADDR_ANY;? ? sockaddr_in client_addr;// ...bind 和 listen操作socklen_tclen =sizeof(sockaddr_in);structtimeval tv;intfds[MAXSIZE];memset(fds,-1,sizeof(fds));? ? fd_set fdset;? ? fds[0] = sockfd;while(1) {? ? ? ? FD_ZERO(&fdset);inti =0;intfdmax = fds[0];for(; i < MAXSIZE; i++) {if(fds[i] !=-1) {? ? ? ? ? ? ? ? FD_SET(fds[i], &fdset);if(fdmax < fds[i]) {? ? ? ? ? ? ? ? ? ? fdmax = fds[i];? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? ? ? tv.tv_sec =2;? ? ? ? tv.tv_usec =0;intres = select(fdmax +1, &fdset,NULL,NULL, &tv);? ? ? ? assert(res !=-1);if(res ==0) {printf("timeout\n");? ? ? ? }else{inti =0;for(; i < MAXSIZE; i++) {if(fds[i] ==-1) {continue;? ? ? ? ? ? ? ? }if(FD_ISSET(fds[i], &fdset)) {if(fds[i] == sockfd) {intc = accept(sockfd, (structsockaddr *)&client_addr, &clen);if(c >=0) {// 找到一個空的設置成新的套接字for(intk =0; k < MAXSIZE; k++) {if(fds[i] ==0) {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? fds[i] = c;break;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }else{charbuff[256] = {0};intn = read(fds[i], buff,255);if(n >0) {printf("read:%s\n", buff);? ? ? ? ? ? ? ? ? ? ? ? ? ? write(fds[i],"OK",2);? ? ? ? ? ? ? ? ? ? ? ? }elseif(n ==0) {// 刪除套接字fds[i] =0;? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }}
這個代碼中有不完善的地方:使用數組保存套接字,建議以鏈表的形式保存鏈表會更好一些;
優點:跨平臺
缺點:
單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內核的方式提升這個限制,但是這樣也會造成效率的降低;
每次都要調用 select ,都需要把?fd?集合從用戶態拷貝到內核態,在fd很多時開銷會很大;
每次調用 select 都需要在內核遍歷傳遞進來的所有fd,在fd很多時開銷也很大;
注意,每次調用select之前都要對fdset集合進行 FD_ZERO(&fdset) 操作,即清空。
參考文章
poll
intpoll(struct pollfd *fds, unsigned int nfds, int timesout);
函數參數:
表示一個pollfd結構的數組。用來保存想要監聽的文件描述符及其注冊(綁定)的相應事件
表示監聽事件集合的大小
指定poll的超時值。當timeout為-1時,就會一直阻塞,直到某個事件發生;當timeout為0時,表示立即返回。
返回值:
當為-1的時候表示失敗,當為0的時候表示超時,當為大于0的整數的時候表示執行成功,表示文件描述符的個數。
不同與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。
structpollfd {intfd;/* file descriptor */shortevents;/* requested events to watch */shortrevents;/* returned events witnessed */};
該結構里包含了要監視等待的event和實際發生的event;
經常檢測的事件標記:
POLLIN/POLLRDNORM:可讀
POLLOUT/POLLWRNORM:可寫
POLLERR:出錯
合法的事件標記如下:
POLLIN: 有數據可讀
POLLRDNORM: 有普通數據可讀
POLLRDBAND: 有優先數據可讀
POLLPRI: 有緊迫數據可讀
POLLOUT: 寫數據不會導致阻塞
POLLWRNORM: 寫普通數據不會導致阻塞
POLLWRBAND: 寫優先數據不會導致阻塞
POLLMSG SIGPOLL: 消息可用
POLLIN | POLLPRI等價于select()的讀事件,POLLOUT |POLLWRBAND等價于select()的寫事件。POLLIN等價于POLLRDNORM |POLLRDBAND,而POLLOUT則等價于POLLWRNORM。
從原理上看,select?和?poll?都需要在返回以后,通過遍歷文件描述符來獲取已經就緒的socket。但是和select不同的是,調用這個函數后,系統不用清空它所檢測的socket描述符集合;
因此select函數適合于只檢測少量socket描述符的情況,而poll函數適合于大量socket描述符的情況;
#include#include#include#include#include#include#include#defineOPEN_MAX 100intmain(int argc, char *argv[]){//1.創建tcp監聽套接字intsockfd = ::socket(AF_INET, SOCK_STREAM,0);//2.綁定sockfdstructsockaddr_in my_addr;? ? bzero(&my_addr,sizeof(my_addr));? ? my_addr.sin_family = AF_INET;? ? my_addr.sin_port = htons(8000);? ? my_addr.sin_addr.s_addr = htonl(INADDR_ANY);? ? bind(sockfd, (structsockaddr *)&my_addr,sizeof(my_addr));//3.監聽listenlisten(sockfd,10);//4.poll相應參數準備structpollfd client[OPEN_MAX];inti =0, maxi =0;for(;i maxi)? ? ? ? ? ? ? ? maxi = i;? ? ? ? }//5.2繼續響應就緒的描述符for(i=1; i<=maxi; i++)? ? ? ? {if(client[i].fd <0)continue;if(client[i].revents & (POLLIN | POLLERR))? ? ? ? ? ? {intlen =0;charbuf[128] ="";//5.2.1接受客戶端數據if((len = recv(client[i].fd, buf,sizeof(buf),0)) <0)? ? ? ? ? ? ? ? {if(errno == ECONNRESET)//tcp連接超時、RST{? ? ? ? ? ? ? ? ? ? ? ? close(client[i].fd);? ? ? ? ? ? ? ? ? ? ? ? client[i].fd =-1;? ? ? ? ? ? ? ? ? ? }elsecout<<"read error:"<
kqueue
intkqueue(void);
生成一個內核事件隊列,返回該隊列的文件描述符,其它API通過這個描述符操作這個?kqueue,結構如下:
structkevent {uintptr_tident;//事件ID,一般為文件描述符shortfilter;//事件過濾器u_short flags;//行為標示u_int fflags;//過濾器標識值intptr_tdata;//過濾器數據void*udata;//應用透傳數據};intkevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);
提供向內核注冊/反注冊事件和返回就緒事件或錯誤事件;在一個kqueue中,{ident,filter}確定一個唯一的事件;
函數參數:
kq:kqueue的文件描述符
changelist:注冊/反注冊的事件數組
nchanges:changelist的元素個數
eventlist:滿足條件的通知事件數組
nevents:eventlist的元素個數
timeout:等待事件到來時的超時時間
返回值為可用事件的個數
kqueue不光能夠處理socket的事件,同時還能處理異步io,信號,文件變化等等;
kqueue有兩個部分,分別是kqueue和kevent;kqueue主要是用來描述event的隊列,而kevent則是監聽的事件;
通過kevent提供三個主要的行為功能,分別是
注冊/反注冊
注意kevent中的neventlist這個輸入參數,當其設為0,且傳入合法的changelist和nchanges,就會將?changelist?中的事件注冊到 kqueue 中;
允許/禁止過濾器事件
通過flags EV_ENABLE 和 EV_DISABLE?使過濾器事件有效或者無效,這個功能在使用?EVFILT_WRITE發送數據時非常有用;
等待事件通知
將?nchangelist 和 nchanges?設置成?null和0?,當kevent非錯誤和超時返回時,在?eventlist和nevents?中保存可用事件集合。
實現
#include#include#include#include#include#include#include#definePORT 5001#defineMAX_EVENT_COUNT 64intcreateSocket(){intsock = socket(PF_INET, SOCK_STREAM,0);if(sock ==-1)? ? {printf("socket() failed:%d\n",errno);return-1;? ? }structsockaddr_in addr;? ? addr.sin_family = AF_INET;? ? addr.sin_port = htons(PORT);? ? addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);intoptval =1;? ? setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval,sizeof(optval));? ? optval =1;? ? setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &optval,sizeof(optval));if(bind(sock, (structsockaddr*)&addr,sizeof(structsockaddr)) ==-1)? ? {printf("bind() failed:%d\n",errno);return-1;? ? }if(listen(sock,5) ==-1)? ? {printf("listen() failed:%d\n",errno);return-1;? ? }returnsock;}intmain(int argc, const char * argv[]){intlistenfd = createSocket();if(listenfd ==-1)return-1;intkq = kqueue();if(kq ==-1)? ? {printf("kqueue failed:%d",errno);return-1;? ? }structkevent event = {listenfd,EVFILT_READ,EV_ADD,0,0,NULL};intret = kevent(kq, &event,1,NULL,0,NULL);if(ret ==-1)? ? {printf("kevent failed:%d",errno);return-1;? ? }while(true)? ? {structkevent eventlist[MAX_EVENT_COUNT];structtimespec timeout = {5,0};intret = kevent(kq,NULL,0, eventlist, MAX_EVENT_COUNT, &timeout);if(ret <=0)continue;for(inti=0; i0)? ? ? ? ? ? ? ? {structkevent changelist[2];? ? ? ? ? ? ? ? ? ? EV_SET(&changelist[0], clientfd, EVFILT_READ, EV_ADD,0,0,NULL);? ? ? ? ? ? ? ? ? ? EV_SET(&changelist[1], clientfd, EVFILT_WRITE, EV_ADD,0,0,NULL);? ? ? ? ? ? ? ? ? ? kevent(kq, changelist,1,NULL,0,NULL);? ? ? ? ? ? ? ? }continue;? ? ? ? ? ? }//異常事件if(flags & EV_ERROR)? ? ? ? ? ? {? ? ? ? ? ? ? ? close(sock);structkevent event = {sock,EVFILT_READ,EV_DELETE,0,0,NULL};? ? ? ? ? ? ? ? kevent(kq, &event,1,NULL,0,NULL);printf("socket broken,error:%ld\n",data);continue;? ? ? ? ? ? }//數據可讀if(filter == EVFILT_READ)? ? ? ? ? ? {charbuffer[data];memset(buffer,'\0', data);ssize_trecvlen = recv(sock, buffer, data,0);if(recvlen <=0)? ? ? ? ? ? ? ? {//鏈接斷開close(sock);structkevent event = {sock,EVFILT_READ,EV_DELETE,0,0,NULL};? ? ? ? ? ? ? ? ? ? kevent(kq, &event,1,NULL,0,NULL);printf("socket broken!\n");continue;? ? ? ? ? ? ? ? }printf("%s\n",buffer);? ? ? ? ? ? }//數據可寫if(filter == EVFILT_WRITE)? ? ? ? ? ? {charbuffer[data];memset(buffer,'a', data);ssize_tsendlen = send(sock, buffer, data,0);if(sendlen <=0)? ? ? ? ? ? ? ? {//鏈接斷開close(sock);structkevent event = {sock,EVFILT_READ,EV_DELETE,0,0,NULL};? ? ? ? ? ? ? ? ? ? kevent(kq, &event,1,NULL,0,NULL);printf("socket broken!\n");continue;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? }return0;}
不同
和前面不同的是,kqueue不會像select或者poll一樣每隔一段事件就去輪詢所有的socket,當socket數量很多,但是很多socket都不活躍的時候,性能是有影響的,而kqueue只會關注事件發生的socket;
epoll
函數
創建事件表
intepoll_create(int size);
創建一個epoll的句柄,參數 size 并不是限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的建議,不同于select中的給出最大監聽的fd+1。
操作事件表
intepoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
函數參數
epfd:事件表的文件描述符
op:何種操作,包括?EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD,分別實現對fd的監聽事件進行添加、刪除、修改
fd:需要監聽的文件描述符
event:告訴內核需要監聽什么事
epoll_event 結構如下:
structepoll_event {__uint32_tevents;/* Epoll events */epoll_data_tdata;/* User data variable */};//events可以是以下幾個宏的集合:EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);EPOLLOUT:表示對應的文件描述符可以寫;EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);EPOLLERR:表示對應的文件描述符發生錯誤;EPOLLHUP:表示對應的文件描述符被掛斷;EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列里
監聽相應事件
intepoll_wait(intepfd,structepoll_event *events,intmaxevents,inttimeout)
函數參數:
epfd:事件表的文件描述符
events:從內核得到事件的集合
maxevents:事件集合的大小(不能大于創建時的size)
timeout:超時時間
工作模式
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:
LT模式:當epoll_wait檢測到描述符事件發生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序并通知此事件。
ET模式:當epoll_wait檢測到描述符事件發生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序并通知此事件。
ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
當使用epoll的ET模型來工作時,當產生了一個EPOLLIN事件后, 讀數據的時候需要考慮的是當recv()返回的大小如果等于請求的大小,那么很有可能是緩沖區還有數據未讀完,也意味著該次事件還沒有處理完,所以還需要再次讀取:
voidhandle_rev(){while(rs){? ? ? ? buflen = ::recv(activeevents[i].data.fd, buf,sizeof(buf),0);if(buflen <0){// 由于是非阻塞的模式,所以當errno為EAGAIN時,表示當前緩沖區已無數據可讀// 在這里就當作是該次事件已處理處.if(errno == EAGAIN){//EAGAIN經常出現在當應用程序進行一些非阻塞(non-blocking)操作(對文件或socket)的時候break;? ? ? ? ? ? }else{return;? ? ? ? ? ? }? ? ? ? }elseif(buflen ==0){// 這里表示對端的socket已正常關閉.}if(buflen ==sizeof(buf){? ? ? ? ? ? rs =1;// 需要再次讀取}else{? ? ? ? ? ? rs =0;? ? ? ? }? ? }}
有時候epoll不一定比select和poll的效率高,比如這樣的場景下:當活動連接數比較高的時候此時epoll會經常觸發回調函數 ,此時在性能上還是有一定的損失.epoll適用于連接數量多,但是活躍的連接少.
實現
epollserver::epollserver(intaf,inttype,intprotocol) : norserver(af, type, protocol) {this->_epollfd = ::epoll_create(MAX_SIZE);if(this->_epollfd == INVALID_SOCKTE) {cout<<"epoll create failed"<close(this->socket());}voidepollserver::wait_events() {structepoll_event _events[EPOLL_EVENTS_NUM];this->add_event(this->socket(), EPOLLIN);while(true) {intret = ::epoll_wait(this->_epollfd, _events, EPOLLEVENTS,-1);this->handle_events(_events, ret);? ? }}voidepollserver::handle_events(structepoll_event* events,intnum) {for(inti =0; i < num; i++) {intsocket = events[i].data.fd;// 服務器本身if(socket ==this->socket()) {this->handle_accept();? ? ? ? }elseif(events[i].events & EPOLLIN) {this->handle_read(socket);? ? ? ? }elseif(events[i].events & EPOLLOUT) {this->handle_write(socket);? ? ? ? }? ? }}voidepollserver::handle_accept() {this->accept();}voidepollserver::handle_read(intsocket) {intnread;charbuf[MAX_SIZE];? ? nread = ::read(socket, buf, MAX_SIZE);if(nread == SOCKET_ERROR)? ? {cout<<"read error:"<close(socket);//記住close fddelete_event(socket, EPOLLIN);//刪除監聽}elseif(nread ==0)? ? {fprintf(stderr,"client close.\n");this->close(socket);//記住close fddelete_event(socket, EPOLLIN);//刪除監聽}else{cout<<"read message is :"<< buf;//修改描述符對應的事件,由讀改為寫modify_event(socket, EPOLLOUT);? ? }}voidepollserver::handle_write(intsocket) {intnwrite;charbuf[MAX_SIZE];? ? nwrite = ::write(socket, buf,strlen(buf));if(nwrite ==-1){cout<<"write error:"<close(socket);//記住close fddelete_event(socket, EPOLLOUT);//刪除監聽}else{? ? ? ? modify_event(socket, EPOLLIN);? ? }memset(buf,0, MAX_SIZE);}boolepollserver::add_event(intsocket,intstate) {structepoll_event ev;? ? ev.events = state;? ? ev.data.fd = socket;if(!epoll_ctl(this->_epollfd, EPOLL_CTL_ADD, socket, fd, &ev)) {cout<<"epoll add event failed"<_epollfd, EPOLL_CTL_DEL, socket, fd, &ev)) {cout<<"epoll delete event failed"<_epollfd, EPOLL_CTL_MOD, socket, fd, &ev)) {cout<<"epoll modify event failed"<
參考文章