4.1 I/O多路转接
I/O多路转接技术:
- 先构造一张有关文件描述符的列表,将要监听的文件描述符添加到列表中
- 然后调用一个函数,监听该列表中的文件描述符,知道这些描述符中的一个进行I/O操作时,该函数才返回
– 该函数为阻塞函数
– 函数对文件描述符的检测操作是由内核完成的- 在返回时,他告诉进程有多少(哪些)描述符要进行I/O操作
使用多路转接技术:
select
、poll
、epoll
第一种:select
、poll
只能确定数量
第二种:epoll
不仅能确定数量、而且能确定是哪一个
4.2 同步I/O多路复用select
优点:
- 跨平台
缺点:
- 每次调用selcet,都需要吧fd集合从用户态拷贝到内核态
- 同时每次调用select都需要在内核遍历传递进来的所有fd
- select支持的文件描述符数量太小了,默认是1024
#include <sys/select.h>
/*
* 函数功能:
* 同步I/O多路复用
* 参数:
* nfds:要检测的文件描述符中最大的fd+1(最大值1024)
* readfds:读集合
* 传入传出参数
* writefds:写集合,通常为NULL
* exceptfds:异常集合,通常为NULL
* timeout:
* NULL:永久阻塞,当检测到fd发生变化时返回
* 阻塞等待:tv_sec+tv_usec
* 返回值:
* 成功:0
* 失败:-1
*/
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}
//把文件描述符集合里fd清0
void FD_CLR(int fd, fd_set *set);
//测试文件描述符集合里fd是否置1
int FD_ISSET(int fd, fd_set *set);
//把文件描述符集合里fd位置1
void FD_SET(int fd, fd_set *set);
//把文件描述符集合里所有位清0
void FD_ZERO(fd_set *set);
4.2.1 select
伪代码实现
int main()
{
int lfd = socket();
bind();
listen();
// 创建一个文件描述符的表
fd_st reads, temp;
// 初始化
FD_ZERO(&reads);
// 将lfd加入用于监听连接请求
FD_SET(lfd, &reads);
int max_fd = lfd;
while(1)
{
// 委托内核检测
temp = reads;
int ret = select(max_fd+1, &temp, NULL, NULL, NULL);
// 判断是否为监听文件描述符
if(FD_ISSET(lfd, &temp))
{
// 有新连接
int cfd = accept();
// 将cfd加入reads
FD_SET(cfd, &reads);
// 更性max_fd
max_fd = max_fd < cfd ? cfd : max_fd;
}
// 客户端发送数据
for(int i = lfd + 1; i <= max_fd; ++i)
{
if(FD_ISSET(i, &temp))
{
int len = read();
if(len == 0)
{
// 客户端断开连接,从集合中删除
FD_CLR(i, &reads);
}
write();
}
}
}
}
4.2.2 select
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#define MAX_LISTEN 128
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./server 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
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定端口和IP
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置监听上限
listen(lfd, MAX_LISTEN);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
printf("Start accept...\n");
// 最大的文件描述符
int max_fd = lfd;
//文件描述符的集合
fd_set reads, temp;
// 初始化
FD_ZERO(&reads);
FD_SET(lfd, &reads);
while(1)
{
temp = reads;
// 使用select委托内核做I/O检测
int ret = select(max_fd + 1, &temp, NULL, NULL, NULL);
if(ret == -1)
{
perror("select error:");
exit(-2);
}
if(FD_ISSET(lfd, &temp))
{
// 有新连接
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
if(cfd == -1)
{
perror("accept error\n");
exit(-3);
}
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));
// 将新连接加入raeds
FD_SET(cfd, &reads);
// 更新
max_fd = max_fd < cfd ? cfd : max_fd;
}
// 客户端发送新数据
int i;
for(i = lfd + 1; i <= max_fd; ++i)
{
if(FD_ISSET(i, &temp))
{
char buf[1024] = { 0 };
int len = recv(i, buf, sizeof(buf), 0);
if(len == -1)
{
perror("rece error\n");
exit(-4);
}
else if(len == 0)
{
printf("Client disconneted\n");
close(i);
FD_CLR(i, &reads);
}
else
{
printf("recv buf: %s\n", buf);
send(i, buf, strlen(buf), 0);
}
}
}
}
close(lfd);
pthread_exit(0);
}
4.3 等待文件描述符的某个事件poll
优点:
- 可以突破1024限制
缺点:
- 每次调用selcet,都需要吧fd集合从用户态拷贝到内核态
- 同时每次调用select都需要在内核遍历传递进来的所有fd
- 不跨平台
#include <poll.h>
/*
* 函数功能:
* 等待文件描述符的某个事件
* 参数:
* fds:数组的地址
* nfds:最大文件描述符+1
* timeout:
* -1:永久阻塞,当检测到fd发生变化时返回
* 0:调用完立即返回
* >0:等待的时长(ms)
* 返回值:
* 成功:0
* 失败:-1
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待事件 */
short revents; /* 实际发生的事件 */
};
事件 | 说明 |
---|---|
POLLIN | 普通或带外优先数据可读,即POLLRDNORM or POLLRDBAND |
POLLRDNORM | 数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级可读数据 |
POLLOUT | 普通或带外数据可写 |
POLLWRNORM | 数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述字不是一个打开的文件 |
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#include <poll.h>
#define MAX_LISTEN 128
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./server 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
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定端口和IP
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置监听上限
listen(lfd, MAX_LISTEN);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
printf("Start accept...\n");
// poll
struct pollfd allfd[MAX_LISTEN];
int max_index = 0;
// init
for(int i = 0;i < MAX_LISTEN; ++i)
{
allfd[i].fd = -1;
allfd[i].events = POLLIN;
}
allfd[0].fd = lfd;
while(1)
{
int i = 0;
// 使用poll委托内核做I/O检测
int ret = poll(allfd, max_index + 1, -1);
if(ret == -1)
{
perror("select error:");
exit(-2);
}
// 判断是否有连接请求
if(allfd[0].revents & POLLIN)
{
client_len = sizeof(client_addr);
// 接收新连接
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
// 添加cfd到poll组
for(i = 0;i < MAX_LISTEN;++i)
{
if(allfd[i].fd == -1)
{
allfd[i].fd = cfd;
break;
}
}
// 更新下标
max_index = max_index < i ? i : max_index;
}
// 有数据到达
for(i = 0; i <= MAX_LISTEN; ++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("rece error\n");
exit(-4);
}
else if(len == 0)
{
printf("Client disconneted\n");
close(fd);
allfd[i].fd = -1;
}
else
{
printf("recv buf: %s\n", buf);
send(i, buf, strlen(buf), 0);
}
}
}
}
close(lfd);
pthread_exit(0);
}
4.4 I/O事件通知epoll
#include <sys/epoll.h>
/*
* 函数功能:
* 生成一个epoll专用的文件描述符
* 参数:
* size: epoll上能关注的最大文件描述符
* 返回值:
* 成功:文件描述符
* 失败:-1
*/
int epoll_create(int size);
/*
* 函数功能:
* 控制整个epoll描述符事件,可以注册,修改,删除
* 参数:
* epfd:epoll_create生成的epoll专用的描述符
* op:
* EPOLL_CTL_ADD:注册
* EPOLL_CTL_MOD:修改
* EPOLL_CTL_DEL:删除
* fd:关联的文件描述符
* 返回值:
* 成功:0
* 失败:-1
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
* 函数功能:
* 等待IO事件发生 - 可以设置阻塞的函数
* 参数:
* epfd:要检测的句柄
* events:用于回传待处理事件的数组(传出参数)
* maxevents:告诉内核events的大小
* timeout:
* -1:永久阻塞
* 0:立即返回
* >0:等待时间(ms)
* 返回值:
* 成功:发生变化的数量
* 失败:-1
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event {
uint32_t events;
epoll_data_t data;
}
event | 说明 |
---|---|
EPOLLIN | 读 |
EPOLLOUT | 写 |
EPOLLERR | 异常 |
4.4.1 epoll
模型
int main()
{
// 创建监听的套接字
int lfd = socket();
// 绑定
bind();
// 监听
listen();
// epoll 数根节点
int epfd = epoll_create(EPOLL_LENGTH);
// 存储发生变化的fd对应信息
struct epoll_event all[EPOLL_LENGTH];
// init
// 将监听的lfd加入到epoll树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
while(1)
{
// 委托内核检测事件
int ret = epoll_wait(epfd, all, EPOLL_LENGTH, -1);
if(ret == -1)
{
peintf("epoll_wait error");
exit(-1);
}
for(int i = 0; i < ret; ++i)
{
int fd = all[i].data.fd;
// 有新连接
if(fd == lfd)
{
int cfd = accept();
// 将cfd加入epoll树
ev.events = EPOLLIN;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
// 新的消息
else
{
// 只处理客户端发送的数据
if(all[i].events & EPOLLIN)
{
int len = recv();
if(len == 0)
{
close(fd);
// 将fd从树上删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
send();
}
}
}
}
}
4.4.2 epoll
代码实现
#include <stdio.h>
#include <sys/epoll.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#define MAX_LISTEN 128
#define EPOLL_NUM 100
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./server 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
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定端口和IP
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置监听上限
listen(lfd, MAX_LISTEN);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
printf("Start accept...\n");
// 创建epoll树
int epfd = epoll_create(EPOLL_NUM);
// 初始化epoll树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// 需要处理的数组
struct epoll_event all[EPOLL_NUM];
while(1)
{
// 使用epoll
int n = epoll_wait(epfd, all, EPOLL_NUM, -1);
if(n == -1)
{
perror("epoll_wait error:");
exit(-2);
}
for(int i = 0;i < n;++i)
{
int fd = all[i].data.fd;
// 新连接
if(fd == lfd)
{
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
if(cfd == -1)
{
perror("accept error:");
exit(-3);
}
// 将cfd加入poll树
struct epoll_event temp;
temp.data.fd = cfd;
temp.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD, cfd,&temp);
printf("new connect...\n");
}
// 新消息
else
{
char buf[1024] = { 0 };
if(!all[i].events & EPOLLIN)
{
// 其他事件处理
continue;
}
int ret = recv(fd, buf, sizeof(buf), 0);
if(ret == 0)
{
// 断开连接
printf("client disconnected...\n");
// 将fd从poll树中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
else if(ret == -1)
{
perror("recv error:");
exit(-4);
}
else
{
printf("recv buf:%s\n", buf);
write(fd, buf, ret);
}
}
}
}
close(lfd);
pthread_exit(0);
}
4.4.3 epoll工作模式
4.4.3.1 水平触发模式(默认)
只要fd对应的缓冲区有数据,
epoll_wait
就返回,返回的次数与发送数据的次数没有关系。
例如:如果缓冲区中的数据一次没有读取完毕,epoll_wait
会再反回,继续读取。
4.4.3.2 边沿(阻塞)触发模式
client给server发送数据:
- 每发送一次数据server的
epoll_wait
返回一次- 即使数据没有读取完毕也不会再返回
边沿模式可以提高效率
只需要将struct epoll_even
中的events
添加EPOLLET
宏
示例:
int cfd = accept(lfd, (struct sockaddr*)cli, &len);
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
4.4.3.3 边沿非阻塞模式
该模式效率最高
解决recv
/read
的阻塞:
- 使用
open()
– 设置flags
– 使用O_WDRW
|O_NONBLOCK
– 终端文件:/dev/tty
- 使用
fcntl()
–int flag = fcntl(fd, F_GETFL);
–flg |= O_NOBLOCK;
–fcntl(fd, F_SETFL, flag);
// 将缓冲区数据完全读取
while(recv() > 0)
{
printf();
}
// 需要判断errno 是否为 EAGAIN
示例代码:
#include <stdio.h>
#include <sys/epoll.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <errno.h>
#define MAX_LISTEN 128
#define EPOLL_NUM 100
int main(int argc, const char* argv[])
{
if(argc < 2)
{
printf("eg: ./server 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
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 绑定端口和IP
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置监听上限
listen(lfd, MAX_LISTEN);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
printf("Start accept...\n");
// 创建epoll树
int epfd = epoll_create(EPOLL_NUM);
// 初始化epoll树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events= EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
// 需要处理的数组
struct epoll_event all[EPOLL_NUM];
while(1)
{
// 使用epoll
int n = epoll_wait(epfd, all, EPOLL_NUM, -1);
if(n == -1)
{
perror("epoll_wait error:");
exit(-2);
}
for(int i = 0;i < n;++i)
{
int fd = all[i].data.fd;
// 新连接
if(fd == lfd)
{
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_len);
if(cfd == -1)
{
perror("accept error:");
exit(-3);
}
// 设置为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将cfd加入poll树
struct epoll_event temp;
temp.data.fd = cfd;
temp.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD, cfd,&temp);
printf("new connect...\n");
}
// 新消息
else
{
char buf[2] = { 0 };
if(!all[i].events & EPOLLIN)
{
// 其他事件处理
continue;
}
int len;
// 循环读取数据
while((len = recv(fd, buf, sizeof(buf), 0)) > 0)
{
write(STDOUT_FILENO, buf, len);
send(fd, buf, len, 0);
}
if(len == 0)
{
// 断开连接
printf("client disconnected...\n");
close(fd);
// 将fd从poll树中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
else if(len == -1 && errno != EAGAIN)
{
perror("recv error:");
exit(-4);
}
}
}
}
close(lfd);
pthread_exit(0);
}
4.5 文件描述符突破1024
select - 突破不了,需要重新编译内核
- 通过数组实现poll和epoll可以突破1024
- poll内部使用的链表实现
- epoll内部使用红黑树实现查看计算机硬件限制的文件描述符上限
cat /proc/sys/fs/file-max
通过配置文件修改上限
/etc/security/limits.conf
在文件尾部添加:
将进程文件描述符上限设置为8000(重启或注销使配置生效)
也可使用ulimit -n 8000
进行修改
4.6 epoll反应堆
自定义epoll模型:
- 在server - >创建树的根节点-> 在树上添加需要监听的节点->监听读事件->有返回-> 通信->epoll_wait
- 在server - >创建树的根节点-> 在树上添加需要监听的节点->监听读事件->有返回-> 通信(接收数据)->将这个fd从树上删除->监听写事件->写操作->fd从树上摘下来->监听fd的读事件->epoll_wait
EPOllOUT
- 水平模式:
- struct epoll_event ev;
- ev.events = EPLLOUT; - 检测写缓冲区是否
可写- epoll_wait会一直返回, 缓冲区能写数据,该函数会返回, 缓冲区满的时候, 不返回
- 边缘模式:
- 第一次设置的时候epoll_wait会返回一次
- 缓冲区从满->到不满的时候