Linux IO多路转接——select,poll,epoll
程序员文章站
2022-06-13 14:03:09
...
文章目录
一、IO操作方式
多路I/O转接服务器
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
阻塞等待
- 好处:不占用CPU宝贵时间
- 缺点:同一时间只能处理一个操作,效率低
非阻塞, 忙轮询
- 优点: 提高了程序的执行效率
- 缺点: 需要占用更多的cpu和系统资源
一个任务
多个任务
解决方案:
使用IO多路转接技术 select/poll/epoll
第一种: select/poll
select 代收员比较懒, 她只会告诉你有几个快递到了,但是哪个快递,你需要挨个遍历一遍。
select服务器代码
/*************************************************************************
> File Name: select_server.c
> Author: 杨永利
> Mail: aaa@qq.com
> Created Time: 2020年10月26日 星期一 10时36分26秒
************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
int main(int argc, char* argv[])
{
if(argc<2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len =sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd=socket(AF_INET,SOCK_STREAM,0);
// 初始化服务器 sockaddr_in
memset(&serv_addr,0,serv_len);
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port=htons(port);
// 绑定IP和端口
bind(lfd,(struct sockaddr*)&serv_addr,serv_len);
// 设置同时监听的最大个数
listen(lfd,36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len=sizeof(client_addr);
// 最大的文件描述符
int maxfd= lfd;
// 文件描述符读集合
fd_set reads, temp;
// init 初始化
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
// 委托内核做IO检测
temp =reads;
int ret=select(maxfd+1,&temp,NULL,NULL,NULL);
if(ret == -1)
{
perror("select error\n");
exit(1);
}
// 客户端发起了新的连接
if(FD_ISSET(lfd, &temp))
{
// 接受连接请求 - accept不阻塞
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
char ip[64];
printf("new client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
// 将cfd加入到待检测的读集合中 - 下一次就可以检测到了
FD_SET(cfd, &reads);
// 更新最大的文件描述符
maxfd = maxfd < cfd ? cfd : maxfd;
}
// 已经连接的客户端有数据到达
for(int i=lfd+1; i<=maxfd; ++i)
{
if(FD_ISSET(i, &temp))
{
char buf[1024] = {0};
int len = recv(i, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("客户端已经断开了连接\n");
close(i);
// 从读集合中删除
FD_CLR(i, &reads);
}
else
{
printf("recv buf: %s\n", buf);
send(i, buf, strlen(buf)+1, 0);
}
}
}
}
return 0;
}
poll服务器代码
/*************************************************************************
> File Name: poll.c
> Author: 杨永利
> Mail: aaa@qq.com
> Created Time: 2020年10月28日 星期三 11时51分23秒
************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <poll.h>
#define SERV_PORT 8989
int main(int argc, const char* argv[])
{
int lfd, cfd;
struct sockaddr_in serv_addr, clien_addr;
int serv_len, clien_len;
// 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(SERV_PORT); // 设置端口
serv_len = sizeof(serv_addr);
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
// poll结构体
struct pollfd allfd[1024];
int max_index = 0;
// init
for(int i=0; i<1024; ++i)
{
allfd[i].fd = -1;
}
allfd[0].fd = lfd;
allfd[0].events = POLLIN;
while(1)
{
int i = 0;
int ret = poll(allfd, max_index+1, -1);
if(ret == -1)
{
perror("poll error");
exit(1);
}
// 判断是否有连接请求
if(allfd[0].revents & POLLIN)
{
clien_len = sizeof(clien_addr);
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
printf("============\n");
// cfd添加到poll数组
for(i=0; i<1024; ++i)
{
if(allfd[i].fd == -1)
{
allfd[i].fd = cfd;
break;
}
}
// 更新最后一个元素的下标
max_index = max_index < i ? i : max_index;
}
// 遍历数组
for(i=1; i<=max_index; ++i)
{
int fd = allfd[i].fd;
if(fd == -1)
{
continue;
}
if(allfd[i].revents & POLLIN)
{
// 接受数据
char buf[1024] = {0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
allfd[i].fd = -1;
close(fd);
printf("客户端已经主动断开连接。。。\n");
}
else
{
printf("recv buf = %s\n", buf);
for(int k=0; k<len; ++k)
{
buf[k] = toupper(buf[k]);
}
printf("buf toupper: %s\n", buf);
send(fd, buf, strlen(buf)+1, 0);
}
}
}
}
close(lfd);
return 0;
}
客户端代码
/*************************************************************************
> File Name: client.c
> Author: 杨永利
> Mail: aaa@qq.com
> Created Time: 2020年10月27日 星期二 23时20分19秒
************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
int port = atoi(argv[1]);
// 创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 连接服务器
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(port);
// oserv.sin_addr.s_addr = htonl();
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
connect(fd, (struct sockaddr*)&serv, sizeof(serv) );
// 通信
while(1)
{
// 发送数据
char buf[1024];
printf("请输入要发送的字符串: \n");
fgets(buf, sizeof(buf), stdin);
write(fd, buf, strlen(buf));
// 等待接收数据
int len = read(fd, buf, sizeof(buf));
if(len == -1)
{
perror("read error");
exit(1);
}
else if(len == 0)
{
printf("服务器端关闭了连接\n");
break;
}
else
{
printf("recv buf: %s\n", buf);
}
}
close(fd);
return 0;
}
第二种: epoll
epoll代收快递员很勤快, 她不仅会告诉你有几个快递到了, 还会告诉你是哪个快递公司的快递
epoll服务器代码
/*************************************************************************
> File Name: epoll.c
> Author: 杨永利
> Mail: aaa@qq.com
> Created Time: 2020年10月28日 星期三 15时38分00秒
************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <sys/epoll.h>
int main(int argc, const char* argv[])
{
if(argc<2)
{
printf("eg: ./a.out port\n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
struct sockaddr_in client_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
struct epoll_event all[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd,all,sizeof(all)/sizeof(all[0]), -1 );
// 遍历all数组中的前ret个元素
for (int i = 0; i < ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd==lfd)
{
// 有就接守新连接
int cfd =accept(lfd,(struct sockaddr*)&client_addr,&cli_len );
if (cfd == -1)
{
perror("accept error\n");
exit(1);
/* code */
}
// 将新得到的cfd挂到树上
struct epoll_event temp;
temp.events =EPOLLIN;
temp.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&temp);
// 打印客户端信息
char ip[64]={0};
printf("New client IP :%s ,Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else // 没有新连接
{
// c处理已经连接进来的客户端发来的信息
if (!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[1024]={0};
int len = recv(fd,buf,sizeof(buf),0);
if (len==-1)
{
perror("recv error");
exit(1);
}
else if(len == 0)
{
printf("client disconnected ....\n");
close(fd);
// fd从epoll树上删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
else
{
printf(" recv buf: %s\n", buf);
write(fd, buf, len);
}
}
}
}
close(lfd);
return 0;
}
二. 什么是I/O多路转接技术:
-
先构造一张有关文件描述符的列表, 将要监听的文件描述符添加到该表中
-
然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数才返回。
- 该函数为阻塞函数
- 函数对文件描述符的检测操作是由内核完成的
-
在返回时,它告诉进程有多少(哪些)描述符要进行I/O操作。
推荐阅读
-
Python—IO多路复用之select模块详解(select、poll、epoll之间的区别)
-
IO多路复用(二) -- select、poll、epoll实现TCP反射程序
-
linux epoll,poll,select
-
IO复用之select poll epoll的总结(推荐)
-
Linux IO多路复用之epoll网络编程
-
IO多路复用select/poll/epoll详解以及在Python中的应用
-
linux进程间通信---本地socket套接字(五)---多路IO转接服务器实现一个server对应多个client---poll实现
-
I/O多路转接——select、poll 和 epoll
-
I/O多路转接之select poll epoll
-
高性能IO模型分析-浅析Select、Poll、Epoll机制(三)