I/O复用
用途:I/O 复用能同时监听多个文件描述符。
在Linux下,有三种系统调用函数,分别是select,poll,epoll。
1.select
用途:在一段指定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件。
1.接口介绍:
1.int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
2. fd_set 是用来存放描述符的对应事件的集合。
2.例子
1.用select监听键盘是否有数据输入
#include <stdio.h>
#include <sys/select.h>
#include <stdlib.h>
#include <sys/time.h>
#include <string.h>
#include <unistd.h>
int main()
{
char buff[256] = {0};
fd_set fdset;
while (1)
{
FD_ZERO(&fdset);
FD_SET(0, &fdset);
struct timeval tim = {5,0}; //每次都需要传入一个新的timeval结构体
int n = select(1,&fdset,NULL,NULL,&tim);//select中会改变tim的值
if(-1 == n)
{
printf("input error\n");
continue;
}
else if(0 == n)
{
printf("timeout\n");
continue;
}
else
{
if(FD_ISSET(0,&fdset))
{
memset(buff,0,256);
read(0,buff,255);
printf("read:%s\n",buff);
}
}
}
}
运行结果:
2.用select处理多个并发客户端
原理图:
服务端参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MaxLen 60
void fds_init(int fds[]) //初始化
{
for (int i = 0; i < MaxLen; ++i)
{
fds[i] = -1; // 无效的描述符
}
}
void fds_add(int fd, int fds[]) //添加描述符
{
for (int i = 0; i < MaxLen; ++i)
{
if (-1 == fds[i])
{
fds[i] = fd;
break;
}
}
}
void fds_del(const int fd, int fds[]) //删除描述符
{
for (int i = 0; i < MaxLen; ++i)
{
if (fd == fds[i])
{
fds[i] = -1;
break;
}
}
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000); // htons 将主机字节序转换为网络字节
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
res = listen(sockfd, 5);
if (-1 == res)
{
exit(1);
}
int fds[MaxLen]; // 存放套结字
fds_init(fds); //初始化
fds_add(sockfd, fds); //加入本地服务器的套结字
fd_set fdset;
while (1)
{
FD_ZERO(&fdset);
int maxfd = -1;
for (int i = 0; i < MaxLen; ++i) // 将所有已经建立连的接描述符写入fdset
{
if (-1 != fds[i])
{
FD_SET(fds[i], &fdset);
maxfd = maxfd > fds[i] ? maxfd : fds[i]; //记录文件描述符最大值
}
}
struct timeval tim = {5, 0};
int n = select(maxfd + 1, &fdset, NULL, NULL, &tim);
if (-1 == n)
{
printf("error\n");
continue;
}
else if (0 == n)
{
printf("timeout\n");
continue;
}
else
{
for (int i = 0; i < MaxLen; ++i)
{
if (-1 == fds[i])
{
continue;
}
if (FD_ISSET(fds[i], &fdset))
{
if (fds[i] == sockfd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (-1 == c)
{
continue;
}
printf("c:%d\n", c);
fds_add(c, fds); //建立连接,保存
}
else
{
char buff[256] = {0};
int n = recv(fds[i], buff, 255, 0);
if (0 >= n)
{
fds_del(fds[i], fds); //断开连接,清除
close(fds[i]);
printf("one client close");
}
else
{
printf("client[%d]:%s\n", fds[i], buff);
send(fds[i], "ok", 2, 0);
}
}
}
}
}
}
}
客户端参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
while (1)
{
char buff[128] = {0};
printf("input:\n");
fgets(buff, 128, stdin); // 会取得\n
if (strncmp(buff, "end", 3) == 0)
{
break;
}
send(sockfd, buff, strlen(buff), 0);
memset(buff, 0, 128);
recv(sockfd, buff, 127, 0);
printf("buff=%s\n", buff);
}
close(sockfd);
exit(0);
}
运行结果:
2.poll
用途:在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪的 。
1.接口介绍:
1.int poll(struct pollfd *fds, nfds_t nfds, int timeout);
2.pollfd 结构体
3.poll支持的事件:
2.例子
poll和select的原理相似;此处不再赘述。
本地服务器端参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <poll.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#define MaxLen 60
void fds_init(struct pollfd fds[])
{
for (int i = 0; i < MaxLen; ++i)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
void fds_add(struct pollfd fds[], int fd)
{
for (int i = 0; i < MaxLen; ++i)
{
if (-1 == fds[i].fd)
{
fds[i].fd = fd;
fds[i].events = POLLIN; // 读事件
fds[i].revents = 0;
break;
}
}
}
void fds_del(struct pollfd fds[], int fd)
{
for (int i = 0; i < MaxLen; ++i)
{
if (fd == fds[i].fd)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000); // htons 将主机字节序转换为网络字节
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
res = listen(sockfd, 5);
if (-1 == res)
{
exit(1);
}
struct pollfd fds[MaxLen]; //存放套接字描述符
fds_init(fds);
fds_add(fds, sockfd);
while (true)
{
int n = poll(fds, MaxLen, 6000); // 阻塞
if (-1 == n)
{
continue;
}
else if (0 == n)
{
printf("time out\n");
continue;
}
else
{
for (int i = 0; i < MaxLen; ++i)
{
if (-1 == fds[i].fd)
{
continue;
}
if (fds->revents & POLLIN)
{
if (sockfd == fds[i].fd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (-1 == c)
{
continue;
}
printf("c:%d\n", c);
fds_add(c,fds);
}
else
{
char buff[256] = {0};
int n = recv(fds[i].fd, buff, 255, 0);
if (0 >= n)
{
fds_del(fds[i].fd, fds); //断开连接,清除
close(fds[i].fd);
printf("one client close");
}
else
{
printf("client[%d]:%s\n", fds[i], buff);
send(fds[i].fd, "ok", 2, 0);
}
}
}
}
}
}
}
epoll
- epoll 是 Linux 特有的 I/O 复用函数。
- epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中。从而无需像 select 和 poll 那样每次调用都要重复传入文件描述符或事件集。但 epoll 需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
1.接口介绍:
1.epoll_create() 用于创建内核事件表
2.epoll_ctl()用于操作内核事件表
3.epoll_wait()用于在一段超时时间内等待一组文件描述符上的事件
2.例子
本地服务器端参考代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#define MaxLen 20
void epoll_add(int epfd, int fd, int op)
{
struct epoll_event ep;
ep.events = op;
ep.data.fd = fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ep) == -1)
{
printf("Add fd error\n");
}
}
void epoll_del(int epfd, int fd)
{
if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
{
printf("Del fd error\n");
}
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000); // htons 将主机字节序转换为网络字节
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
res = listen(sockfd, 5);
if (-1 == res)
{
exit(1);
}
int epfd = epoll_create(MaxLen); // 内核事件表的文件描述符
if (-1 == epfd)
{
exit(1);
}
struct epoll_event evs[MaxLen]; // 存放就绪的文件描述符
epoll_add(epfd,sockfd,EPOLL_CTL_ADD);
while (true)
{
int n = epoll_wait(epfd, evs, MaxLen, 5000);
if (-1 == n)
{
continue;
}
else if (0 == n)
{
printf("time out\n");
continue;
}
else
{
for (int i = 0; i < n; ++i) // 区别于select和poll
{
int fd = evs[i].data.fd;
if (evs[i].events & EPOLLIN) //读事件发生
{
if (sockfd == fd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(fd, (struct sockaddr *)&caddr, &len);
if (0 >= c)
{
continue;
}
printf("accept client:%d",c);
epoll_add(epfd, c, EPOLL_CTL_ADD);
}
else
{
char buff[256] = {0};
int c = recv(fd, buff, 255, 0);
if (0 >= c)
{
printf("one client close\n");
epoll_del(epfd, fd);
close(fd);
continue;
}
printf("client(%d):%s\n", fd, buff);
send(fd, "OK", 2, 0);
}
}
}
}
}
}
3.LT 和 ET 模式
1.LT模式
以读事件为例,当缓冲区有数据准备好的时候,此时会触发读事件,**如果我们一直不去读取缓冲区里的数据,epoll模型就会一直通知我们有事件就绪。**LT模式也是epoll模型的默认模式。
2.ET模式
对于读事件 EPOLLIN,当该描述符对应的接受缓冲区的数据准备好的时候,也会触发读事件,但是只会触发一次,如果我们这次没有调用read/recv 读取 或者 没有一次读完,后面就不会通知有读事件就绪了。简单来说,只有当该描述符对应的接受缓冲区里的数据量发生变化的时候,才会通知我们一次,不会像LT模式那样一直通知。
示例图:
3.ET模式下的非阻塞编程:
阻塞状态下epoll存在问题:
只有接收缓冲区的数据变化时才会通知,通知的次数少了自然也会引发一些问题,比如触发读事件后必须把数据收取干净,因为你不一定有下一次机会再收取数据了,即使不采用一次读取干净的方式,也要把这个激活状态记下来,后续接着处理,否则如果数据残留到下一次消息来到时就会造成延迟现象。
解决方法:
1.为避免接收缓冲区中的数据一次取不完,我们采用循环来将缓冲区读取干净,当recv发现缓冲区里没有数据了,此时会默认进入阻塞状态,等待数据就绪,这就严重影响到后面的文件描述符读取/写入内容了。所以ET模式下必须要设为非阻塞。
2.当非阻塞状态下时,当缓冲区没有数据时,会返回error,并且将全局变量( errno ) 置为 EAGAIN 或 EWOULDBLOCK;
置为非阻塞的两种方法:
1.fnctl函数设定
int flag = fcntl(client_fd, F_GETFL);
flag |= O_NONBLOCK;
int ret = fcntl(client_fd, F_SETFL, flag);
if (ret == -1)
{
printf("Set Wait error\n");
}
2.recv的参数设定
int c = recv(fd,buff,1,MSG_DONTWAIT);
if(0 >= c)
{
if(errno != EAGAIN && errno != EWOULDBLOCK )
{
epoll_del(epfd,fd);
close(fd);
printf("one client close\n");
}
}
4.例子
本地服务器端参考代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <fcntl.h> //新增
#include <errno.h> //新增
#define MaxLen 20
void set_NoWait(int fd)
{
int oldfl = fcntl(fd,F_GETFL); //获取描述符的状态
int newfl = oldfl | O_NONBLOCK;
int n = fcntl(fd,F_SETFL,newfl); //新增非阻塞状态
if(-1 == n)
{
printf("Set NoWait error\n");
}
}
void epoll_add(int epfd, int fd, int op)
{
struct epoll_event ep;
ep.events = op | EPOLLET; //开启ET模式
ep.data.fd = fd;
set_NoWait(fd); //置为非阻塞
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ep) == -1)
{
printf("Add fd error\n");
}
}
void epoll_del(int epfd, int fd)
{
if (epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) == -1)
{
printf("Del fd error\n");
}
}
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000); // htons 将主机字节序转换为网络字节
saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 回环地址
int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
res = listen(sockfd, 5);
if (-1 == res)
{
exit(1);
}
int epfd = epoll_create(MaxLen); // 内核事件表的文件描述符
if (-1 == epfd)
{
exit(1);
}
struct epoll_event evs[MaxLen]; // 存放就绪的文件描述符
epoll_add(epfd,sockfd,EPOLL_CTL_ADD);
while (true)
{
int n = epoll_wait(epfd, evs, MaxLen, 5000);
if (-1 == n)
{
continue;
}
else if (0 == n)
{
printf("time out\n");
continue;
}
else
{
for (int i = 0; i < n; ++i) // 区别于select和poll
{
int fd = evs[i].data.fd;
if (evs[i].events & EPOLLIN) //读事件发生
{
if (sockfd == fd)
{
struct sockaddr_in caddr;
int len = sizeof(caddr);
int c = accept(fd, (struct sockaddr *)&caddr, &len);
if (0 >= c)
{
continue;
}
printf("accept client:%d",c);
epoll_add(epfd, c, EPOLL_CTL_ADD);
}
else
{
char buff[256] = {0};
while(true) //采用循环清空接受缓冲区
{
int c = recv(fd,buff,1,0);
if(0 >= c)
{
if(errno != EAGAIN && errno != EWOULDBLOCK )
{
epoll_del(epfd,fd);
close(fd);
printf("one client close\n");
}
break;
}
printf("client(%d):%s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
}
}
客户端参考代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (-1 == res)
{
exit(1);
}
while (1)
{
char buff[128] = {0};
printf("input:\n");
fgets(buff, 128, stdin); // 会取得\n
buff[strlen(buff)-1] = 0; //删除\n
if (strncmp(buff, "end", 3) == 0)
{
break;
}
send(sockfd, buff, strlen(buff), 0);
memset(buff, 0, 128);
recv(sockfd, buff, 127, 0);
printf("buff=%s\n", buff);
}
close(sockfd);
exit(0);
}
运行结果:
select/poll/epoll的区别
select
poll
epoll
总结:
I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。