// IO多路復用,事件驅動+非阻塞,實現一個線程完成對多個fd的監控和響應,提升CPU利用率 // epoll優點: // 1.select需要每次調用select時拷貝fd,epoll_ctl拷貝一次,epoll_wait就不需要重復拷貝 // 2.不需要像select遍歷fd做檢查,就緒的會被加入就緒list,遍歷list完成處理 // 3.沒有最大連接限制,與最大文件數目相關:cat /proc/sys/fs/file-max,與內存相關 // epoll實現相關: // 1.epoll_ctl,將fd的event使用RB tree保存,讀寫O(logN); // 2.一旦有event,內核負責添加到rdlist鏈表 // 3.epoll_wait檢查鏈表看是否有事件,並進行處理 // Ref // https://www.cnblogs.com/lojunren/p/3856290.html // http://blog.chinaunix.net/uid-28541347-id-4273856.html // Question: // 是否需要每個event一個實例? #include <cstdlib> /* exit() */ #include <cstdio> /* perror(): 打印信息+發生錯誤的原因,可用於定位。 */ #include <iostream> /* cin cout */ #include <cstdint> /* uint32 */ #include <cstring> /* memset memcpy*/ #include <sys/types.h> /* 為了滿足一些 BSD系統添加頭文件*/ #include <sys/socket.h> /* socket(); listen(); baccept(); socklen_t */ #include <netinet/in.h> /* struct sockaddr_in: 保存socket信息; ntohl(), ntohs(), htonl() and htons()*/ #include <arpa/inet.h> /* inet_ntoa */ #include <sys/epoll.h> /* epoll_create(); epoll_ctlstruct epoll_event*/ #include <unistd.h> /* read() write(), 不是C語言范疇,所以沒有cxxxx的實現 */ #include <cerrno> /* errno */ // http://minirighi.sourceforge.net/html/errno_8h.html typedef void (*eventHandleFunc)(void); typedef struct tzEventHandler{ eventHandleFunc event_handler_func; void *ptr; int fd; }TzEventHandler; void handlerImpl(void){ std::cout << "handle an event." << std::endl; } void read_handlerImpl(void){ std::cout << "handle an read event." << std::endl; } void send_handlerImpl(void){ std::cout << "handle an send event." << std::endl; } void checEventType(uint32_t type){ std::cout << "type check:" << std::endl; if(type & EPOLLIN) std::cout << "\tEPOLLIN" << std::endl; if(type & EPOLLOUT) std::cout << "\tEPOLLOUT" << std::endl; if(type & EPOLLRDHUP ) std::cout << "\tEPOLLRDHUP " << std::endl; if(type & EPOLLPRI) std::cout << "\tEPOLLPRI" << std::endl; if(type & EPOLLERR) std::cout << "\tEPOLLERR" << std::endl; if(type & EPOLLHUP) std::cout << "\tEPOLLHUP" << std::endl; if(type & EPOLLET) std::cout << "\tEPOLLET" << std::endl; if(type & EPOLLONESHOT ) std::cout << "\tEPOLLONESHOT " << std::endl; if(type & EPOLLWAKEUP ) std::cout << "\tEPOLLWAKEUP " << std::endl; } /** \brief 錯誤處理函數 */ void tzError(const char *msg) { perror(msg); exit(1); // 一般不同原因不同的exit code更為規范 } int main(int argc, char *argv[]){ // listen socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 監聽端口非阻塞 if(listenfd<0){ tzError("listen socket()"); } // bind struct sockaddr_in listen_addr = {0}; listen_addr.sin_family = AF_INET; listen_addr.sin_port = htons(8081); listen_addr.sin_addr.s_addr = INADDR_ANY; int socket_opt_ret = bind(listenfd, (struct sockaddr*)&listen_addr, sizeof(listen_addr)); if(socket_opt_ret<0){ tzError("bind()"); } // listen #define MAX_LISTEN_TCP 10 socket_opt_ret = listen(listenfd, MAX_LISTEN_TCP); if(socket_opt_ret<0){ tzError("listen()"); } // epoll int epoll_fd = epoll_create(5); // int epoll_create(int size); // http://man7.org/linux/man-pages/man2/epoll_create.2.html // size: 用於告訴kernel可能被添加的caller數量,內核估計需要開辟的空間 // 2.6.8被忽略,kernel會自動開辟需要的空間。但必須大於0(兼容)。 // return: epoll相關句柄,一組連接的管理只需要一個 if(epoll_fd<0){ tzError("epoll_create()"); } // 自定義處理對象的設置 TzEventHandler listen_handler; listen_handler.fd = listenfd; // event相關fd listen_handler.event_handler_func = handlerImpl; int cnt = 0; listen_handler.ptr = &cnt; // event設置 struct epoll_event event = {0}; // Hint:對於結構體,{0}觸發聚合初始化,全置0 // 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 events: // EPOLLIN 可以read時觸發 // read event: // socket of TCP,三次握手結束,可以accept時 // socket 接收緩沖數據>SO_RCVLOWAT,default 1,使用read處理 // socket peer關閉連接時,且read為0;如果非阻塞無數據,read返回-1並設置errno=EAGAIN // socket 有未處理的錯誤,此時可以用getsockopt來讀取和清除該錯誤 // EPOLLOUT 可以write時觸發 // write event: // socket 發送緩沖數據>SO_SNDLOWAIT // socket 非阻塞模式下,connect返回之后,發起連接成功或失敗 // socket上有未處理的錯誤,此時可以用getsockopt來讀取和清除該錯誤 // EPOLLET 邊緣觸發模式??? // EPOLLRDHUP >2.6.17,如果對方close connection或write時對方退出時觸發。可用於邊緣模式的探測。 event.events = EPOLLIN; event.data.ptr = (void*)&listen_handler; // 指向處理事件的對象 // event注冊 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listenfd, &event); // int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // http://man7.org/linux/man-pages/man2/epoll_ctl.2.html // epfd: epoll實例的fd // op: // EPOLL_CTL_ADD 添加事件,將要監聽的fd注冊,並設置event中的事件進行監聽 // EPOLL_CTL_MOD 修改事件 // EPOLL_CTL_DEL 刪除事件,刪除對應的df,event參數不為NULL但是被忽略(after 2.6.9可以為NULL) // fd: // 要被監聽事件的fd,所以epoll數量只受文件系統限制 // event: // 指定了監聽設置的結構體,包含要監測的事件類型,處理方法 // 注意:每個fd只能add一次,改變監聽事件使用MOD; // 如果添加多次fd,視為無效,並errno返回EEXIST,epoll_ctl返回-1 // event處理 #define BUFSIZE 100 // read接收緩存 #define MAXNFD 10 // 一次最多接受read的數量 struct epoll_event recv_events[MAXNFD] = {0}; // 用於保存獲取的事件隊列,依次處理 int n_ready_event=0; char buf[MAXNFD][BUFSIZE] = {0}; // wait事件 #define EPOLL_TIMEOUT_MS -1 // -1 nonblock while(true){ n_ready_event = epoll_wait(epoll_fd, recv_events, MAXNFD, EPOLL_TIMEOUT_MS); // int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // http://man7.org/linux/man-pages/man2/epoll_wait.2.html // timeout: // 如果-1非阻塞,如果有事件未處理,且為設置邊緣觸發,則一直觸發事件。 if(n_ready_event<0){ tzError("epoll_wait()"); } int iter = 0; for(iter=0; iter<n_ready_event; iter++){ // 處理事件 TzEventHandler *returned_event_handler = (TzEventHandler *)recv_events[iter].data.ptr; std::cout << ">>>get event, handler fd:" << returned_event_handler->fd << ", cnt:" << *(int*)returned_event_handler->ptr << std::endl; (*(int*)returned_event_handler->ptr)++; checEventType(recv_events[iter].events); // 如果是可讀事件 if(recv_events[iter].events & EPOLLIN){ // 如果是像listen發出的監聽請求 if(returned_event_handler->fd==listenfd){ // listener struct sockaddr_in clientaddr = {0}; socklen_t client_sock_addr_len = sizeof(clientaddr); int tcp_socketfd = accept(listenfd, (struct sockaddr*)&clientaddr, &client_sock_addr_len); if (tcp_socketfd<0){ tzError("accept()"); } else{ std::cout << "accept" << std::endl; std::cout << "incoming:" << inet_ntoa(clientaddr.sin_addr) << std::endl; // 為新的socket注冊事件 TzEventHandler socket_read_handler; socket_read_handler.fd = tcp_socketfd; // event相關fd socket_read_handler.event_handler_func = read_handlerImpl; int cnt = 0; socket_read_handler.ptr = &cnt; struct epoll_event new_event = {0}; new_event.events = EPOLLIN; new_event.data.ptr = (void*)&socket_read_handler; // 指向處理事件的對象 int epoll_ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, tcp_socketfd, &new_event); if(epoll_ret==0) std::cout << "add event" << std::endl; } } else{ // tcp peer msg returned_event_handler->event_handler_func(); #define BUF_SIZE 100 char buf[BUF_SIZE]; memset(buf, 0, BUF_SIZE); int ret = read(returned_event_handler->fd, buf, BUF_SIZE); // 注意:如果read的長度小於到達的數據,會留下剩余的數據再次觸發IN evnent if(ret == 0){ // connect closed std::cout << "TCP fd:" << returned_event_handler->fd << " disconnect." << std::endl; epoll_ctl(epoll_fd, EPOLL_CTL_DEL, returned_event_handler->fd, &recv_events[iter]); // 刪除event,否則會不斷報事件,其他處理方法? close(returned_event_handler->fd); }else{ std::cout << "recv content:" << buf << std::endl; // server在收到時才被動應答,所以此時才設置發送event recv_events[iter].events = EPOLLOUT; // 修改events類型,這樣設置在有發送時不觸發接收事件 returned_event_handler->event_handler_func = send_handlerImpl; int ctl_ret = epoll_ctl(epoll_fd, EPOLL_CTL_MOD, returned_event_handler->fd, &recv_events[iter]); if(ctl_ret<0){ // 使用error檢查錯誤原因 std::cout << "ctl_ret:" << ctl_ret << " errno:"; std::cout << errno << std::endl; } } } } // 如果是可寫事件 else if(recv_events[iter].events & EPOLLOUT){ returned_event_handler->event_handler_func(); int write_ret = write(returned_event_handler->fd, "get one msg", 11); recv_events[iter].events = EPOLLIN; // 修改events類型,這樣設置在有接受時不觸發發送事件 returned_event_handler->event_handler_func = read_handlerImpl; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, returned_event_handler->fd, &recv_events[iter]); }else{ std::cout << "unknown event" << std::endl; } } } // while true return 0; }