# 网络io与select poll epoll
=====
## 简介
一个对话框实验,主要用于理解网络io,使用了阻塞,多线程,select,poll,epoll等多种方式
## 五种网络IO模型与其实现
### 1.阻塞——单线程与多线程
- accept位于whlie循环之前:只能连接一个客户端
- accept位于whlie循环之中:能连接多个客户端,但只能接收一条消息
- thread 多线程
- 每个请求对应一个线程,但是多线程需要进行CPU的上下文切换
- 优点:结构简单
- 缺点:无法支持大量客户端
// 4G / 8m = 512 假设运行内存只有4g,每个客户端连接占8m,只能支持512个连接
// C10K
void *client_routine(void *arg)
{
int connfd = *(int *)arg;
char buff[MAXLNE];
while (1)
{
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
break;
}
}
return NULL;
}
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
//使用socket套接字创建listenfd
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//设置sockaddr结构体
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//绑定地址
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//开启listen
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
#if 0
//首先是最基本的方案,在while循环之前accept
//只能实现单个客户端的连接
//定义客户端地址
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("=================wait for client===================\n");
while (1)
{
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
//加上字符串的尾部,以便显示和转发
buff[n] = '\0';
printf("recv msg from clientL %s \n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
}
}
#elif 0
//将accept放入while循环,可以实现多个客户端连接,
//但是每次只能进行对话一次
printf("========waiting for client's request========\n");
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
}
else if (n == 0)
{
close(connfd);
}
// close(connfd);
}
#elif 0
//开线程,可以实现多个客户端连接,
//但是数量有限,开销大
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, (void *)&connfd);
}
#elif 0
//这份代码有问题
//使用select实现多路复用
//分别声明select中的读集合,写集合,读操作,写操作
fd_set rfds, wfds, rset, wset;
//全都置0
FD_ZERO(&rfds);
//将listen_fd加入读集合
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int max_fd = listenfd;
while (1)
{
//将新的读写操作更新读写集合
rset = rfds;
wset = wfds;
//登记读写集合
int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);
//监听到新的套接字
if (FD_ISSET(listenfd, &rset))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//加入到写集合中
FD_SET(connfd, &rfds);
//更新max_fd
if (connfd > max_fd)
max_fd = connfd;
//返回的套接字里面啥也没有
if (--nready == 0)
continue;
}
//循环读操作里面的
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++)
{
//在读操作里
if (FD_ISSET(i, &rset))
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
//设置为可写
FD_SET(i, &wfds);
// reactor
// send(i, buff, n, 0);
}
else if (n == 0)
{
FD_CLR(i, &rfds);
close(i);
}
if (--nready == 0)
break;
}
//在写操作里
else if (FD_ISSET(i, &wset))
{
send(i, buff, n, 0);
//设置为可读
FD_SET(i, &rfds);
}
}
}
#elif 1
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
int max_fd = listenfd;
while (1)
{
//每次都会被清空,需要重新放入
rset = rfds;
int nready = select(max_fd + 1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rfds);
if (connfd > max_fd)
max_fd = connfd;
if (--nready == 0)
continue;
}
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++)
{
if (FD_ISSET(i, &rset))
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
}
else if (n == 0)
{
FD_CLR(i, &rfds);
close(i);
}
if (--nready == 0)
break;
}
}
}
#endif
close(listenfd);
return 0;
}
### 2.阻塞——select
- 执行流程
- select 是一个阻塞函数,当没有数据时,会一直阻塞在select这一行
- 当有数据时会将reset中对应的那一位置1
- select函数返回不再阻塞
- 遍历文件描述符数组,判断哪个fd被置1了
- 读取数据,然后处理
- 缺点
- 所使用的bitmap默认大小是1024,虽然可以调整但还是有限
- rset每次循环都必须重新置1,不可以重复使用
- 尽管将rset从用户态拷贝到内核态,由内核态判断是否有数据,但是还是有拷贝的开销
- 当有数据时select就会返回,但是select不知道哪个文件描述符发生变化,还需要进行遍历,效率比较低
//这份代码有问题
//使用select实现多路复用
//分别声明select中的读集合,写集合,读操作,写操作
fd_set rfds, wfds, rset, wset;
//全都置0
FD_ZERO(&rfds);
//将listen_fd加入读集合
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int max_fd = listenfd;
while (1)
{
//将新的读写操作更新读写集合
rset = rfds;
wset = wfds;
//登记读写集合
int nready = select(max_fd + 1, &rset, &wset, NULL, NULL);
//监听到新的套接字
if (FD_ISSET(listenfd, &rset))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//加入到写集合中
FD_SET(connfd, &rfds);
//更新max_fd
if (connfd > max_fd)
max_fd = connfd;
//返回的套接字里面啥也没有
if (--nready == 0)
continue;
}
//循环读操作里面的
int i = 0;
for (i = listenfd + 1; i <= max_fd; i++)
{
//在读操作里
if (FD_ISSET(i, &rset))
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
//设置为可写
FD_SET(i, &wfds);
// reactor
// send(i, buff, n, 0);
}
else if (n == 0)
{
FD_CLR(i, &rfds);
close(i);
}
if (--nready == 0)
break;
}
//在写操作里
else if (FD_ISSET(i, &wset))
{
send(i, buff, n, 0);
//设置为可读
FD_SET(i, &rfds);
}
}
}
### 3.阻塞——poll
- pollfd结构体
- fd:文件描述符
- events:对应的时间,如果是读事件就是POLLIN,如果是写事件就是POLLOUT
- revents:对events的反馈,开始时为0,当有数据可读时就为POLLIN,类似select的rset
/* Data structure describing a polling request. */
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
// poll 先把listenfd放进去,关注读事件
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
int i = 0;
for (i = 1; i < POLL_SIZE; i++)
{
fds[i].fd = -1;
}
while (1)
{
int nready = poll(fds, max_fd + 1, -1);
// listenfd发生读事件
if (fds[0].revents & POLLIN)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//接收此次收到的客户端文件描述符
printf("accept \n");
fds[connfd].fd = connfd;
fds[connfd].events = POLLIN;
if (connfd > max_fd)
max_fd = connfd;
if (--nready == 0)
continue;
}
//遍历fds内部,读取revent确定是否收到数据
for (i = listenfd + 1; i <= max_fd; i++)
{
if (fds[i].revents & POLLIN)
{
n = recv(i, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
//把刚刚收到的发出去
send(i, buff, n, 0);
}
//客户端断开连接,回收资源
else if (n == 0)
{
fds[i].fd = -1;
close(i);
}
if (--nready == 0)
break;
}
}
}
- 执行流程
- 将需要用到的fd从用户态拷贝到内核态
- poll为阻塞方法,执行poll方法,如果有数据会将fd对应的revents置为POLLIN
- poll方法返回
- 循环遍历,查找哪个fd被置为POLLIN
- 将revents重新置0,便于复用
- 将置位的fd进行读取和处理
- 对于select提升的地方
- 解决了bitmap的1024大小限制
- 解决了rset不可重用的情况
- 弊端
- 每次poll都仍要重新遍历全量的fds
-
服务程序也要遍历全量的fds,查看每个文件描述符的revents字段是否需要读写操作
### 4.非阻塞——epoll
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epoll_create() 返回一个文件描述符,该文件描述符“描述”的是内核中的一块内存区域,size现在不起任何作用。
- epoll_ctl()用来操作内核事件表,
- int epfd表示epoll_create() 返回的事件表
- int fd:新创建的socket文件描述符
- int op
- EPOLL_CTL_ADD: 事件表中添加一个文件描述符,内核应该关注的socket的事件在epoll_event结构体中,添加到事件表中的文件描述符以红黑树的形式存在,防止重复添加
- EPOLL_CTL_MOD:修改fd上注册的事件
- EPOLL_CTL_DEL:删除fd上注册的事件
- struct epoll_event *event
struct epoll_event{
_uint32_t events; //epoll事件,读、写、异常三种
epoll_data_t data; //用户数据
}
struct epoll_data{
void* prt;
int fd;
_uint32_t u32;
_uint64_t u64;
}epoll_data_t;
-
epoll_wait()该函数返回就绪文件描述符的个数
// poll/select -->
// epoll_create
// epoll_ctl(ADD, DEL, MOD)
// epoll_wait
//创建一个空间,假设是一个大房子
int epfd = epoll_create(1);
struct epoll_event events[POLL_SIZE] = {0};
struct epoll_event ev;
//先把listenfd先塞进去
ev.events = EPOLLIN;
ev.data.fd = listenfd;
// add
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1)
{
//非阻塞,最后一个是超时时间
int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
if (nready == -1)
{
continue;
}
int i = 0;
for (i = 0; i < nready; i++)
{
//读取获取的fd
int clientfd = events[i].data.fd;
//如果是listenfd
if (clientfd == listenfd)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1)
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept\n");
//添加到ev里
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 获取到的消息
else if (events[i].events & EPOLLIN)
{
n = recv(clientfd, buff, MAXLNE, 0);
if (n > 0)
{
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(clientfd, buff, n, 0);
}
else if (n == 0)
{
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
- 执行流程
图片转载自https://file.cfanz.cn/uploads/jpeg/2022/01/15/19/291353M663.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1h1ZXlpbkd1bw==,size_16,color_FFFFFF,t_70https://file.cfanz.cn/uploads/jpeg/2022/01/15/19/291353M663.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1h1ZXlpbkd1bw==,size_16,color_FFFFFF,t_70