多路I/O共用一个线程或进程
- 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到该表中.
- 然后调用一个函数,监听该表中的文件描述符,直到这些描述符表中的一个进行I/O操作时,该函数返回. (该函数为阻塞函数) (函数对文件描述符的检测操作是由内核完成的)
- 在返回时,它告诉进程有多少(或哪些)文件描述符要进行I/O操作.
此篇的服务器端也可配合上一篇的客户端测试
select
/**
* @param nfds 要检测的文件描述符中最大的fd + 1 (1024)
* @param readfds 读集合
* @param writefds 写集合
* @param exceptfds 异常集合
* @param timeout
* @return I/O发生变化的文件描述符的个数
* NULL 永久阻塞,直到检测到fd引用的对象发生变化时
* == 0 不阻塞
* > 0 设置多少,阻塞多久
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 文件描述符集类型
// 文件描述符操作函数
// // // // // // //
// 全部清空
void FD_ZERO(fd_set *set);
// 从集合中删除某一项
void FD_CLR(int fd, fd_set *set);
// 将某个文件描述符添加到集合
void FD_SET(int fd, fd_set *set);
// 判断某个文件描述符是否在集合中
int FD_ISSET(int fd, fd_set *set);
struct timeval
{
long tv_sec;
long tv_usec;
};
/**
* @name server.c
* @author IYATT-yx
* @brief select I/O多路复用
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdbool.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
int main(void)
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket error");
exit(EXIT_FAILURE);
}
struct sockaddr_in server;
socklen_t serverLen = sizeof(server);
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(11111);
int flag = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
if (bind(lfd, (struct sockaddr *)&server, serverLen) == -1)
{
perror("bind error");
exit(EXIT_FAILURE);
}
if (listen(lfd, 20) == -1)
{
perror("listen error");
exit(EXIT_FAILURE);
}
printf("服务器启动监听...\n");
struct sockaddr_in client;
socklen_t clientLen = sizeof(client);
// 最大的文件描述符
int maxfd = lfd;
// 文件描述符读集合
fd_set read, temp;
// 初始化
FD_ZERO(&read);
FD_SET(lfd, &read);
// 保存建立连接的IP和端口, 状态码
char ipbuf[1024][64];
uint16_t port[1024];
bool status[1024] = {false};
while (true)
{
// 让内核做I/O检测
temp = read;
if (select(maxfd + 1, &temp, NULL, NULL, NULL) == -1)
{
perror("select error");
exit(EXIT_FAILURE);
}
// 有客户端发起了新连接
if (FD_ISSET(lfd, &temp))
{
// 接收连接
int cfd = accept(lfd, (struct sockaddr *)&client, &clientLen);
if (cfd == -1)
{
perror("accept error");
exit(EXIT_FAILURE);
}
// 将cfd加入下一次待检测的读集合中
FD_SET(cfd, &read);
// 更新最大的文件描述符
maxfd = maxfd > cfd ? maxfd : cfd;
}
// 已经连接的客户端有数据送达
for (int i = lfd + 1; i <= maxfd; ++i)
{
if (FD_ISSET(i, &temp))
{
char buf[1024] = {0};
ssize_t len = recv(i, buf, sizeof(buf), 0);
if (len == -1)
{
perror("recv error");
exit(EXIT_FAILURE);
}
else if (len == 0)
{
printf("客户端%s:%d断开了连接\n", ipbuf[i], port[i]);
close(i);
status[i] = false;
// 从读集合中删除
FD_CLR(i, &read);
}
else
{
if (status[i] == false)
{
inet_ntop(AF_INET, &client.sin_addr.s_addr, ipbuf[i], sizeof(ipbuf[0]));
port[i] = ntohs(client.sin_port);
status[i] = true;
}
printf("收到来自%s:%d消息: %s\n", ipbuf[i], port[i], buf);
memset(buf, 0, sizeof(buf));
strcpy(buf, "服务器已收到数据\n");
send(i, buf, strlen(buf) + 1, 0);
}
}
}
}
close(lfd);
}
poll
/**
* @param fds 数组的地址
* @param nfds 数组的最大长度 (数组中最后使用的一个元素的下标 + 1)
* @param timeout
* -1 永久阻塞
* 0 调完后立即返回
* >0 阻塞时常 (ms)
* @return I/O发生变化的文件描述符的个数
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd
{
// 文件描述符
int fd;
// 等待的事件
short events;
// 实际发生的事件
short revents;
};
事件 | 常值 | 作为events的值 | 作为revents的值 | 说明 |
读事件 | POLLIN | √ | √ | 普通或优先带数据可读 |
POLLRDNORM | √ | √ | 普通数据可读 | |
POLLRDBAND | √ | √ | 优先级带数据可读 | |
POLLPRI | √ | √ | 高优先级数据可读 | |
写事件 | POLLOUT | √ | √ | 普通或优先带数据可读 |
POLLWRNORM | √ | √ | 普通数据可写 | |
POLLWRBAND | √ | √ | 优先级带数据可写 | |
错误事件 | POLLERR | √ | 发生错误 | |
POLLHUP | √ | 发生挂起 | ||
POLLNVAL | √ | 描述不是打开的文件 |
epoll
/**
* @brief 创建句柄
* @param size epoll关注的最大文件描述符数
*/
int epoll_create(int size);
/**
* @brief 事件注册
* @param epfd 前一个函数生成的epoll专用文件描述符
* @param op
* EPOLL_CTL_ADD 注册
* EPOLL_CTL_MOD 修改
* EPOLL_CTL_DEL 删除
* @param fd 关联的文件描述符
* @param event 告诉内核要监听什么事件
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event
{
uint32_t events;
epoll_data_t data;
};
// events:
// EPOLLIN 读
// EPOLLOUT 写
// EPOLLERR 异常
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
/**
* @brief 等待I/O事件产生
* @param epfd 要检测的句柄
* @param events 用于回传待处理事件的数组
* @param maxevents 告诉内核这个events的大小
* @param timeout
* -1 永久阻塞
* 0 立即返回
* >0 阻塞的时间 (ms)
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/**
* @name server.c
* @author IYATT-yx
* @brief epoll I/O多路复用
*/
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
// 自定义数据结构用于用于存放连入的客户端的IP和端口信息以及对应的文件描述符
typedef struct info
{
int fd;
char ipbuf[64];
uint16_t port;
} info;
int main(void)
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket error");
exit(EXIT_FAILURE);
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(11111);
socklen_t serverLen = sizeof(server);
int flag = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
if (bind(lfd, (struct sockaddr *)&server, serverLen) == -1)
{
perror("bind error");
exit(EXIT_FAILURE);
}
if (listen(lfd, 20) == -1)
{
perror("listen error");
exit(EXIT_FAILURE);
}
printf("服务器开始监听...\n");
struct sockaddr_in client;
socklen_t clientLen = sizeof(client);
// 创建epoll树根节点
int epfd = epoll_create(1024);
// 初始化epoll树
struct epoll_event ev;
struct info ino;
ev.events = EPOLLIN;
ino.fd = lfd;
ev.data.ptr = &ino;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev) == -1)
{
perror("初始化epoll_ctl error");
exit(EXIT_FAILURE);
}
struct epoll_event all[1024];
while (true)
{
// 使用epoll通知内核
int ret = epoll_wait(epfd, all, 1024, -1);
if (ret == -1)
{
perror("epoll_wait error");
exit(EXIT_FAILURE);
}
// 遍历all数组的前ret个元素
for (int i = 0; i < ret; ++i)
{
info *inf = (info *)all[i].data.ptr;
// 判断是否有新的连接
if (inf->fd == lfd)
{
// 接收连接请求
int cfd = accept(lfd, (struct sockaddr *)&client, &clientLen);
if (cfd == -1)
{
perror("accept error");
exit(EXIT_FAILURE);
}
// 将新得到的cfd, 对应的IP和端口挂到epoll树上
info *infTemp = (info *)malloc(sizeof(inf));
infTemp->fd = cfd;
inet_ntop(AF_INET, &client.sin_addr.s_addr, infTemp->ipbuf, sizeof(infTemp->ipbuf));
infTemp->port = ntohs(client.sin_port);
struct epoll_event eventTemp;
eventTemp.events = EPOLLIN;
eventTemp.data.ptr = infTemp;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &eventTemp) == -1)
{
perror("添加epoll_ctl error");
exit(EXIT_FAILURE);
}
}
else
{
// 处理已连接的客户端发送过来的数据
if (!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[1024] = {0};
ssize_t len = recv(inf->fd, buf, sizeof(buf), 0);
if (len == -1)
{
perror("recv error");
exit(EXIT_FAILURE);
}
else if (len == 0)
{
printf("客户端%s:%d断开了连接\n", inf->ipbuf, inf->port);
if (epoll_ctl(epfd, EPOLL_CTL_DEL, inf->fd, NULL) == -1)
{
perror("删除epoll_ctl error");
exit(EXIT_FAILURE);
}
close(inf->fd);
}
else
{
printf("收到%s:%d的信息: %s\n", inf->ipbuf, inf->port, buf);
memset(buf, 0, sizeof(buf));
strcpy(buf, "服务器已收到消息");
send(inf->fd, buf, strlen(buf) + 1, 0);
}
}
}
}
close(lfd);
}
水平触发模式 (默认) | fd的缓冲区有数据就返回,与数据发送次数无关 |
边沿触发模式 EPOLLET | 发送一次就返回一次,不管数据是否读完 |
边沿非阻塞触发模式 | 在边沿触发模式上,对文件描述符设置非阻塞 |