Linux 网络编程 day5 多路IO转接之改进select and poll
三种多路IO转接方法:select , poll , epoll
改进select多路IO转接,使用数组来保存含有需要连接的套接字cfd,不用循环至1024,节约时间提高效率。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#define SER_PORT 9003
void sys_err(char* s)
{perror(s);exit(1);
}int main(int argc , char *argv[])
{int cfd , lfd , maxi;int client[FD_SETSIZE] ;char buf[BUFSIZ] , str[INET_ADDRSTRLEN];struct sockaddr_in serv_addr , clit_addr;socklen_t clit_addr_len;bzero(&serv_addr , sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(SER_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);lfd = socket(AF_INET , SOCK_STREAM , 0);if(lfd == -1)sys_err("socket error");int opt = 1;setsockopt(lfd , SOL_SOCKET , SO_REUSEADDR , (void*)&opt , sizeof(opt));int ret = bind(lfd , (struct sockaddr*)&serv_addr , sizeof(serv_addr));if(ret == -1)sys_err("bind error");listen(lfd , 128);fd_set rset , allset;int maxfd , i , k , n ,j , m , sockfd;maxfd = lfd;maxi = -1;for(j = 0 ; j < 1024 ; j++)client[j] = -1;FD_ZERO(&allset);FD_SET(lfd , &allset);while(1){rset = allset;clit_addr_len = sizeof(clit_addr);ret = select(maxfd + 1 , &rset , NULL , NULL , NULL);if(ret < 0){sys_err("select error");}else if(ret > 0){if(FD_ISSET(lfd , &rset)){cfd = accept(lfd , (struct sockaddr*)&clit_addr , &clit_addr_len);if(cfd == -1)sys_err("accept error");printf("--received from %s at port %d\n" , inet_ntop(AF_INET , &clit_addr.sin_addr.s_addr , str , sizeof(str)) , ntohs(clit_addr.sin_port));for( m = 0 ; m < 1024 ;m++){if(client[m] < 0){client[m] = cfd;break;}}if( m == 1023){printf("too many clients\n");exit(1);}if(m > maxi)maxi = m;FD_SET(cfd , &allset);if(maxfd < cfd)maxfd = cfd ;if(ret == 1)continue;}for(i = lfd + 1 ; i <= maxi ; i++){if((sockfd = client[i]) < 0 )continue;if(FD_ISSET(sockfd , &rset)){n = read(sockfd , buf , sizeof(buf));if(n == 0){close(i);FD_CLR(sockfd , &allset);client[i] = -1;}else if( n > 0){for( k = 0 ; k < n; k++){buf[k] = toupper(buf[k]);}write(STDOUT_FILENO , buf , n);write(sockfd , buf , n);}else{sys_err("read error");}if(ret == 1)break;}}}}close(cfd);return 0 ;
}
poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);fds:监听的文件描述符数组struct pollfd {int fd; //监听的文件描述符short events; //待监听的文件描述符对应的监听事件取值events : 读POLLIN , 写POLLOUT , 错误POLLERRshort revents; //传入时,给0,满足对应事件会返回非0-->上述取值};nfds:监听数组的实际有效监听个数
timeout:超时时长,毫秒级等待
-1:阻塞等
0:立即返回,不阻塞进程
>0:等待指定毫秒数返回值:成功返回满足对应监听事件的文件描述符总个数初始化
struct pollfd pfds[1024];
pfds[0].fd = lfd;
pfds[0].events = POLLIN;
pfds[0].revents = 0;
read函数返回值
>0:实际读到字节数
=0:socket中,表示对端关闭。close()
<0:如果errno == EINTR 被异常中断,需要重启 。
如果errno == EAGIN/EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。需要再次读
如果errno == ECONNRSET 说明连接被重置 需要close , 移除监听队列。
poll优缺点
优点:
1、自带数组结构,可以将 监听事件 和 返回事件 分离。
2、拓展监听上限。select无法修改,除非重新编译内核。
缺点:
1、不能跨平台。只有Linux和类Unix
2、无法直接定位满足监听事件的文件描述符,编码难度较大
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<poll.h>
#define SER_PORT 9003void sys_err(char* s)
{perror(s);exit(1);
}int main(int argc , char* argv[])
{int cfd , lfd , sockfd;char buf[BUFSIZ] , str[INET_ADDRSTRLEN];struct sockaddr_in serv_addr , clit_addr ;bzero(&serv_addr , sizeof(serv_addr));socklen_t clit_addr_len;lfd = socket(AF_INET , SOCK_STREAM , 0);if(lfd == -1)sys_err("socket error");int opt = 1 ;setsockopt(lfd , SOL_SOCKET , SO_REUSEADDR , (void*)&opt , sizeof(opt));int ret = bind(lfd , (struct sockaddr*)&serv_addr , sizeof(serv_addr));if(ret == -1)sys_err("bind error");listen(lfd , 128);struct pollfd pfds[1024];int maxi = lfd;pfds[0].fd = lfd;pfds[0].events = POLLIN;int i , n ,k;while(1){ret = poll(pfds , maxi + 1 , -1);if(ret < 0){sys_err("poll error");}else if(ret > 0){if(pfds[0].revents & POLLIN){clit_addr_len = sizeof(clit_addr);cfd = accept(lfd , (struct sockaddr*)&clit_addr , &clit_addr_len);if(cfd == -1)sys_err("accept error");printf("received from %s at PORT %d\n" ,inet_ntop(AF_INET , &clit_addr.sin_addr.s_addr , str , sizeof(str)),ntohs(clit_addr.sin_port));for(i = 0 ; i < 1024 ; i++){if(pfds[i].fd<0){pfds[i].fd = cfd;break;}}if(i == 1024){printf("too many clients\n");exit(1);}if(maxi < i)maxi = i;if(ret == 1)continue;}for(i = 1 ; i <= maxi ; i++){sockfd = pfds[i].fd;if(sockfd < 0)continue;if(pfds[i].revents & POLLIN){n = read(sockfd , buf ,sizeof(buf));if(n == 0){close(sockfd);pfds[i].fd = -1;}else if(n > 0){for(k = 0 ; k < n ; k++){buf[k] = toupper(buf[k]);}write(sockfd , buf , n);write(STDOUT_FILENO , buf , n);}if(ret == 1)break;}}}}return 0 ;
}
突破1024文件描述符限制
cat /proc/sys/fs/file-max -->当前计算机所能打开的最大文件个数,受硬件影响。
ulimit -a -->当前用户下的进程,默认打开文件描述符个数。1024
修改: sudo vi /etc/security/limits.conf 修改soft 基本不用
* soft nofile 65536 ---> 修改默认值,可直接借助命令修改:ulimit -n 数字(注销用户生效)
* hard nofile 100000 ---> 命令修改上限
epoll实现多路IO转接(重点)
本质是一个平衡二叉树,又是其特例红黑树。
epoll_create
创建一颗监听红黑树
int epoll_create(int size);
size:创建的红黑树的监听节点数量(仅供内核参考)。 返回值:成功返回指向新创建的红黑树的根节点的文件描述符fd
失败-1 ,errno
epoll_ctl(重点)
操作监听红黑树
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create函数的返回值
op:对该监听红黑树所做的操作EPOLL_CTL_ADD:添加fd到监听红黑树EPOLL_CTL_MOD:修改fd在监听红黑树的监听事件EPOLL_CTL_DEL:将一个fd从监听红黑树上摘下(取消监听)
fd:待监听的fd
event:本质是struct epoll_event类型的结构体 地址events:EPOLLIN EPOLLOUT EPOLLERRdata:联合体共用体int fd; 对应监听事件的fdvoid *ptr;uint32_t u32;uint32_t u64; 返回值 成功0 , 失败-1 errno
epoll_wait
阻塞监听
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll_create函数的返回值
events:传出参数【数组】,传出满足监听条件文件描述符结构体
maxevents:数组元素总个数 1024struct epoll_event events[1024];
timeout:超时时长,毫秒级等待
-1:阻塞等
0:立即返回,不阻塞进程
>0:等待指定毫秒数返回值:>0 满足监听的总个数,可以用作循环上限。
=0 , 没有fd满足监听事件
-1 , 失败 errno
流程
lfd = socket();
bind()
listen()int epfd = epoll_create(1024); //epfd,监听红黑树的树根struct epoll_event tep , ep[1024]; //tep用来设置单个fd的属性,ep是epoll_wait函数传出的满足监听事件的数组
tep.events = EPOLLIN;
tep.data.fd = lfd;epoll_ctl(epfd , EPOLL_CTL_ADD , lfd , &tep); //将lfd添加到监听红黑树上while(1){int ret = epoll_wait(epfd , ep , 1024 , -1); //实施监听for(i = 0 ; i < ret ; i++){ if(ep[i].data.fd == lfd){cfd = accept();tep.events = EPOLLIN;tep.data.fd = cfd;epoll_ctl(epfd , EPOLL_CTL_ADD , cfd , &tep);}else{ n = read(); if(n == 0){close();epoll_ctl(epfd , EPOLL_CTL_ADD , cfd , NULL); //摘除最后一个不需要初始化了,直接传NULL。}else if(n>0){小-->大write();}}}
使用epoll进行多路IO转接代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<ctype.h>
#define SER_PORT 9004
void sys_err(char* s)
{perror(s);exit(1);
}int main(int argc , char *argv[])
{int lfd , cfd ,epfd , sockfd , i , n , kfd , k;char buf[BUFSIZ],str[INET_ADDRSTRLEN];struct sockaddr_in serv_addr , clit_addr;socklen_t clit_addr_len;bzero(&serv_addr , sizeof(serv_addr));serv_addr.sin_family = AF_INET ;serv_addr.sin_port = htons(SER_PORT);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);lfd = socket(AF_INET , SOCK_STREAM , 0);if(lfd == -1)sys_err("socket error");int opt = 1;setsockopt(lfd , SOL_SOCKET , SO_REUSEADDR , (void*)&opt , sizeof(opt));int ret = bind(lfd , (struct sockaddr*)&serv_addr , sizeof(serv_addr));if(ret == -1)sys_err("bind error");listen(lfd , 128);epfd = epoll_create(1024);if(epfd == -1)sys_err("epoll_create error");struct epoll_event tep , ep[1024];tep.events = EPOLLIN;tep.data.fd = lfd;ret = epoll_ctl(epfd , EPOLL_CTL_ADD , lfd , &tep);if(ret == -1)sys_err("epoll_ctl error");while(1){kfd = epoll_wait(epfd , ep , 1024 , -1);if(kfd == -1 ){sys_err("epoll_wait error");}else if(kfd > 0){for(i = 0 ; i < kfd ; i++){sockfd = ep[i].data.fd;if(sockfd == lfd){clit_addr_len = sizeof(clit_addr);cfd = accept(lfd,(struct sockaddr*)&clit_addr, &clit_addr_len);if(cfd == -1)sys_err("accept error");printf("------received from %s at port %d------\n" ,inet_ntop(AF_INET, &clit_addr.sin_addr, str , sizeof(str)),ntohs(clit_addr.sin_port));tep.events = EPOLLIN;tep.data.fd = cfd;ret = epoll_ctl(epfd , EPOLL_CTL_ADD , cfd , &tep);if(ret == -1)sys_err("epoll_ctl error");}else{n = read(sockfd , buf , sizeof(buf));if(n == 0){close(sockfd);epoll_ctl(epfd , EPOLL_CTL_DEL , sockfd , NULL);}else if(n > 0){for(k = 0 ; k < n ; k++){buf[k] = toupper(buf[k]);}write(sockfd , buf , n);write(STDOUT_FILENO , buf , n);}else{sys_err("read error");}}}}}return 0 ;
}
感觉epoll有些东西容易和poll弄混,黑马老师说可以只记epoll,在之后基本都是使用到epoll进行多路IO转接。
epoll实现多路IO转接主要是使用3个函数,第一个函数是epoll_create(1024),参数创建的红黑树的监听节点数量(仅供内核参考)。 第二个函数是epoll_ctl(epfd , EPOLL_CTL_ADD/MOD/DEL , lfd/cfd , &tep) , tep初始化是 struct epoll_event tep ; tep.events = EPOLLIN/OUT/ERR; tep.data.fd = lfd/cfd; 将lfd挂到红黑树上之后进行while循环,进行监听,kfd = epoll_wait(epfd , ep , 1024 , -1); ep是结构体数组,初始化为struct epoll_event ep[1024];
随后只需要根据下标取出epoll_wait函数的传出参数ep数组中的元素进行判断。是否是lfd或者是cfd,如果是lfd说明有要连接的客户端,进行accept连接,若是cfd,则说明有客户端进行写操作,服务器则进行相应的读操作以及小写转大写的操作即可。