多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
主要使用的方法有三种:
select
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
- 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
函数原型:
//select函数是用来监视一个或多个文件句柄的状态变化的,可阻塞也可不阻塞。 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0 int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1 void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1 void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
参数:
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生到达文件描述符集合,传入传出参数
timeout: 定时阻塞监控时间,3种情况:
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
返回值:
- -1:发生错误,并将所有描述符集合清0,可通过errno输出错误详情。
- 0:超时。
- 正数:发生变化的文件描述符数量。
select实现tcp服务端:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include "wrap.h" #define SERV_PORT 6666 int main(int argc, const char *argv[]) { int i, j, n, maxi; int nready, client[FD_SETSIZE]; int maxfd, listenfd, connfd, sockfd; int buf[BUFSIZ], str[INET_ADDRSTRLEN]; struct sockaddr_in clie_addr, serv_addr; socklen_t clie_addr_len; fd_set rset, allset; listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); Listen(listenfd, 20); maxfd = listenfd //起初listenfd为最大文件描述符 maxi = -1; for(i=0; i<FD_SETSIZE; i++) client[i] = -1; FD_ZERO(&allset); FD_SET(listenfd, &allset); //把服务端socket文件描述符加入监控文件描述符集合中 while(1){ rset = allset; nready = select(maxfd+1, &rset, NULL, NULL, NULL); //阻塞监听读事件集合 if(nready < 0) perr_exit("select error"); //判断listenfd是否在rset中,如果在表示有新的客户端链接请求 if(FD_ISSET(listenfd, &rset)){ clie_addr_len = sizeof(clie_addr); connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len); //立即连上客户端,不会阻塞 printf("received from %s at PORT %d ", inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port)); for(i=0; i<FD_SETSIZE; i++){ if(client[i] < 0){ client[i] = connfd; break; } } if(i == FD_SETSIZE){ //达到select能监控的文件描述符上限(1024) fputs("too many clients ", stderr); exit(1); } FD_SET(connfd, &allset); //把客户端文件描述符加入监控文件描述符集合中 if(connfd > maxfd) maxfd = connfd; if(i > maxi) maxi = i; if(--nready == 0) continue; } for(i=0; i<=maxi; i++){ //检测哪个client有数据就绪 if((sockfd = client[i]) < 0) continue; if(FD_ISSET(sockfd, &rset)){ if((n = Read(sockfd, buf, sizeof(buf))) == 0){ //当client关闭连接时,服务端也关闭对应连接 Close(sockfd); FD_CLR(sockfd, &allset); //解除select对此文件描述符的监控 } else if(n > 0){ for(j=0; j<n; j++) buf[j] = toupper(buf[j]); sleep(10); Write(sockfd, buf, n); } if(--nready == 0) break; } } } }
poll
函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd { int fd; /* 文件描述符 */ short events; /* 监控的事件 */ short revents; /* 监控事件中满足条件返回的事件 */ };
功能:
监视并等待多个文件描述符的属性变化
参数:
fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件
nfds:要监视的描述符的数目
timeout:毫秒级等待
-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
0:立即返回,不阻塞进程
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
events:
POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级可读数据
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
返回值:
- -1:发生错误,并将所有描述符集合清0,可通过errno输出错误详情。
- 0:超时。
- 正数:发生变化的文件描述符数量。
epoll
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
基础API
1. 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。
#include <sys/epoll.h> int epoll_create(int size) size:监听数目
2. 控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) epfd: 为epoll_creat的句柄 op: 表示动作,用3个宏来表示: EPOLL_CTL_ADD (注册新的fd到epfd), EPOLL_CTL_MOD (修改已经注册的fd的监听事件), EPOLL_CTL_DEL (从epfd删除一个fd); event: 告诉内核需要监听的事件 struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) EPOLLOUT: 表示对应的文件描述符可以写 EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) EPOLLERR: 表示对应的文件描述符发生错误 EPOLLHUP: 表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. 等待所监控文件描述符上有事件的产生,类似于select()调用。
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) events: 用来存内核得到事件的集合, maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size, timeout: 是超时时间 -1: 阻塞 0: 立即返回,非阻塞 >0: 指定毫秒 返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
epoll实现server端
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include <errno.h> #include <sys/epoll.h> #include "wrap.h" #define SERV_PORT 6666 #define MAXLINE 8192 #define OPEN_MAX 5000 int main(int argc, const char *argv[]) { int i, n, num = 0; int listenfd, connfd, sockfd; ssize_t nready, efd, res; int buf[BUFSIZ], str[INET_ADDRSTRLEN]; struct sockaddr_in clie_addr, serv_addr; socklen_t clie_addr_len; struct epoll_event tep, ep[open_max]; listenfd = Socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用 bzero(&serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); Bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); Listen(listenfd, 20); efd = epoll_create(OPEN_MAX); //创建epoll模型,efd指向红黑树根节点 if(efd == -1) perr_exit("epoll create error"); tep.events = EPOLLIN; //指定lfd的监听时间为读 tep.data.fd = listend; res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); //将lfd及对应的结构体设置到树上,efd可找到该树 if(res == -1) perr_exit("epoll_ctl error"); while(1){ //epoll为server阻塞监听事件,ep为struct epoll_event类型数组,OPEN_MAX为数组容量,-1表示永久阻塞 nready = epoll_wait(efd, ep, OPEN_MAX, -1); //阻塞监听读事件集合 if(nready == 1) perr_exit("epoll_wait error"); //判断listenfd是否在rset中,如果在表示有新的客户端链接请求 for(i=0; i<nready; i++){ if(!(ep[i].events & EPOLLIN)) continue; //如果不是读事件就跳过 if(ep[i].data.fd == listenfd){ //判断满足事件的fd是不是lfd clie_addr_len = sizeof(clie_addr); connfd = Accept(listenfd, (struct sockaddr*)&clie_addr, &clie_addr_len); //立即连上客户端,不会阻塞 printf("received from %s at PORT %d ", inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port)); tep.events = EPOLLIN; //指定lfd的监听时间为读 tep.data.fd = listend; res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); //将lfd及对应的结构体设置到树上,efd可找到该树 if(res == -1) perr_exit("epoll_ctl error"); } else{ sockfd = ep[i].data.fd; n = Read(sockfd, buf, MAXLINE); if(n == 0){ //0表示客户端关闭连接 res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //将该文件描述符从红黑树中摘除 if(res == -1) perr_exit("epoll_ctl error"); Close(sockfd); } else if(n < 0){ perror("read error"); res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); Close(sockfd); } else{ for(i=0; i<n; i++) buf[i] = toupper(buf[i]); Write(sockfd, buf, n); Write(STDOUT_FILENO, buf, n); } } } } }