通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪,程序的阻塞就会被解除,之后就可以基于这些就绪的文件描述符进行通信。通过这种方式在单线程 / 进程的场景下也可以在服务器端实现并发。
#include
struct timeval
{
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
int select(
int nfds, // 委托内核检测的下列三个集合中最大的文件描述符+1
// 内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
fd_set *readfds, // 传入传出参数,内核只检测该集合中文件描述符对应的读缓冲区
fd_set *writefds, // 传入传出参数,内核只检测该集合中文件描述符对应的写缓冲区
fd_set *exceptfds, // 传入传出参数,内核只检测该集合中文件描述符是否异常
struct timeval *timeout // 超时时长,用来强制解除函数阻塞
);
// 返回值:发生错误返回值为-1,超时返回值为0,成功返回就绪的文件描述符的个数。
// 将集合中所有的文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set);
// 将fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// fd对应的标志位是0还是1
int FD_ISSET(int fd, fd_set *set);
局限性:
优势:
// server.c
#include
#include
#include
#include
#include
int main()
{
// 1. 创建监听的文件描述符
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;
bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
// 3. 设置监听
listen(lfd, 128);
// 最大的文件描述符
int maxfd = lfd;
// 委托内核检测的读集合
fd_set rdset;
// 读事件就绪的文件描述符集合
fd_set rdtemp;
FD_ZERO(&rdset);
// 将用于监听的文件描述符设置到集合中
FD_SET(lfd, &rdset);
// 应该让内核持续检测
while (1)
{
rdtemp = rdset;
int num = select(maxfd + 1, &rdtemp, NULL, NULL, NULL);
// 检测用于监听的文件描述符
if (FD_ISSET(lfd, &rdtemp))
{
struct sockaddr_in cliaddr;
int cliLen = sizeof(cliaddr);
// 接受连接请求(不会阻塞)
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &cliLen);
// 将用于通信的文件描述符设置到集合中
FD_SET(cfd, &rdset);
// 重置最大的文件描述符
maxfd = cfd > maxfd ? cfd : maxfd;
}
// 检测用于通信的文件描述符
for (int i = 0; i < maxfd + 1; ++i)
{
if (i != lfd && FD_ISSET(i, &rdtemp))
{
// 接收数据
char buf[10] = {0};
int len = read(i, buf, sizeof(buf));
if (len == 0)
{
printf("客户端关闭了连接...\n");
// 将检测的文件描述符从读集合中删除
FD_CLR(i, &rdset);
close(i);
}
else if (len > 0)
{
// 发送接收到的数据
printf("客户端:%s\n", buf);
write(i, buf, strlen(buf) + 1);
}
else
{
// 异常
perror("read");
}
}
}
}
return 0;
}
struct pollfd
{
int fd; // 委托内核检测的文件描述符
short events; // 委托内核检测文件描述符的什么事件
short revents; // 传出参数,文件描述符实际发生的事件
};
int poll(
struct pollfd *fds, // 存储待检测的文件描述符的信息
nfds_t nfds, // 第一个参数数组中元素总个数
int timeout // 指定函数的阻塞时长
);
// 返回值:发生错误返回值为-1,超时返回值为0,成功返回就绪的文件描述符的个数。
局限性:
优势:
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 1.创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定
struct sockaddr_in addr;
addr.sin_port = htons(9999);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(0);
}
// 3. 监听
ret = listen(lfd, 100);
if (ret == -1)
{
perror("listen");
exit(0);
}
// 初始化
struct pollfd fds[1024];
for (int i = 0; i < 1024; ++i)
{
fds[i].fd = -1;
fds[i].events = POLLIN; // 读事件
}
fds[0].fd = lfd;
// 最大的文件描述符
int maxfd = 0;
// 应该让内核持续检测
while (1)
{
ret = poll(fds, maxfd + 1, -1);
if (ret == -1)
{
perror("poll");
exit(0);
}
// 监听的文件描述符
if (fds[0].revents & POLLIN)
{
struct sockaddr_in sockcli;
int len = sizeof(sockcli);
// 接收连接请求(不会阻塞)
int connfd = accept(lfd, (struct sockaddr *)&sockcli, &len);
// 找到第一个可用的文件描述符
for (int i = 0; i < 1024; i++)
{
if (fds[i].fd == -1)
{
// 将该文件描述符设置到检测集合
fds[i].fd = connfd;
break;
}
}
// 重置最大的文件描述符
maxfd = i > maxfd ? i : maxfd;
}
// 通信的文件描述符
for (int i = 1; i <= maxfd; i++)
{
if (fds[i].revents & POLLIN)
{
// 接收数据
char buf[128];
int ret = read(fds[i].fd, buf, sizeof(buf));
if (ret == -1)
{
// 异常
perror("read");
exit(0);
}
else if (ret == 0)
{
printf("对方已经关闭了连接。\n");
close(fds[i].fd);
// 将该文件描述符从集合中删除
fds[i].fd = -1;
}
else
{
// 发送接收到的数据
printf("客户端: %s\n", buf);
write(fds[i].fd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
return 0;
}
// 1.创建 epoll 实例
int epoll_create(int size);
// 返回值:失败返回-1,成功返回 epoll 实例的文件描述符。
// 联合体类型
typedef union epoll_data
{
void *ptr;
int fd; // 目标文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; // EPOLLIN:读事件
// EPOLLOUT:写事件
// EPOLLERR:异常事件
epoll_data_t data;
};
// 2.管理红黑树上的文件描述符
int epoll_ctl(
int epfd, // epoll 实例文件描述符
int op, // EPOLL_CTL_ADD:添加新节点
// EPOLL_CTL_MOD:修改已存在节点
// EPOLL_CTL_DEL:删除节点
int fd, // 目标文件描述符
struct epoll_event *event // 检测该文件描述符的事件
);
// 返回值:失败返回-1,成功返回0。
```c
// 3.检测红黑树中是否有就绪的文件描述符
int epoll_wait(
int epfd, // epoll 实例文件描述符
struct epoll_event *events, // 传出参数,结构体数组存储已就绪文件描述符的信息
int maxevents, // 结构体数组的容量
int timeout // 函数阻塞的时长
);
// 返回值:发生错误返回值为-1,超时返回值为0,成功返回就绪的文件描述符的个数。
优势:
检测到文件描述符有事件发生,则将其通知给应用程序,应用程序可以不立即处理该事件。当下一次检测时,还会再次向应用程序报告此事件,直至被处理。
// server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
// 创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket error");
exit(1);
}
// 绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret == -1)
{
perror("bind error");
exit(1);
}
// 监听
ret = listen(lfd, 64);
if (ret == -1)
{
perror("listen error");
exit(1);
}
// 创建一个epoll实例
int epfd = epoll_create(100);
if (epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 往epoll实例中添加监听文件描述符
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1)
{
perror("epoll_ctl");
exit(0);
}
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
// 应该让内核持续检测
while (1)
{
int num = epoll_wait(epfd, evs, size, -1); // 阻塞式
for (int i = 0; i < num; i++)
{
// 取出当前的文件描述符
int curfd = evs[i].data.fd;
// 用于监听的文件描述符
if (curfd == lfd)
{
// 建立新的连接
int cfd = accept(curfd, NULL, NULL);
// 添加到epoll实例
ev.events = EPOLLIN;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1)
{
perror("epoll_ctl-accept");
exit(0);
}
}
// 用于通信的文件描述符
else
{
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = recv(curfd, buf, sizeof(buf), 0);
if (len == 0)
{
printf("客户端已经断开了连接\n");
// 从epoll实例中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else if (len > 0)
{
printf("客户端: %s\n", buf);
send(curfd, buf, len, 0);
}
else
{
perror("recv");
exit(0);
}
}
}
}
return 0;
}
检测到文件描述符有事件发生,则将其通知给应用程序,应用程序必须立即处理该事件。必须要一次性将数据读取完,套接字默认是阻塞的,当读缓冲区数据被读完之后,读操作阻塞,当前进程 / 线程无法执行其他操作。所以把套接字修改为非阻塞,读缓冲区数据被读完,对应的全局变量errno值为EAGAIN
或者EWOULDBLOCK
。
// server.c
// epoll的工作模式 -- 边沿触发
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
// 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1)
{
perror("socket error");
exit(1);
}
// 绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(9999);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地有多个IP
// 设置端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret == -1)
{
perror("bind error");
exit(1);
}
// 监听
ret = listen(lfd, 64);
if (ret == -1)
{
perror("listen error");
exit(1);
}
// 创建一个epoll实例
int epfd = epoll_create(100);
if (epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 往epoll实例中添加监听的文件描述符
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1)
{
perror("epoll_ctl");
exit(0);
}
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(struct epoll_event);
// 应该让内核持续检测
while (1)
{
int num = epoll_wait(epfd, evs, size, -1); // 阻塞式
printf("num = %d\n", num);
for (int i = 0; i < num; i++)
{
// 取出当前的文件描述符
int curfd = evs[i].data.fd;
// 用于监听的文件描述符
if (curfd == lfd)
{
// 建立新的连接
int cfd = accept(curfd, NULL, NULL);
// 将文件描述符设置为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 文件描述符添加到epoll实例中
ev.events = EPOLLIN | EPOLLET; // 边沿触发模式
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1)
{
perror("epoll_ctl-accept");
exit(0);
}
}
// 用于通信的文件描述符
else
{
// 接收数据
char buf[5];
memset(buf, 0, sizeof(buf));
// 循环读数据
while (1)
{
int len = recv(curfd, buf, sizeof(buf), 0);
if (len == 0)
{
printf("客户端断开了连接。\n");
// 将这个文件描述符从epoll实例中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
break;
}
else if (len > 0)
{
// 接收的数据打印到终端
write(STDOUT_FILENO, buf, len);
// 发送数据
send(curfd, buf, len, 0);
}
else
{
// len == -1
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("数据读完了。\n");
break;
}
else
{
// 异常
perror("recv");
exit(0);
}
}
}
}
}
}
return 0;
}
epoll
需要建立文件系统、红黑树和链表,效率不如select
和poll
。select
或者poll
。epoll
能够明显提升性能。参考:https://subingwen.cn/linux/
参考:https://www.bilibili.com/video/BV1Rq4y1s7uu