epoll多路復用的一個實例程序(C實現)
本文實例為大傢分享瞭epoll多路復用一個實例程序的具體代碼,供大傢參考,具體內容如下
1、實例程序描述
編寫一個echo server程序,功能是客戶端向服務端發送消息,服務端接收到消息後輸出,並原樣返回給客戶端,客戶端接收到服務端的應答消息並打印輸出。
2、公共接口函數部分
2.1、common.h 源文件
/** **描述:公共頭文件 */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <fcntl.h> #include <unistd.h> #include <netdb.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/epoll.h> typedef struct epoll_event EPOLL_EVENT_T; #define BUF_SIZE 2048 #define LISTENQ 10 #define FD_SEZE 1000 #define MAX_EVENTS 100 #define ERROR_SOCKET_SELECT -1 #define ERROR_SOCKET_TIMEOUT -2 #define ERROR_SOCKET_READ -3 #define ERROR_SOCKET_WRITE -4 #define ERROR_SOCKET_CLOSE -5 #define ERROR_SOCKET_CREATE -6 #define ERROR_SOCKET_BIND -7 #define ERROR_SOCKET_LISTEN -8 #define ERROR_SOCKET_CONNECT -9 #define ERROR_EPOLL_CREATE -10 #define ERROR_EPOLL_CTL_ADD -11 #define ERROR_EPOLL_CTL_DEL -12 #define ERROR_EPOLL_CTL_MOD -13 #define ERROR_ARGUMENT -999 int add_epoll_event(int epollfd, int fd, int events); int del_epoll_event(int epoll, int fd, int events); int mod_epoll_event(int epoll, int fd, int events); int make_socket_nonblock(int sock_fd); int listen_socket(char *ip, int port, int nonblock); int connect_socket(char *ip, int port,int nonblock);
2.2、common.c 源文件
/** **描述:公共函數 */ #include "common.h" int add_epoll_event(int epollfd, int fd, int events) { EPOLL_EVENT_T ev; ev.events = events; ev.data.fd = fd; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1){ perror("epoll_ctl_add"); return ERROR_EPOLL_CTL_ADD; } } int del_epoll_event(int epollfd, int fd, int events) { EPOLL_EVENT_T ev; ev.events = events; ev.data.fd = fd; if(epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &ev) == -1){ perror("epoll_ctl_del"); return ERROR_EPOLL_CTL_ADD; } } int mod_epoll_event(int epollfd, int fd, int events) { EPOLL_EVENT_T ev; ev.events = events; ev.data.fd = fd; if(epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &ev) == -1){ perror("epoll_ctl_mod"); return ERROR_EPOLL_CTL_MOD; } } //設置socket為非阻塞模式函數 int make_socket_nonblock(int sock_fd) { int flags; if((flags = fcntl(sock_fd, F_GETFL, NULL)) < 0){ printf("get socket fd flags error:%d %s", errno, strerror(errno)); return -1; } if(fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) == -1){ printf("set socket non-block error:%d %s", errno, strerror(errno)); return -1; } return 0; } int listen_socket(char *ip, int port, int nonblock) { int opt=1, sockfd; struct sockaddr_in svr_addr; if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ close(sockfd); return ERROR_SOCKET_CREATE; } if(nonblock) //設置socket為非阻塞模式 make_socket_nonblock(sockfd); //SO_REUSEADDR是讓端口釋放後立即就可以被再次使用 setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); memset(&svr_addr, 0, sizeof(struct sockaddr_in)); svr_addr.sin_family = AF_INET; if(ip == NULL) svr_addr.sin_addr.s_addr = htonl(INADDR_ANY); else svr_addr.sin_addr.s_addr = inet_addr(ip); svr_addr.sin_port = htons(port); if(bind(sockfd, (struct sockaddr*)&svr_addr, sizeof(struct sockaddr)) == -1){ close(sockfd); return ERROR_SOCKET_BIND; } if(listen(sockfd, LISTENQ) == -1){ close(sockfd); return ERROR_SOCKET_LISTEN; } return sockfd; } int connect_socket(char *ip, int port,int nonblock) { int sockfd; struct sockaddr_in svr_addr; if(ip==NULL || strlen(ip)==0 || port<=0) return ERROR_ARGUMENT; sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0){ perror("socket"); return ERROR_SOCKET_CREATE; } memset(&svr_addr, 0, sizeof(svr_addr)); svr_addr.sin_family = AF_INET; svr_addr.sin_addr.s_addr = inet_addr(ip); svr_addr.sin_port = htons(port); if(connect(sockfd, (struct sockaddr*)&svr_addr, sizeof(svr_addr)) == -1){ perror("connect"); close(sockfd); return ERROR_SOCKET_CONNECT; } if(nonblock) make_socket_nonblock(sockfd); return sockfd; }
3、服務端源文件 epoll_server.c
/** **程序描述:編寫一個echo server程序,功能是客戶端向服務器發送信息,服務器端接收數據後輸出並原樣返回給客戶端,客戶端接收到消息並輸出到終端。 */ #include "common.h" void do_epoll(int listen_fd); void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int listen_fd, char *buf); void on_accept(int epollfd, int listen_fd); void on_read(int epollfd, int fd, char *buf); void on_write(int epollfd, int fd, char *buf); int main(int argc, int *argv[]) { char svr_ip[32]={0}; int svr_port; int listen_fd, nonblock=0; if(argc < 3){ printf("ERROR: too few command-line arguments\n"); printf("Usage: %s <svr_ip> <svr_port>\n", argv[0]); return -1; } strncpy(svr_ip, argv[1], strlen(argv[1])); svr_port = atoi(argv[2]); listen_fd = listen_socket(svr_ip, svr_port, nonblock); if(listen_fd < 0){ printf("listen socket error:%d %s\n", errno, strerror(errno)); return -2; } printf("epoll_svr listen on[%s:%d] succ\n", svr_ip, svr_port); do_epoll(listen_fd); printf("exit epoll_svr succ\n"); return 0; } void do_epoll(int listen_fd) { EPOLL_EVENT_T events[MAX_EVENTS]; int epollfd, conn_fd, nfds; char buf[BUF_SIZE]={0}; //創建一個epoll文件描述符 epollfd = epoll_create(FD_SEZE); if(epollfd == -1){ perror("epoll_create"); close(listen_fd); return; } //添加監聽描述符的讀事件 add_epoll_event(epollfd, listen_fd, EPOLLIN); for(;;){ nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if(nfds == -1){ perror("epoll_wait"); close(listen_fd); break; } handle_epoll_events(epollfd, events, nfds, listen_fd, buf); } close(epollfd); } void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int sockfd, char *buf) { int i=0; int fd; //printf("nfds = %d\n", nfds); for(i=0; i < nfds; i++){ fd = events[i].data.fd; //根據文件描述符類型和事件類型進行相應處理 if(fd == sockfd && (events[i].events & EPOLLIN)) on_accept(epollfd, sockfd); else if(events[i].events & EPOLLIN) on_read(epollfd, fd, buf); else if(events[i].events & EPOLLOUT) on_write(epollfd, fd, buf); } } void on_accept(int epollfd, int listen_fd) { int conn_fd; struct sockaddr_in cli_addr; socklen_t cli_addr_len=sizeof(struct sockaddr); conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_addr_len); if(conn_fd == -1){ perror("accept"); } else{ printf("accept a new client:[%s:%d], conn_fd=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), conn_fd); //make_socket_nonblock(conn_fd); add_epoll_event(epollfd, conn_fd, EPOLLIN); //添加一個客戶端連接描述符的讀事件 } } void on_read(int epollfd, int fd, char *buf) { int nread; nread = read(fd, buf, BUF_SIZE); if(nread == -1){ perror("read"); close(fd); del_epoll_event(epollfd, fd, EPOLLIN); } else if(nread == 0){ printf("ERROR:client close\n"); close(fd); del_epoll_event(epollfd, fd, EPOLLIN); } else{ printf("recv req msg from client succ, fd=%d, msg:%s", fd, buf); //修改描述符對應的事件,由讀改為寫 mod_epoll_event(epollfd, fd, EPOLLOUT); } } void on_write(int epollfd, int fd, char *buf) { int nwrite; nwrite=write(fd, buf, strlen(buf)); if(nwrite == -1){ perror("write"); close(fd); del_epoll_event(epollfd, fd, EPOLLOUT); } else{ printf("send resp msg to client succ, fd=%d, msg:%s\n", fd, buf); mod_epoll_event(epollfd, fd, EPOLLIN); //將描述符對應的事件,由寫改為讀 } memset(buf, 0, sizeof(buf)); }
4、客戶端源文件 epoll_client.c
/** **程序描述:回射程序echo客戶端。客戶端也要使用epoll實現對路復用,控制STDIN_FILENO、STDOUT_FILENO和sockfd這三個描述符對應的事件。 STDIN_FILENO:標準輸入描述符,隻有一個讀事件 STDOUT_FILENO:標準輸出描述符,隻有一個寫事件 sockfd: 有兩個事件,一個是讀事件,即讀取從服務端發送來的數據;另一個是寫事件,即發送數據給服務端。需要註意的是,在同一時刻,它隻能有一個事件 */ #include "common.h" void do_epoll(int sockfd); void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int sockfd, char *buf); void on_read(int epollfd, int fd, int sockfd, char *buf); void on_write(int epollfd, int fd, int sockfd, char *buf); int main(int argc, char *argv[]) { char svr_ip[32]={0}; int svr_port; int conn_fd, nonblock=0; if(argc < 3){ printf("ERROR: too few command-line arguments\n"); printf("Usage: %s <svr_ip> <svr_port>\n", argv[0]); return -1; } strncpy(svr_ip, argv[1], strlen(argv[1])); svr_port = atoi(argv[2]); conn_fd=connect_socket(svr_ip, svr_port, nonblock); if(conn_fd < 0){ printf("connect to server failed!\n", conn_fd); return -2; } printf("connect to server succ, sockfd=%d\n", conn_fd); do_epoll(conn_fd); printf("exit epoll_cli succ\n"); return 0; } void do_epoll(int sockfd) { int epollfd; EPOLL_EVENT_T events[MAX_EVENTS]; int nfds; char buf[BUF_SIZE]={0}; epollfd = epoll_create(FD_SEZE); add_epoll_event(epollfd, STDIN_FILENO, EPOLLIN); //註冊標準輸入讀事件 for(;;){ nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if(nfds == -1){ perror("epoll_wait"); close(sockfd); break; } handle_epoll_events(epollfd, events, nfds, sockfd, buf); } close(epollfd); } void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int sockfd, char *buf) { int i, fd; //printf("nfds = %d\n", nfds); for(i=0; i<nfds; i++){ fd = events[i].data.fd; //根據文件描述符類型和事件類型進行相應處理 if(events[i].events & EPOLLIN) on_read(epollfd, fd, sockfd, buf); else if(events[i].events & EPOLLOUT) on_write(epollfd, fd, sockfd, buf); } } void on_read(int epollfd, int fd, int sockfd, char *buf) { int nread; nread = read(fd, buf, BUF_SIZE); if(nread < 0){ perror("read error"); close(fd); } else if(nread == 0){ printf("epoll_svr close.\n"); close(fd); exit(-1); } else{ if(fd == STDIN_FILENO){ //標準輸入描述符的讀事件 printf("stdin buf=%s", buf); add_epoll_event(epollfd, sockfd, EPOLLOUT); //註冊sockfd的寫事件 } else{//sockfd描述符的讀事件 del_epoll_event(epollfd, sockfd, EPOLLIN); //刪除當前sockfd描述符的讀事件 add_epoll_event(epollfd, STDOUT_FILENO, EPOLLOUT); //註冊標準輸出寫事件 } } } void on_write(int epollfd, int fd, int sockfd, char *buf) { int nwrite; nwrite = write(fd, buf, strlen(buf)); if(nwrite < 0){ perror("write error"); close(fd); } else{ if(fd == STDOUT_FILENO) //標準輸出描述符的寫事件 { printf("recv resp_msg from svr, msg=%s\n", buf); del_epoll_event(epollfd, fd, EPOLLOUT); //刪除輸出描述符的寫事件 } else //sockfd描述符的寫事件 { printf("send req_msg to svr succ, msg=%s", buf); mod_epoll_event(epollfd, fd, EPOLLIN); //修改sockfd描述符為讀事件 } } memset(buf, 0, BUF_SIZE); }
5、Makefile文件
#第1種方式 all: epoll_server epoll_client epoll_server: epoll_server.o common.o $(LINK) epoll_client: epoll_client.o common.o $(LINK) %.o: %.c $(COMPILE) #compile & link CFLAGS += -g COMPILE=gcc -c -g -std=gnu99 -o $@ $< LINK=gcc -g -o $@ $^ clean: rm -rf *.o epoll_server epoll_client
6、總結分析
6.1 客戶端程序分析
1、對於客戶端程序而言,我們監聽3個文件描述符,分別是連接服務端的sockfd,標準輸入描述符STDIN_FILENO 以及 標準輸出描述符STDOUT_FILENO。在 do_poll函數中,我們首先註冊瞭標準輸入描述符的讀事件(EPOLLIN),然後在for循環中,循環調用epoll_wait系統調用,handle_epoll_events是整個epoll事件表的handler函數。當我們向終端輸入數據完畢的時候,就會觸發標準輸入描述符的讀事件,從而調用讀事件處理函數on_read,在on_read函數中,註冊瞭sockfd的寫事件。
2、在do_poll的for循環中,繼續調用epoll_wait 和 handle_epoll_events,發現sockfd的寫事件已就緒,轉向調用寫事件處理函數on_write。在on_write函數中,發送消息給服務端,然後修改sockfd描述符的讀事件就緒,準備接收服務端發來的應答消息。
3、在do_poll的for循環中,繼續調用epoll_wait 和 handle_epoll_events,發現sockfd的讀事件已就緒,轉向調用寫事件處理函數on_write。在on_read函數中,刪除當前sockfd描述符的讀事件並註冊標準輸出描述符的寫事件。之所以要刪除掉sockfd的讀事件,是避免其一直處於就緒狀態。
4、在do_poll的for循環中,繼續調用epoll_wait 和 handle_epoll_events,發現標準輸出描述符的寫事件已就緒,轉向調用寫事件處理函數on_write。在on_write函數中,輸出服務端發來的應答消息並刪除標準輸出描述符的寫事件。之所以要刪除掉標準輸出描述符的寫事件,還是為瞭避免其一直處於就緒狀態,具體表現就是不停地在終端打印信息。
6.2 服務端程序分析
1、對於服務端程序而言,我們監聽2個文件描述符,分別是接受多個客戶端連接請求的listen_fd和處理單個客戶端的數據讀寫的conn_fd。
2、在main函數中,首先創建瞭監聽連接請求的listen_fd文件描述符。然後在do_poll函數中,首先註冊瞭listen_fd描述符的讀事件(EPOLLIN)。在for循環中,循環調用epoll_wait系統調用和epoll事件handler函數handle_epoll_events。
3、當有客戶端發起連接請求時,會觸發listen_fd描述符的讀事件,轉向執行listen_fd的讀事件處理函數on_accept。在on_accept函數中,客戶端與服務端成功建立連接,並返回一個conn_fd文件描述符,然後註冊這個描述符的讀事件。
4、在do_poll的for循環中,繼續調用epoll_wait 和 handle_epoll_events,當客戶端有數據發送給服務端時,觸發conn_fd的讀事件,轉向執行on_read函數。在on_read函數中,接收客戶端發來的消息並在終端輸出,然後修改conn_fd描述符為寫事件就緒。如果接收到的數據大小為0,則說明連接已經斷開,則關閉conn_fd描述符並刪除其讀事件。
5、在do_poll的for循環中,繼續調用epoll_wait 和 handle_epoll_events,發現conn_fd的寫事件就緒,轉向執行on_write函數。在on_write函數中,發送應答消息給客戶端,並修改conn_fd描述符的讀事件就緒。
題外話
本epoll實例程序,本人已經在CentOS 7.6系統下測試通過瞭。
以上就是本文的全部內容,希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。
推薦閱讀:
- epoll封裝reactor原理剖析示例詳解
- C++基於reactor的服務器百萬並發實現與講解
- 基於epoll的多線程網絡服務程序設計
- 如何用C寫一個web服務器之I/O多路復用
- Linux下Select多路復用實現簡易聊天室示例