Linux 网络编程——IO Multiplexing之select, poll, epoll详解
上一章节Linux 网络编程——多进程,多线程模型已经介绍了两种服务器并发模型,但其并发量受进程/线程数量限制。这一章节,我们将介绍三种IO多路复用模型,可以实现单进程监听多个网络IO事件。
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
SELECT模型
Common APIint select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);void FD_ZERO(fd_set *set); clear the setvoid FD_CLR(int fd, fd_set *set); remove a given file descriptor from the setint FD_ISSET(int fd, fd_set *set); tests to see if a file descriptor is part of the setvoid FD_SET(int fd, fd_set *set); add a given file descriptor from the set
SELECT模型服务器demo
#include <arpa/inet.h> // 大小端转换 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <sys/socket.h> // socket套接字 #include <unistd.h> #define _SERVER_PORT 8080 #define BACK_LOG 128 #define IP_SIZE 16 #define BUFFER_SIZE 1600 int server_net_init(void) { struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(_SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置本机任意IP // inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr); // //转换并设置自定义IP int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind Error"); exit(0); } listen(server_fd, BACK_LOG); printf("TCP Server Waiting for Connect\n"); return server_fd; } int server_recv_response(int sockfd) { char recv_buffer[BUFFER_SIZE]; bzero(recv_buffer, BUFFER_SIZE); ssize_t recvlen; if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) { printf("%s", recv_buffer); send(sockfd, recv_buffer, recvlen, 0); bzero(recv_buffer, BUFFER_SIZE); } if (recvlen == -1) { perror("recv error"); exit(-1); } else if (recvlen == 0) { printf("client exit\n"); return -1; } return 0; } void select_starting(int sockfd) { int client_fd; struct sockaddr_in client_addr; socklen_t addrlen; int readycode; // 就绪量 fd_set nset, oset; // nset传入,oset传出 int clientfd_array[1021]; // 客户端sock数组 int maxfd; // 最大的描述符 char client_ip[IP_SIZE]; bzero(client_ip, IP_SIZE); // 初始化 maxfd = sockfd; FD_ZERO(&nset); FD_SET(sockfd, &nset); for (int i = 0; i < 1021; i++) clientfd_array[i] = -1; printf("Select Server Running ...\n"); while (1) { oset = nset; readycode = select(maxfd + 1, &oset, NULL, NULL, NULL); // 阻塞监听sock读取事件 while (readycode) { // 循环处理就绪事件 // 辨别就绪 if (FD_ISSET(sockfd, &oset)) { addrlen = sizeof(client_addr); if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen)) > 0) { printf( "TCP Server Accept Success:client ip[%s] client " "prot[%d] \n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, IP_SIZE), ntohs(client_addr.sin_port)); if (client_fd > maxfd) maxfd = client_fd; FD_SET(client_fd, &nset); for (int i = 0; i < 1021; i++) if (clientfd_array[i] == -1) { clientfd_array[i] = client_fd; break; } FD_CLR(sockfd, &oset); } else { perror("accept error"); exit(0); } } else { // clientfd就绪 for (int i = 0; i < 1021; i++) if (clientfd_array[i] != -1) if (FD_ISSET(clientfd_array[i], &oset)) { if (server_recv_response(clientfd_array[i]) == -1) { FD_CLR(clientfd_array[i], &nset); clientfd_array[i] = -1; } FD_CLR(sockfd, &oset); break; } } --readycode; } } } int main(void) { int sfd = server_net_init(); // 服务端网络初始化 select_starting(sfd); // 阻塞等到连接 close(sfd); return 0; }
SELECT模型的优点:
- SELECT模型采用IO 复用技术,业务处理交替执行,可以仅用单进程实现一对多处理效果
- 每个平台语言都对select有兼容和实现,便于移植
- SELECT模型定时阻塞支持微秒级别,有较高的时间精度
SELECT模型的缺点:
- 监听最大数为1024,不能满足高并发需求。
- SELECT监听采用轮询模式,随着轮询的数量增多,IO处理能力呈线性下降。
- SELECT对于sockfd的事件监听是批处理的,如果你希望不同的sockfd监听不同的网络事件,SELECT无法实现。
- 在退出时,SELECT对每个文件描述符集进行适当的修改,以指示哪些文件描述符实际上已更改状态。因此,如果在循环中使用select(),则必须在此之前重新初始化这些集。
- 单进程限制,阻塞问题导致任务无法继续,单进程模型不允许业务处理时间过长。
- SELECT模型在使用的时候会出现大量的无意义的重复开销,浪费系统资源(每次往set监听集合增加监听sockfd描述符时,select()都会将整个监听集合拷贝到内核层,并且将集合的监听项挂载到监听设备,无论以前是否挂载过)
POLL模型
#include <poll.h>
用户自定义长度结构体数组作为POLL监听集合
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
API:
int poll(struct pollfd * fds , nfds_t nfds , int timeout ); //timeout(工作模式):-1 阻塞监听 >0 定时阻塞 0非阻塞
返回值:成功时,poll () 返回一个非负值,该值是pollfds中其revents字段已设置为非零值(指示事件或错误)的元素数。返回值为0表示系统调用在任何文件描述符被读取之前超时。出错时返回 -1,并设置 errno以指示错误。
POLL模型服务器demo
#include <arpa/inet.h> // 大小端转换 #include <poll.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> // socket套接字 #include <unistd.h> #define _SERVER_PORT 8080 #define BACK_LOG 128 #define IP_SIZE 16 #define BUFFER_SIZE 1600 int server_net_init(void) { struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(_SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置本机任意IP // inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr); // //转换并设置自定义IP int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind Error"); exit(0); } listen(server_fd, BACK_LOG); printf("TCP Server Waiting for Connect\n"); return server_fd; } int server_recv_response(int sockfd) { char recv_buffer[BUFFER_SIZE]; bzero(recv_buffer, BUFFER_SIZE); ssize_t recvlen; if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) { printf("%s", recv_buffer); send(sockfd, recv_buffer, recvlen, 0); bzero(recv_buffer, BUFFER_SIZE); } if (recvlen == -1) { perror("recv error"); exit(-1); } else if (recvlen == 0) { printf("client exit\n"); return -1; } return 0; } // poll模型 void poll_starting(int sockfd) { int readycode; // 就绪量 struct pollfd listen_array[1024]; // POLL监听集合 int client_fd; struct sockaddr_in client_addr; socklen_t addrlen; char client_ip[IP_SIZE]; bzero(client_ip, IP_SIZE); // init listen_array[0].fd = sockfd; listen_array[0].events = POLLIN; for (int i = 1; i < 1024; i++) { listen_array[i].fd = -1; listen_array[i].events = POLLIN; } printf("POLL Server Running ...\n"); while (1) { readycode = poll(listen_array, 1024, -1); // 阻塞监听sockfd读取事件 while (readycode) { // 循环处理就绪事件 if (listen_array[0].revents == POLLIN) { // server_fd就绪 addrlen = sizeof(client_addr); if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen)) > 0) { printf( "TCP Server Accept Success:client ip[%s] client " "prot[%d] \n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, IP_SIZE), ntohs(client_addr.sin_port)); for (int i = 1; i < 1024; i++) { if (listen_array[i].fd == -1) { listen_array[i].fd = client_fd; break; } } listen_array[0].revents = 0; } else { perror("accept error"); exit(0); } } else { // client_fd就绪 for (int i = 1; i < 1024; i++) { if (listen_array[i].fd != -1) { if (listen_array[i].revents == POLLIN) { if (server_recv_response(listen_array[i].fd) == -1) { close(listen_array[i].fd); listen_array[i].fd = -1; } listen_array[i].revents = 0; break; } } } } --readycode; } } } int main(void) { int sfd = server_net_init(); // 服务端网络初始化 poll_starting(sfd); // 阻塞等到连接 close(sfd); return 0; }
POLL模型优点:
- 针对sockfd的监听设置更为灵活,可以针对不同的sockfd设置不同的监听项,相比select可以监听的事件更为丰富,选择更多
- poll监听大小没有select模型的1024的数量限制,但是poll采用的依然是轮询监听机制
- poll采用events和revents两个参数将传入传出分离
POLL模型的缺点和select一致
EPOLL模型
EPOLL模型服务器demo
#include <arpa/inet.h> // 大小端转换 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/epoll.h> #include <sys/socket.h> // socket套接字 #include <unistd.h> #define _SERVER_PORT 8080 #define BACK_LOG 128 #define IP_SIZE 16 #define BUFFER_SIZE 1600 #define EPOLLSIZE 1e5 int server_net_init(void) { struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(_SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //设置本机任意IP // inet_pton(AF_INET, _SERVER_IP, &server_addr.sin_addr.s_addr); // //转换并设置自定义IP int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("bind Error"); exit(0); } listen(server_fd, BACK_LOG); printf("TCP Server Waiting for Connect\n"); return server_fd; } int server_recv_response(int sockfd) { char recv_buffer[BUFFER_SIZE]; bzero(recv_buffer, BUFFER_SIZE); ssize_t recvlen; if ((recvlen = recv(sockfd, recv_buffer, sizeof(recv_buffer), 0)) > 0) { printf("%s", recv_buffer); // 回传 send(sockfd, recv_buffer, recvlen, 0); bzero(recv_buffer, BUFFER_SIZE); } if (recvlen == -1) { perror("recv error"); exit(-1); } else if (recvlen == 0) { printf("client exit ...\n"); return -1; } return 0; } // epoll模型 void epoll_starting( int sockfd) { // 文件描述符, 默认情况下进程打开的最大文件描述符数量为1024 int client_fd; struct sockaddr_in client_addr; socklen_t addrlen; char client_ip[IP_SIZE]; bzero(client_ip, IP_SIZE); int readycode; // 就绪量 struct epoll_event ready_array[(int)EPOLLSIZE]; // 就绪等待队列 // init server_fd struct epoll_event node; node.data.fd = sockfd; node.events = EPOLLIN; //创建监听树 int epfd = epoll_create(EPOLLSIZE); int i; // 第一次添加节点, 添加server_fd epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &node); printf("EPOLL Server Running ...\n"); while (1) { readycode = epoll_wait(epfd, ready_array, EPOLLSIZE, -1); // 阻塞监听sock读取事件 while (readycode) { // 循环处理就绪事件 i = 0; // 辨别就绪 if (ready_array[i].data.fd == sockfd) { addrlen = sizeof(client_addr); if ((client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen)) > 0) { printf( "TCP Server Accept Success:client ip[%s] client " "prot[%d] \n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, IP_SIZE), ntohs(client_addr.sin_port)); node.data.fd = client_fd; node.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &node); } else { perror("accept error"); exit(0); } } else { // clientfd就绪 if ((server_recv_response(ready_array[i].data.fd)) == -1) { close(ready_array[i].data.fd); // 关闭连接 epoll_ctl(epfd, EPOLL_CTL_DEL, ready_array[i].data.fd, NULL); // 删除监听节点 } } --readycode; ++i; } } } int main(void) { int sfd = server_net_init(); // 服务端网络初始化 epoll_starting(sfd); // 阻塞等到连接 close(sfd); return 0; }
EPOLL模型相比其他模型的优势:
- epoll模型没有轮询限制,理论上epoll可以监听系统上最大的描述符数量,也不会有多余的开销
- epollm没有多余的拷贝开销和挂载开销,保证每个监听节点只拷贝一次挂载一次
- epoll不仅会返回监听数量还会返回就绪节点,用户直接处理即可
- epoll设置监听比较灵活,可以对不同的sock设置不同的事件监听,可监听的事件也比较丰富
EPOLL监听能力较为出色,但是处理能力而言与select,poll没有区别
posted on 2022-05-01 11:08 SocialistYouth 阅读(124) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人