网络编程基础(6):多路复用技术
- 1.select/poll/epoll三种方案的使用&区别
- 2.I/O多路复用,结合多线程编程的技术手段
基本概念:
什么叫I/O多路复用?作用是什么?
所谓I/O多路复用,是指单一I/O为不同的客户服务,实现同一进程,同时监听多个文件描述符。
其作用是,不需要为每一个客户单独建立新的处理进程,节省大量的资源。
一:select
作用:在一段指定的时间内,监听用户感兴趣的文件描述符上的事件(包括:可读,可写,异常等)
具体使用中的API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <sys/select.h> int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
|
具体使用时的处理逻辑
select函数返回正整数说明,有描述符就绪。因为其内部机制是,当调用完成后,监听的位如果发生变化,那么依然置为1,未发生变化,置为0.所以对经过select监听的集合进行遍历,依然为1的部分就是就绪的描述符。
下面观察一个Linux下采用I/O复用的逻辑代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| int main(int argc, char *argv[]) { fd_set reads; FD_ZERO(&reads); FD_SET(serv_sock, &reads); fd_max = serv_sock; while (1) { cpy_reads = reads; timeout.tv_sec = 5; timeout.tv_usec = 5000; fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout); for { if (FD_ISSET(i, &cpy_reads)) { if (i == serv_sock) { accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); FD_SET(clnt_sock, &reads); } else { if (str_len == 0) { FD_CLR(i, &reads); } else { } } } } } close(serv_sock); return 0; }
|
分析其过程:
先定义监听集合,初始化集合,注册集合, 监听变化,判断变化类型, 处理变化内容。重新更新监听集合,继续监听…….
二:poll
核心API
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct pollfd{ int fd; short events; short revents; };
#include <poll.h> int poll(struct pollfd* fds, nfds_t nfds, int timeout);
|
三:epoll
是Linux中特有的I/O复用技术,采用一组函数来设计实现多路复用的功能。
具体实现方案
1 2 3 4 5 6
| #include <sys/epoll.h> int epoll_creat(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);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
#include <sys/epoll.h>
int epoll_creat(int size);
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event event; event.events= EPOLLIN; event.data.fd= sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);
struct epoll_event { __uint32_t events; epoll_data_t data; }
|
具体的实现案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| #include<stdio.h> #include<unistd.h> #include<string.h> #include<stdlib.h> #include<sys/select.h> #include<sys/socket.h> #include<arpa/inet.h> #include<sys/epoll.h> #define BUF_SIZE 30 void error_handling(char* message); int main(int argc,char* argv[]) { int serv_sock,clnt_sock; struct sockaddr_in serv_addr,clnt_addr; int clnt_addr_sz; int epoll_fd; struct epoll_event event; struct epoll_event* pevents;
int fd_num,i; int str_len; char buf[BUF_SIZE]; if(argc!=2) { printf("Uasge %s <port>\n",argv[0]); exit(1); } serv_sock=socket(AF_INET,SOCK_STREAM,0); memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family=AF_INET; serv_addr.sin_addr.s_addr=htonl(INADDR_ANY); serv_addr.sin_port=htons(atoi(argv[1])); if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) { error_handling("bind error"); } if(listen(serv_sock,5)==-1) { error_handling("listen error"); } epoll_fd=epoll_create(50); pevents=malloc(sizeof(struct epoll_event)*50); event.events=EPOLLIN; event.data.fd=serv_sock; epoll_ctl(epoll_fd,EPOLL_CTL_ADD,serv_sock,&event); while(1) { fd_num=epoll_wait(epoll_fd,pevents,50,-1); puts("wait succeed"); for(i=0;i<fd_num;i++) { if(serv_sock==pevents[i].data.fd) { clnt_addr_sz=sizeof(clnt_addr); clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_addr_sz); puts("accept succeed"); event.events=EPOLLIN; event.data.fd=clnt_sock; epoll_ctl(epoll_fd,EPOLL_CTL_ADD,clnt_sock,&event); } else { str_len=read(pevents[i].data.fd,buf,BUF_SIZE); if(str_len==0) { epoll_ctl(epoll_fd,EPOLL_CTL_DEL,pevents[i].data.fd,NULL); close(pevents[i].data.fd); } else { printf("client:%s\n",buf); write(pevents[i].data.fd,buf,str_len); } } } } close(epoll_fd); close(serv_sock); return 0; } void error_handling(char* message) { fputs(message,stderr); fputc('\n',stderr); exit(1); }
|
LT&&ET的区别
在epoll中,对文件描述符有两种操作方式:水平触发&边沿触发。
采用LT的描述符,epoll_wait检测到有事件发生,可以暂时不用处理,因为还会重复提醒,直到该事件被处理
采用ET的描述符,只会提醒一次,相对来说,效率会高很多。
注意:每一个使用ET模式的文件描述符都应该是非阻塞的,因为:如果文件描述符阻塞,那么读或者写操作会因为没有后续的事件而一直阻塞。
四:结合多线程编程聊EPOLLONESHOT
设想一个场景:
采用多线程(进程)编程时,某一个线程在读取完某个socket数据后开始处理这个数据,而在处理数据的时候该socket上又有了新的数据可读,此时另一个线程被唤醒处理读取这些数据。于是会出现,两个线程同时操作一个socket的情景。这个可能会造成额外的线程竞争,应该尽量避免。采取EPOLLONESHOT事件来避免这种情况。
当注册了EPOLLONESHOT事件,操作系统最多为其触发一次。之后再想触发则需要使用epoll_cnt函数来重置事件,这样可以避免多个线程同时处理一个套接字。同时,我们也要注意到在利用这个特性时,当某一线程处理完事件之后,应该立即重置,这样以后才可以在新事件发生时被处理。
五:三种I/O复用的对比
需要注意的一点:
因为回调相对而言是要花费更多资源的,当活动连接比较多的时候,epoll_wait的效率未必高很多。epoll适用于连接数量多,但活动连接较少的情况。