多路 io 复用最大的意义就是将多个阻塞转换成一个阻塞
可以实现同时监测多个文件描述符的异动
包括 可写性 可读性 出错
TCP 的服务器的代码里就有多个阻塞造成程序执行不正常情况
在 TCP 的服务端代码里共有两类套接字
分别是用来处理客户端的链接请求的套接字
通过 socket 函数创建而来
还有专门用来跟客户端通讯的套接字
通过 accept 函数得到
这两类套接字对应的函数
read和accept 都会产生阻塞
并且这两个阻塞会互相影响
想要解决这个问题可以考虑将多个阻塞转换成一个阻塞
当 socket 函数创建的文件描述符产生了可读性表示有新的客户端来链接
这个时候再去调用 accept 函数
当用来通讯的套接字产生了可读性表示有客户端发送了通讯的消息
这个时候再去调用 read/recv 函数
这种解决的方法叫做多路 io 复用
共有三种手段
select
poll
epoll
select 这种多路 io 复用的方法
是将所有的要监测的文件描述符放入到一张表里
当表里的文件描述符产生了异动之后
select 函数会马上解除阻塞,会将所有的没有产生异动的文件描述符清空
我们就可以通过特定的宏查看哪个文件描述符还在表里
找到文件描述符以后再根据文件描述符的不同采用不同的动作
函数的头文件
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
函数的参数
int nfds, 监测的文件描述符里最大的那个描述符+1
fd_set *readfds, 可读表
fd_set *writefds, 可写表
fd_set *exceptfds, 出错表
struct timeval *timeout 超时时间 NULL 表示永久等待
函数的返回值
失败返回 -1
超时返回 0
成功返回 产生异动的文件描述符的个数
将文件描述符从表里删除
void FD_CLR(int fd, fd_set *set);
判断哪个文件描述符还在表里
int FD_ISSET(int fd, fd_set *set);
将要监测的文件描述符放入到表里
void FD_SET(int fd, fd_set *set);
清空当前表
void FD_ZERO(fd_set *set);
PS:由于表里的文件描述符产生异动了以后会将没有产生异动的文件描述符清除,所以在使用 select 这种多路 io 复用的时候,一般是创建两个一样的表,表 一个用来备份, 一个用来使用
#include
#include
#include
#include
#include
#include
#include
#include
#include
int MAX,i;
int sfd,cfd,ret;
struct sockaddr_in seraddr;//服务端结构体
struct sockaddr_in cliaddr;//连接结构体
socklen_t len=sizeof(cliaddr);
fd_set list,lock;
char buf[100]={0};
int main()
{
sfd=socket(AF_INET,SOCK_STREAM,0);//1.创建套接字
if(sfd < 0)
{
perror("socket");
return -1;
}
seraddr.sin_family=AF_INET;//地址类型IPV4
seraddr.sin_port=htons(4444);//端口号//转换小端到大端
seraddr.sin_addr.s_addr=inet_addr("192.168.39.201");//将小端IP转换为大端IP
ret= bind(sfd, (struct sockaddr *)&seraddr,sizeof(seraddr));//2.绑定 ip 端口信息 服务端自身IP
if(ret < 0)
{
perror("bind");
return -1;
}
//3.监听网络
ret = listen(sfd,10);
if(ret < 0)
{
perror("listen");
return -1;
}
MAX=sfd;//刚开始最大描述符为sfd
FD_ZERO(&lock);//清空lock
FD_SET(sfd,&lock);//把sfd写入lock
while(1)
{
list=lock;
select(MAX+1,&list,NULL,NULL,NULL);//阻塞进行
for(i=sfd;i MAX)
{
MAX=cfd;
}
}
else//不是sfd 就是有人发信息 - read
{
if(read(i,buf,100)==0)//读信息
{
printf("客户端下线\n");
FD_CLR(i,&lock);//清除文件描述符
}
else
{
printf("buf=%s\n",buf);//打印发送的数据
for(int j=sfd+1;j
poll 轮询将所有的要监测的文件描述符放入到一个结构体数组里
这个结构体数组里存放的有:
要监测的文件描述符 要监测的事件的类型 监测到的事件的类型
struct pollfd {
int fd; 要监测的文件描述符
short events; 要监测的事件的类型
POLLIN 可读
POLLOUT 可写
POLLERR 出错
short revents; 监测到的事件的类型
};
当要监测的文件描述符产生了异动以后 系统会将产生的异动的类型
对应的宏填入到结构体里的 revents 这个成员里
我们就可以遍历整个的结构体数组 找到 revents 有变化的那个套接字
通过套接字的类型判断接下来要调用的函数
函数的头文件
include
函数的原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数的参数
struct pollfd *fds, 要监测的文件描述符存放的位置
nfds_t nfds, 要监测的文件描述符的个数
int timeout 超时时间 -1 表示永久等待
函数的返回值
失败返回 -1
超时返回 0
有异动产生 返回产生异动的文件描述符的个数
#include
#include
#include
#include
#include
#include
#include
#include
int i, j, count;
int sfd, cfd, ret;
struct sockaddr_in seraddr; // 定义服务器地址结构体
struct sockaddr_in cliaddr; // 定义客户端地址结构体
socklen_t len = sizeof(cliaddr); // 定义客户端地址结构体的长度
char buf[100] = {0}; // 定义缓冲区,用于存储接收的数据
struct pollfd myfd[50]; // 定义 pollfd 结构体数组,用于存储文件描述符和事件信息
int main()
{
sfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (sfd < 0)
{
perror("socket");
return -1;
}
seraddr.sin_family = AF_INET; // 设置地址类型为 IPv4
seraddr.sin_port = htons(4444); // 设置端口号为 4444
seraddr.sin_addr.s_addr = inet_addr("192.168.39.201"); // 设置服务器 IP 地址
ret = bind(sfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); // 绑定套接字到地址
if (ret < 0)
{
perror("bind");
return -1;
}
ret = listen(sfd, 10); // 设置套接字为监听状态
if (ret < 0)
{
perror("listen");
return -1;
}
myfd[0].fd = sfd; // 将服务器套接字加入 pollfd 数组
myfd[0].events = POLLIN; // 设置监听事件为可读事件
count = 1; // 初始化计数器
while (1)
{
ret = poll(myfd, count, -1); // 调用 poll 函数,阻塞等待事件发生
if (ret < 0) // 检查 poll 函数是否出错
{
perror("poll");
continue;
}
for (i = 0; i < count; i++)
{
if (myfd[i].revents & POLLIN) // 检查是否有可读事件
{
myfd[i].revents = 0; // 清除事件标志
if (myfd[i].fd == sfd) // 如果是服务器套接字
{
cfd = accept(sfd, (struct sockaddr *)&cliaddr, &len); // 接受客户端连接
if (cfd < 0)
{
perror("accept");
continue;
}
printf("新的套接字产生: %d\n", cfd);
// 检查是否超出数组范围
if (count >= sizeof(myfd) / sizeof(myfd[0]))
{
printf("连接数达到上限,拒绝新的连接\n");
close(cfd); // 关闭新的连接
continue;
}
myfd[count].fd = cfd; // 将新连接的套接字加入 pollfd 数组
myfd[count].events = POLLIN; // 设置监听事件为可读事件
count++;
}
else // 如果是客户端套接字
{
memset(buf, 0, sizeof(buf)); // 清零缓冲区
ret = read(myfd[i].fd, buf, sizeof(buf)); // 读取数据
if (ret == 0) // 客户端断开连接
{
printf("客户端下线:%d\n", myfd[i].fd);
close(myfd[i].fd); // 关闭文件描述符
for (j = i; j < count - 1; j++) // 移除该文件描述符
{
myfd[j] = myfd[j + 1];
}
count--; // 减少计数器
}
else if (ret < 0) // 读取失败
{
perror("read");
close(myfd[i].fd); // 关闭文件描述符
for (j = i; j < count - 1; j++) // 移除该文件描述符
{
myfd[j] = myfd[j + 1];
}
count--; // 减少计数器
}
else // 成功读取数据
{
printf("buf=%s\n", buf);
}
}
}
}
}
return 0;
}
epoll 是 poll 和 select 的一个加强版
epoll 将所有的要监测的文件描述符放到一个文件里,当要监测的文件描述符产生异动以后会将所有的产生异动的文件描述符的信息放到一个结构体数组
函数的功能
创建 epoll 的文件描述符
函数的头文件
#include
函数的原型
int epoll_create(int size);
函数的参数参数
int size:大于 1 的值
函数的返回值
成功返回 epoll 的文件描述符
失败返回 -1
函数的头文件
#include
函数的原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数的参数
int epfd, epoll 的文件描述符
int op, 要执行的操作
EPOLL_CTL_ADD 添加文件描述符到 epoll
EPOLL_CTL_DEL 删除文件描述符
int fd, 要操作的对象 文件描述符
struct epoll_event *event 文件描述符的信息结构体
函数的返回值
成功返回 0
失败返回 -1
函数的功能
轮询 epoll
函数的头文件
#include
函数的原型
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
函数的参数
int epfd,
epoll 的文件描述符
struct epoll_event *events, 用来存放产生异动的文件描述符的结构体
int maxevents, 最大产生异动的文件描述符的个数
int timeout -1 永久等待
函数的返回值
失败返回 -1
超时返回 0
有异动返回产生异动的个数
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int sfd,cfd,ret,epid,count,i;
struct sockaddr_in seraddr;// 定义服务器地址结构体
struct sockaddr_in cliaddr;// 定义客户端地址结构体
socklen_t len=sizeof(cliaddr);// 定义客户端地址结构体的长度
char buf[100]={0};// 定义缓冲区,用于存储接收的数据
struct epoll_event myev[10];// 定义epoll事件数组
struct epoll_event ev;// 定义epoll事件结构体
int main()
{
sfd=socket(AF_INET,SOCK_STREAM,0);//1.创建套接字
if(sfd < 0)
{
perror("socket");
return -1;
}
seraddr.sin_family=AF_INET;//地址类型IPV4
seraddr.sin_port=htons(4444);// 端口号为4444,转换为网络字节序
seraddr.sin_addr.s_addr=inet_addr("192.168.39.201");//将小端IP转换为大端IP
ret= bind(sfd, (struct sockaddr *)&seraddr,sizeof(seraddr));//2.绑定 ip 端口信息 服务端自身IP
if(ret < 0)
{
perror("bind");
return -1;
}
//3.监听网络
ret = listen(sfd,10);
if(ret < 0)
{
perror("listen");
return -1;
}
epid = epoll_create(1);// 创建epoll实例
ev.events = EPOLLIN;// 设置epoll事件类型为可读事件
ev.data.fd=sfd;// 将监听套接字加入epoll
epoll_ctl(epid,EPOLL_CTL_ADD,sfd,&ev);
while(1)
{
count = epoll_wait(epid,myev,10,-1); // 等待epoll事件,最多处理10
for(i=0;i