alin的学习之路(Linux网络编程:四)(多路IO转接:select、poll、epoll)
2020-08-02 21:11
351 查看
alin的学习之路(Linux网络编程:四)(多路IO转接:select、poll、epoll)
1. select
1. select优化思路
当select转接的文件描述符跨度过大时,每次都遍历文件描述符表显然会降低效率,因此要对其优化。
优化思路:添加一个数组来存储要监听的文件描述符,直接遍历这个数组即可
程序流程:
- 创建监听套接字lfd = Socket()
- 设置端口复用 setsockopt()
- Bind() 绑定ip和端口号
- Listen()设置最大监听个数
- 定义fd_set rset,allset; rset传出,allset用于存所有需要监听的文件描述符
- FD_SET(lfd,&allset);
- 定义一个存最大文件描述符的变量,暂时等于lfd, maxfd = lfd;
- 定义一个 client[1024] 数组存放所有需要监听的文件描述符
- 将 client 数组初始化为全 -1 ,-1代表没有文件描述符。定义一个maxindex为client数组的最大有效文件描述符下标
- while(1){
- rset = allset 初始化rset为全部要监听的文件描述符
- int nread = select(maxfd+1,&rset,NULL,NULL,NULL); 阻塞等待监听的文件描述符中有读事件产生
- if(FD_ISSET(lfd,&rset))
-
说明有客户端请求接入,cfd = Accept() ,将 cfd 添加到client数组中的第一个不为-1的位置,并更新maxindex。
- 然后判断 nread 如果等于1 的话,continue;
- else
-
不为 lfd 即为 cfd 有读事件,遍历client数组,循环到最大下标maxindex,数组值为-1时continue。
- 判断 cfd 是否在rset中,使用FD_ISSET(client[i]),如果存在的话调用Read()
- Read()返回值为0:客户端退出,FD_CLR(client[i],&allset); ,client[i] 置为 -1,Close(client[i])
- Read() 返回值大于0:小写转大写,Write()
- }
- Close(lfd);
2. 代码实现
#include "wrap.h" #include <sys/select.h> #include <strings.h> #include <ctype.h> #define SRV_PORT 6669 #define OPEN_MAX 1024 int main() { int cfd,lfd,ret; struct sockaddr_in srv_addr,clt_addr; srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(SRV_PORT); srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); socklen_t clt_addr_len; char buf[BUFSIZ] = {0}; char cltip[16]; lfd = Socket(AF_INET,SOCK_STREAM,0); int opt = 1; ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt)); if(-1 == ret) sys_err("setsockopt error"); Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr)); Listen(lfd,128); int maxfd = lfd; int maxindex = 0; fd_set rset,allset; FD_ZERO(&rset); FD_ZERO(&allset); FD_SET(lfd,&allset); int client[OPEN_MAX]; for(int i=0 ;i<OPEN_MAX ;++i) { client[i] = -1; } int nread; while(1) { rset = allset; nread = select(maxfd+1,&rset,NULL,NULL,NULL); if(-1 == ret) sys_err("select error"); if(nread == 0) continue; if(FD_ISSET(lfd,&rset)) //有新客户端接入 { clt_addr_len = sizeof(clt_addr); cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len); printf("客户端ip:%s,port:%d 接入\n", inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,cltip,sizeof(cltip)), ntohs(clt_addr.sin_port)); int i; //将新文件描述符添加到数组中 for(i=0 ;i<OPEN_MAX; ++i) { if(client[i] == -1) { client[i] = cfd; if(maxindex < i) maxindex = i; if(maxfd < cfd) maxfd = cfd; //将新文件描述符添加到allset FD_SET(cfd,&allset); break; } } if(i == OPEN_MAX) { printf("too much fd\n"); exit(1); } } else // 客户端通信 { for(int i = 0 ;i <= maxindex ;++i) { if(client[i] != -1){ int sockfd = client[i]; if(FD_ISSET(sockfd,&rset)) //判断如果在rset中,通信 { ret = Read(sockfd,buf,sizeof(buf)); if(0 == ret) { FD_CLR(sockfd,&allset); client[i] = -1; Close(sockfd); } else if(ret > 0) { for(int j = 0;j < ret ;++j) { buf[j] = toupper(buf[j]); } Write(sockfd,buf,ret); Write(STDOUT_FILENO,buf,ret); } } } } } } Close(lfd); return 0; }
3. select 函数优缺点
- 优点: 跨平台。Windows、Linux、MacOS、Unix、类Unix
-
监听上限受文件描述符限制。 最大 1204
4. 同步和异步的区分
同步:阻塞
异步:不阻塞
2. 突破文件描述符1024最大打开(Ubuntu 18.04以后)
-
查看 当前 Linux 系统所能打开的 最大文件个数 —— 受硬件影响。
cat /proc/sys/fs/file-max
-
获取当前 用户下的进程, 默认打开的文件描述符个数。 —— 默认值 1024
-
使用命令
ulimit -a
itcast@ubuntu:~/classcode/itcode/4day/select_server$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 3472 max locked memory (kbytes, -l) 65536 max memory size (kbytes, -m) unlimited open files (-n) 65536 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 3472 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
-
sudo vi /etc/security/limits.conf 打开文件, 添加设置。 不能超过 cat /proc/sys/fs/file-
max 命令查询结果。* soft nofile 65536 和 * hard nofile 65536
-
sudo vi /etc/systemd/user.conf 打开文件, 添加设置
DefaultLimitNOFILE=65535
-
sudo vi /etc/systemd/system.conf 打开文件, 添加设置
DefaultLimitNOFILE=65535
-
必须重启 Linux 系统,使配置文件生效。
-
ulimit -a 查看到 修改后的 默认打开的文件数
-
ulimit -n 4096。 设置的临时数量 < 65535 即可生效。
2. poll
1.相关函数
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); fds:【数组】监听的文件描述符 struct pollfd { int fd; // 待监听的文件描述符 short events; // 待监听的文件描述符的监听事件 POLLIN、 POLLOUT、 POLLERR short revents; // 传入时,赋值为0。 poll 函数返回后,如果满足对应事件, // 该成员变量的值变为非0。 POLLIN、 POLLOUT、POLLERR }; nfds: 监听数组的实际有效的监听个数。————不是数组的容量。 timeout: > 0: 超时时长。 单位:毫秒。 -1: 阻塞等。 0: 不阻塞。 返回值: 成功:返回满足对应监听事件的文件描述符 【总个数】 失败:-1, errno
2. 实现流程
3. poll优缺点
- 优点: 自带数组结构。 可以将 “监听事件集合” 和 “满足监听事件的集合” 分离。
- 可以 拓展监听上限。 超出 1024。
-
不能跨平台。Linux
3. Read函数返回值
> 0: 实际读到的字节数 = 0: socket 中 对端关闭。close() - 1: 如果 errno == EINTR 被异常中断。 需要重启。 如果 errno == EAGAIN 或 EWOULDBLOCK 以非阻塞方式读数据, 但没有数据。 需要,再次读。 如果 errno == ECONNRESET。 说明连接被重置。 需要 close,移除出监听队列。 其余为 异常。
4. epoll
1. 相关函数
-
epoll_create()
#include <sys/epoll.h> // 创建一棵监听红黑树 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 函数的返回值。epfd 参数op:对监听红黑树所做的操作。 EPOLL_CTL_ADD: 添加 fd 到监听红黑树。(设置监听) EPOLL_CTL_MOD: 修改 fd 在监听红黑树上的监听事件。 EPOLL_CTL_DEL: 将一个fd从监听红黑树上摘下。(取消监听) 参数fd:待监听的 fd。 参数event:【不是数组】本质是 struct epoll_event 结构体变量的地址。----传入参数。 成员 events: EPOLLIN、 EPOLLOUT、 EPOLLERR 成员 data:联合体(共用体) int fd: 对应监听事件的 fd void *ptr: uint32_t u32: uint64_t u64: 返回值: 成功:0 失败:-1, errno
-
epoll_wait()
// 阻塞监听 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); epfd:epoll_create 函数的返回值。epfd events:传出参数【数组】,满足监听条件的那些 fd 结构体。 maxevents:数组元素的总个数。1024 举例:struct epoll_event events[1024]; timeout: > 0: 超时时间,单位:毫秒 -1:阻塞监听。 0:非阻塞。 返回值: > 0: 满足监听事件的文件描述符【总个数】。可以用作循环 events 数组的上限。 0:没有fd满足 -1:失败。 errno
2. 实现思路
- Socket()创建监听套接字
- 设置端口复用 setsockopt()
- Bind()绑定IP地址和端口号
- Listen() 设置最大监听数
- int epfd = epoll_create() 创建epoll树根节点
- struct epoll_event tep,ep[1024]; 创建一个中间量的epoll事件结构体,一个结构体数组。一个用于epoll_ctl()函数传参,一个用于epoll_wait()函数传参
- tep初始化:tep.events = EPOLLIN;tep.data.fd = lfd;
- epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&tep); 将lfd挂上ep树
- while(1){
- int ret = epoll_wait(epfd,ep,1024,-1); 监听ep树上的文件描述符
- for(int i=0 ;i<ret ;++i)
- if(ep[i].data.fd == lfd)
-
Accept() 连接客户端
- 使用epoll_ctl()将cfd挂上ep树
- else
-
非lfd即cfd ,Read()
- Read() 返回值等于0:从ep树上摘下该文件描述符,使用epoll_ctl(),第二个参数是EPOLL_CTL_DEL,Close()
- Read() 返回值大于0:小写转大写,Write()
- }
- Close(lfd);
3. 实现代码
#include "wrap.h" #include <strings.h> #include <ctype.h> #include <sys/epoll.h> #define SRV_PORT 6668 #define OPEN_MAX 1024 int main() { int lfd,cfd; int ret; char cltip[16]; char buf[BUFSIZ] = {0}; socklen_t clt_addr_len; struct sockaddr_in srv_addr,clt_addr; bzero(&clt_addr,sizeof(clt_addr)); bzero(&srv_addr,sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(SRV_PORT); srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); lfd = Socket(AF_INET,SOCK_STREAM,0); int opt = 1; ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt)); if(-1 == ret) { sys_err("setsockopt error"); } Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr)); Listen(lfd,128); struct epoll_event tep,ep[OPEN_MAX]; int epfd = epoll_create(OPEN_MAX); if(-1 == epfd) sys_err("epoll_create error"); tep.events = EPOLLIN; tep.data.fd = lfd; ret = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&tep); if(-1 == ret) sys_err("epoll_ctl error"); while(1) { int nread = epoll_wait(epfd,ep,OPEN_MAX,-1); if(-1 == nread) sys_err("epoll_wait error"); for(int i=0 ;i<nread ;++i) { if (!(ep[i].events & EPOLLIN)) //如果不是"读"事件, 继续循环 continue; int sockfd = ep[i].data.fd; if(sockfd == lfd) //有新的客户端接入 { clt_addr_len = sizeof(clt_addr); cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len); printf("客户端ip:%s,port:%d 接入\n", inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,cltip,sizeof(cltip)), ntohs(clt_addr.sin_port)); tep.events = EPOLLIN; tep.data.fd = cfd; ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&tep); if(-1 == ret) sys_err("epoll_ctl"); } else //与客户端通信 { ret = Read(sockfd,buf,sizeof(buf)); if(0 == ret) //客户端退出 { int n = epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL); if(-1 == n) sys_err("epoll_ctl error"); Close(sockfd); } else if(ret > 0) { for(int j = 0 ;j < ret ;++j) buf[j] = toupper(buf[j]); Write(sockfd,buf,ret); Write(STDOUT_FILENO,buf,ret); } } } } Close(lfd); return 0; }
相关文章推荐
- Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)
- Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)
- LINUX 网络编程---多路复用和信号驱动I/O(王德仙)2012-04-07 客户端和服务器端编写完成,明天开始学习poll 和epoll
- alin的学习之路(Linux网络编程:五)(epoll ET\LT模式、epoll反应堆模型)
- linux网络编程之socket(十三):epoll 系列函数简介、与select、poll 的区别
- Linux网络编程总结-多进程,多线程,select,poll,epoll,libevent
- 网络通信 --> IO多路复用之select、poll、epoll详解
- (转)Linux IO多路复用之epoll网络编程
- linux 网络编程 I/O复用 select,poll ,epoll
- (转)Linux IO多路复用之epoll网络编程
- alin的学习之路(Linux网络编程:八)(libevent库)
- Linux下多路复用IO接口epoll/select/poll的区别
- 网络通信 --> IO多路复用之select、poll、epoll详解
- (转)Linux IO多路复用之epoll网络编程
- Linux下多路复用IO接口epoll/select/poll的区别
- Linux IO多路复用之epoll网络编程
- [转载] Linux下多路复用IO接口 epoll select poll 的区别
- 【Linux网络编程】I/O多路转接之 epoll 高性能简洁http服务器模型
- 06-Linux网络编程-IO多路复用学习记录(华清)
- 多路IO转接模型select、poll、epoll,以及epoll的底层实现