linux系统编程——从简单的聊天系统看epoll、poll、select
网上找到了一份基于epoll的简单的多人聊天室代码,感觉对epoll的学习十分有用,代码会附在后面,简单看一下epoll相关的API。
epoll相关的API主要有三个:epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
int epoll_create(int size);
参数size:用来告诉内核要监听的数目一共有多少个。
返回值:成功时,返回一个非负整数的文件描述符,作为创建好的epoll句柄。调用失败时,返回-1,错误信息可以通过errno获得。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数epfd:epoll_create()函数返回的epoll句柄。
参数op:操作选项,op的可选值有三个:EPOLL_CTL_ADD(注册新的fd到epfd上)、EPOLL_CTL_MOD(修改已经注册的fd的监听事件)和EPOLL_CTL_DEL(从epfd中删除一个fd)。
参数fd:要进行操作的目标文件描述符。
参数event:struct epoll_event结构指针,将fd和要进行的操作关联起来。
返回值:成功时,返回0,作为创建好的epoll句柄。调用失败时,返回-1,错误信息可以通过errno获得。
说明:epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
另外,event结构体的结构如下:
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 */ };
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数epfd:epoll_create()函数返回的epoll句柄。
参数events:struct epoll_event结构指针,用来从内核得到事件的集合。
参数 maxevents:告诉内核这个events有多大
参数 timeout: 等待时的超时时间,以毫秒为单位。
返回值:成功时,返回需要处理的事件数目。调用失败时,返回0,表示等待超时。
epoll和select、poll的对比
epoll提供了三个调用接口,而select和poll都只提供了一个接口。先说select,select有三个非常明显的缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,最后又会将fd从内核空间拷贝到用户空间,这个开销在fd很多的时候会很大(每次调用都需要拷贝的原因是因为select的监控是建立在对内核的修改上的,也就是说经过一次监控后,内核会修改位);
- 每次调用select的时候都需要在内核遍历传递进来的所有fd,这个开销在fd很多的时候也会很大,尤其因为是要遍历所有fd,因此需要耗费的时间是随fd数目的增长而线性增长的;
- select支持的fd的数目有限;
poll和select比较类似,只不过数据结构上做了一点改变,fd的数目不受限了,但是遍历fd的时间依旧是要随着fd数目的增长而线性增长的,这就会带来效率上的问题,因此便诞生了epoll。
epoll解决了select和poll的这三个问题,解决的方式是:
- epoll在linux内核中有一个高速缓存区,这个高速缓冲区中是有一个fd的就绪链表和红黑树结构的,调用epoll_ctl的时候会将用户态传入的fd放到这个高速缓存区的红黑树上,由于内核不修改fd的位,因此每次监控不需要重新拷贝
- epoll_ctl执行add动作时除了将文件句柄放到红黑树上之外,还向内核注册了该文件句柄的回调函数,内核在检测到某句柄可读可写时则调用该回调函数,回调函数将文件句柄放到就绪链表,这样遍历的时候只需要看一下这个链表里面的内容是否为空就可以了。
- epoll没有fd数目的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
参考资料:
- http://www.cnblogs.com/Anker/p/3265058.html
- http://oldblog.donghao.org/docs/linux_kernel_poll_epoll_2.pdf
- https://blog.csdn.net/russell_tao/article/details/7160071
- https://www.zhihu.com/question/20122137
Server.cpp
//server.cpp #include <iostream> #include <list> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> using namespace std; //选用list存放sockfd list<int> clients_list; //server port #define SERVER_PORT 8888 //epoll支持的最大并发量 #define EPOLL_SIZE 5000 //message buf size #define BUF_SIZE 0xFFFF #define SERVER_WELCOME "Welcome you join to the chat room! Your chat ID is: Client #%d" #define SERVER_MESSAGE "ClientID %d say >> %s" // exit #define EXIT "EXIT" #define CAUTION "There is only one int the char room!" // 设置成非阻塞 int setnonblocking(int sockfd){ /* fcntl用于修改某个文件描述符的属性,F_SETFL则表示方法名(这是一个设置fd描述符状态的方法),类似的方法还有几个,可以参照https://baike.baidu.com/item/fcntl */ fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK); return 0; } void addfd(int epollfd, int fd, bool enable_et){ struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN; if(enable_et){ ev.events = EPOLLIN | EPOLLET; } epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev); setnonblocking(fd); } int sendBroadcastmessage(int clientfd){ char buf[BUF_SIZE], message[BUF_SIZE]; //清零 bzero(buf, BUF_SIZE); bzero(message, BUF_SIZE); printf("read from client(clientID = %d)\n", clientfd); int len = recv(clientfd, buf, BUF_SIZE, 0); //len=0 client关闭了连接 if(len == 0){ close(clientfd); clients_list.remove(clientfd); printf("ClientID = %d closed.\n now there are %d client in the char room\n", clientfd, (int)clients_list.size()); }else{//进行广播 if(clients_list.size() == 1){ send(clientfd, CAUTION, strlen(CAUTION), 0); return len; } sprintf(message, SERVER_MESSAGE, clientfd, buf); list<int>::iterator iter; for(iter = clients_list.begin(); iter != clients_list.end(); iter++){ if(*iter != clientfd){ if(send(*iter, message, BUF_SIZE, 0) < 0){ perror("error"); exit(-1); } } } } } int main(int argc, char* argv[]){ //服务器IP + port struct sockaddr_in serverAddr; serverAddr.sin_family = PF_INET; serverAddr.sin_port = htons(SERVER_PORT); serverAddr.sin_addr.s_addr = htonl (INADDR_ANY); //表示所有地址 //创建监听socket int listenfd = socket(PF_INET, SOCK_STREAM, 0); //PF_INET或者AF_INET是IPv4网络协议的套接字类型,SOCK_STREAM提供面向连接的稳定数据传输,即表示TCP协议 if(listenfd < 0){ perror("listenfd"); exit(-1); } printf("listen socket created"); //绑定地址 //bind()的作用就是将参数listenfd和serverAddr绑定在一起,即使listenfd这个用于网络通讯的文件描述符监听serverAddr所描述的地址和端口号 if( bind(listenfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) { perror("bind error"); exit(-1); } //监听 int ret = listen(listenfd, 5); if(ret < 0) { perror("listen error"); exit(-1); } //在内核中创建事件表 int epfd = epoll_create(EPOLL_SIZE); if(epfd < 0){ perror("epfd error"); exit(-1); } printf("epoll created, epoll size = %d\n", epfd); static struct epoll_event events[EPOLL_SIZE]; //往内核事件表里添加事件 addfd(epfd, listenfd, true); //主循环 while(1){ int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1); if(epoll_events_count < 0){ perror("epoll failure"); break; } printf("epoll event counts = %d\n", epoll_events_count); for(int i = 0; i < epoll_events_count; i++){ int sockfd = events[i].data.fd; if(sockfd == listenfd){ struct sockaddr_in client_address; socklen_t client_addrLength = sizeof(struct sockaddr_in); int clientfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrLength); printf("client connection from: %s : % d(IP : port), clientfd = %d \n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port), clientfd); addfd(epfd, clientfd, true); //服务端用list保存用户连接 clients_list.push_back(clientfd); printf("Add new clientfd = %d to epoll\n", clientfd); printf("Now there are %d clients int the chat room\n", (int)clients_list.size()); //服务端发送欢迎消息 char message[BUF_SIZE]; bzero(message, BUF_SIZE); sprintf(message, SERVER_WELCOME, clientfd); int ret = send(clientfd, message, BUF_SIZE, 0); if(ret < 0){ perror("error"); exit(-1); } } else{ int ret = sendBroadcastmessage(sockfd); if(ret < 0){ perror("error"); exit(-1); } } } } close(listenfd); //关闭socket close(epfd); //关闭内核 return 0; }
Client.cpp
//client.cpp #include <iostream> #include <list> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> using namespace std; // server port #define SERVER_PORT 8888 //epoll 支持的最大并发量 #define EPOLL_SIZE 5000 //message buffer size #define BUF_SIZE 0xFFFF // exit #define EXIT "EXIT" //设置sockfd,pipefd非阻塞 int setnonblocking(int sockfd){ fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK); return 0; } int addfd(int epollfd, int fd, bool enable_et){ struct epoll_event ev; ev.data.fd = fd; ev.events = EPOLLIN; //输入出发epoll-event if(enable_et){ ev.events = EPOLLIN | EPOLLET; } epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev); setnonblocking(fd); } int main(int argc, char* argv[]){ //用户连接的服务器IP、端口 struct sockaddr_in serverAddr; serverAddr.sin_family = PF_INET; serverAddr.sin_port = htons(SERVER_PORT); const char* servInetAddr = "127.0.0.1"; inet_pton(AF_INET, servInetAddr, &serverAddr.sin_addr); //创建socket int sock = socket(PF_INET, SOCK_STREAM, 0); if(sock < 0){ perror("sock error"); exit(-1); } // 连接服务端 if(connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0){ perror("connect error"); exit(-1); } //创建管道,其中fd[0]用于父进程读,fd[1]用于子进程写 int pipe_fd[2]; if(pipe(pipe_fd) < 0){ perror("pipe error"); exit(-1); } // 1 创建epoll int epfd = epoll_create(EPOLL_SIZE); if(epfd < 0) { perror("epfd error"); exit(-1); } static struct epoll_event events[2]; //将sock和管道读端都加到内核事件表中 addfd(epfd, sock, true); addfd(epfd, pipe_fd[0], true); // 表示客户端是否正常工作 bool isClientwork = true; // 聊天信息缓冲区 char message[BUF_SIZE]; //Fork int pid = fork(); if(pid < 0) { perror("fork error"); exit(-1); } else if(pid == 0){ //子进程 //子进程负责写入管道,因此先关闭读端 close(pipe_fd[0]); printf("Please input 'exit' to exit the chat room\n"); while(isClientwork){ bzero(&message, BUF_SIZE); fgets(message, BUF_SIZE, stdin); //客户端输出exit,退出 if(strncasecmp(message, EXIT, strlen(EXIT)) == 0){ isClientwork = 0; }else{ //子进程将信息写入管道 if(write(pipe_fd[1], message, strlen(message) - 1) < 0){ { perror("fork error"); exit(-1); } } } } }else{ //pid > 0 父进程 //父进程负责读管道数据,因此先关闭写端 close(pipe_fd[1]); while(isClientwork){ int epoll_events_count = epoll_wait(epfd, events, 2, -1); //处理就绪事件 for(int i = 0; i < epoll_events_count; i++){ bzero(&message, BUF_SIZE); //服务端发来消息 if(events[i].data.fd == sock){ //接受服务端消息 int ret = recv(sock, message, BUF_SIZE, 0); //ret = 0 服务端关闭 if(ret == 0){ printf("Server closed connection: %d\n", sock); close(sock); isClientwork = 0; }else{ printf("%s\n", message); } }else{ //子进程写入事件发生,父进程处理并发送数据 int ret = read(events[i].data.fd, message, BUF_SIZE); if(ret = 0){ isClientwork = 0; }else{ send(sock, message, BUF_SIZE, 0); } } } } } if(pid){ close(pipe_fd[1]); close(sock); }else{ close(pipe_fd[0]); } return 0; }