文章目录
原理:使用一个线程来检查多个文件描述符,委托内核进行检查,如果有一个文件描述符就绪,则返回,否则阻塞直到超时,大大减少需要的线程数量、内存开销和上下文切换的CPU开销(比如一个事用1000个线程去做,但如果使用IO复用,可以只用一个线程)。
1.Select
1.1 工作流程
需要进行IO操作的socket 添加到socket
阻塞直到select系统调用返回(委托内核进行操作)
用户线程发起read请求
内核进行数据拷贝,给用户线程,完成read
有一张图非常的形象:
1.2 fd_set函数
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
// 将文件描述符fd从set集合中删除
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中
int FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set);
主要用于将文件描述符fd与fd_set集合进行关联
1.3 select函数
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
readfds:内核检测该集合中的IO是否可读。
writefds:内核检测该集合中的IO是否可写
exceptfds:内核检测该集合中的IO是否异常
nfds:以上三个集合中最大的文件描述符数值 + 1,例如集合是{0,1,4},那么 maxfd 就是 5
timeout:用户线程调用select的超时时长
timeout = NULL,等待无限长时间
timeout = 0,不等待,立刻返回
timeout>0,等待指定时间
返回值:
大于0:成功,返回集合中已就绪的IO总个数
等于-1:调用失败
等于0:没有就绪的IO
1.4 例程
先用FD_ZERO将位置0,然后使用FD_SET设置所监听的文件描述符到fd_set,select函数进行监听,当select返回大于0,则使用FD_ISSET遍历所有fd到maxfd,如果可操作,则去操作,操作完后需要用FD_CLR清除已产生的事件。
假设fd = 1,fd = 2上发生事件,则select返回时,rset的值为0x0003,当处理完fd = 1的事件,调用FD_CLR后,则值变为0x0002,以此类推
服务端代码:
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
int main()
{
// 创建socket
int lFd = socket(PF_INET, SOCK_STREAM, 0);
if (lFd < 0)
{
printf("socket error\n");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
int iRet = 0;
// 绑定
iRet = bind(lFd, (struct sockaddr *)&saddr, sizeof(saddr));
if (iRet < 0)
{
printf("bind error\n");
return -1;
}
// 监听
iRet = listen(lFd, 8);
if (iRet < 0)
{
printf("listen error\n");
return -1;
}
int maxFd = lFd;
fd_set allFdSets, tmpFdSets;
FD_ZERO(&allFdSets);
FD_SET(lFd, &allFdSets);
while(1)
{
memcpy(&tmpFdSets, &allFdSets, sizeof(tmpFdSets));
iRet = select(maxFd + 1, &tmpFdSets, NULL, NULL, 0);
if (iRet == -1)
{
perror("select error:");
continue;
}
else if (iRet == 0)
{
printf("select return no results\n");
continue;
}
else
{
for (int i = lFd; i < maxFd + 1; i++)
{
if (i == lFd)
{
/// new client
if (FD_ISSET(lFd, &tmpFdSets))
{
struct sockaddr_in addr = {0};
int iLen = sizeof(addr);
int clientFd = accept(lFd, (struct sockaddr *)&addr, &iLen);
FD_SET(clientFd, &allFdSets);
maxFd = clientFd > maxFd ? clientFd : maxFd;
FD_CLR(lFd, &tmpFdSets);
}
}
else
{
/// msg
if (FD_ISSET(i, &tmpFdSets))
{
char acBuf[1024] = {0};
int iLen = read(i, acBuf, sizeof(acBuf));
if (iLen == -1)
{
printf("fd:%d error read ret:%d\n", i, iLen);
continue;
}
else if (iLen == 0)
{
printf("fd %d closed\n", i);
}
else
{
printf("fd %d, recv buf :%s, return ok\n", i, acBuf);
write(i, "ok", strlen("ok"));
}
}
}
FD_CLR(i, &tmpFdSets);
}
}
}
return 0;
}
客户端代码:
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "data%d", num++);
printf("write buf:%s\n", sendBuf);
write(fd, sendBuf, strlen(sendBuf) + 1);
char recvBuf[1024] = {0};
// 接收
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", recvBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
sleep(1);
}
close(fd);
return 0;
}
2.poll
2.1 poll函数
pol和select原理基本相同,使用起来稍微有点差别,它没有最大1024文件描述符限制,也不需要每次重置fd_set数组的值。
struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
fds:struct pollfd类型的数组, 存储了待检测的文件描述符,struct pollfd有三个成员
fd:委托内核检测的文件描述符
events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
其中event的取值如下,不同事件对应不同值
nfds:描述的是数组 fds 的大小
timeout: 指定poll函数的阻塞时长 ,-1代表无限等待
返回值:
-1:失败,并设置errno,可以用perror打印
大于0:检测的集合中已就绪的文件描述符的总个数
2.2 例程
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <poll.h>
#include <signal.h>
int main()
{
// 创建socket
int lFd = socket(PF_INET, SOCK_STREAM, 0);
if (lFd < 0)
{
printf("socket error\n");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
int iRet = 0;
// 绑定
iRet = bind(lFd, (struct sockaddr *)&saddr, sizeof(saddr));
if (iRet < 0)
{
printf("bind error\n");
return -1;
}
// 监听
iRet = listen(lFd, 8);
if (iRet < 0)
{
printf("listen error\n");
return -1;
}
int nFds = 0;
struct pollfd fds[512] = {0};
int maxFds = sizeof(fds)/ sizeof(fds[0]);
for (int i = 0; i < maxFds; i++)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lFd;
fds[0].events = POLLIN;
while(1)
{
iRet = poll(fds, nFds + 1, -1);
if (iRet == -1)
{
perror("poll error:");
continue;
}
else if (iRet == 0)
{
printf("poll return no results\n");
continue;
}
else
{
/// new client
if (fds[0].revents & POLLIN)
{
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lFd, (struct sockaddr *)&cliaddr, &len);
printf("new client connect\n");
for (int i = 1; i < maxFds; i++)
{
if (fds[i].fd == -1)
{
fds[i].fd = cfd;
fds[i].events = POLLIN;
nFds = i > nFds ? i : nFds;
printf("new client connect success, fd:%d, nFds:%d\n", fds[i].fd, nFds);
break;
}
}
}
/// client have data
for (int i = 1; i <= nFds; i++)
{
if (fds[i].revents & POLLIN)
{
char acBuf[1024] = {0};
int iLen = read(fds[i].fd, acBuf, sizeof(acBuf));
if (iLen == -1)
{
printf("fd:%d error read ret:%d\n", i, iLen);
continue;
}
else if (iLen == 0)
{
if (i == nFds)
{
nFds--;
}
printf("fd %d closed, relase source, nFds:%d\n", fds[i].fd, nFds);
fds[i].fd = -1;
fds[i].events = POLLIN;
}
else
{
printf("fd %d, recv buf :%s, return ok\n", fds[i].fd, acBuf);
write(fds[i].fd, "ok", strlen("ok"));
}
}
}
}
}
return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
sprintf(sendBuf, "data%d", num++);
printf("write buf:%s\n", sendBuf);
write(fd, sendBuf, strlen(sendBuf) + 1);
char recvBuf[1024] = {0};
// 接收
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", recvBuf);
} else {
printf("server disconnect...\n");
break;
}
sleep(1);
}
close(fd);
return 0;
}
3.epoll
Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,且epoll采用红黑树管理文件描述符,效率会更高
3.1 工作流程
正如这个图一样,epoll相比较select和poll一个比较大的优点就在于,它能够准确告知应用层是哪一个事件来了,而不需要去一个个遍历,减少很大一部分开销
epoll整体流程如下:
在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是 需检测文件描述符信息(红黑树),还有一个是就绪列表,存放已改变的文件描述符信息(双向链表)
3.2 相关函数
创建epoll句柄
int epoll_create(int size);
int epoll_create1(int flags);
控制epoll实例,主要是增加或删除需要监控的IO事件
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll句柄
op:操作选项
EPOLL_CTL_ADD: 向 epoll 句柄注册文件描述符对应的事件
EPOLL_CTL_DEL:向 epoll 句柄删除文件描述符对应的事件
fd:操作的文件描述符
event:注册的事件类型,并且可以通过这个结构体设置用户自定义数据
events:注册的事件类型
data:用户自定义数据
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待I/O事件就绪
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
**epfd:**epoll句柄
events:出参,代表发生变化的文件描述符信息,可能是多个
maxevents:events的结构体数组大小
timeout:
-1,一直阻塞,直到有事件就绪后返回
0,不阻塞,函数马上返回
大于0:等待指定时间后返回
3.3 epoll的两种工作模式
epoll由两种工作模式,分别为LT模式(条件触发)、ET模式(边缘触发),默认为条件触发
条件触发:只要输入缓冲有数据,便一直触发事件
a. 用户不读数据,数据一直在缓冲区,epoll 会一直通知
b. 用户只读了一部分数据,epoll会通知
c. 缓冲区的数据读完了,不通知
边缘触发:只有描述符从未就绪变为就绪时,才会为文件描述符发送一次就绪通知,之后不再通知
减少了事件被重复触发的次数,效率比LT模式高,且可以分离接收数据和处理数据的时间点,工作在该模式必须要使用非阻塞等待
a. 用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不再次通知了
b. 用户只读了一部分数据,epoll不再次通知
c. 缓冲区的数据读完了,不再次通知
3.4 示例代码
条件触发模式
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 监听
listen(lfd, 8);
// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100);
// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
struct epoll_event epevs[1024];
while(1) {
int ret = epoll_wait(epfd, epevs, 1024, -1);
if(ret == -1) {
perror("epoll_wait");
exit(-1);
}
printf("ret = %d\n", ret);
for(int i = 0; i < ret; i++) {
int curfd = epevs[i].data.fd;
if(curfd == lfd) {
// 监听的文件描述符有数据达到,有客户端连接
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
} else {
if(epevs[i].events & EPOLLOUT) {
continue;
}
// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len == 0) {
printf("client closed...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if(len > 0) {
printf("read buf = %s\n", buf);
write(curfd, buf, strlen(buf) + 1);
}
}
}
}
close(lfd);
close(epfd);
return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
// 创建socket
int fd = socket(PF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(9999);
// 连接服务器
int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if(ret == -1){
perror("connect");
return -1;
}
int num = 0;
while(1) {
char sendBuf[1024] = {0};
// sprintf(sendBuf, "send data %d", num++);
fgets(sendBuf, sizeof(sendBuf), stdin);
write(fd, sendBuf, strlen(sendBuf) + 1);
// 接收
int len = read(fd, sendBuf, sizeof(sendBuf));
if(len == -1) {
perror("read");
return -1;
}else if(len > 0) {
printf("read buf = %s\n", sendBuf);
} else {
printf("服务器已经断开连接...\n");
break;
}
}
close(fd);
return 0;
}
4.总结
IO复用中epoll会更高效,内存拷贝次数少,时间复杂度低,且不受fd数量的限制