- 多路IO
转接
-
1 -
select
- 原理:
select
委托内核监听多个文件描述符的变化,当内核监听到文件描述符变化时,select
函数会返回有多少个文件描述符发生了变化,但不会告诉用户是哪些个文件描述符发生了变化,用户需要自己遍历文件描述符集合来判断是哪些文件描述符有数据到达。 - 数据结构:由于
select
是通过数组实现的,数组大小为1024个bit
,所以和1024
个文件描述符相对应,因此不能突破1024
的限制,最大只能监听1024
个文件描述符。每一个bit
对应一个文件描述符,将要内核监听的fd_set
集合中的对应文件描述符的bit
位置为1
,内核就知道了需要监听这个比特位所对应的文件描述符。 - 优点:跨平台,
Linux和Windows
都可以用。而且select
比多进程和多线程效率高的原因在于,它可以一次监听多个文件描述符的变化,而多进程和多线程同一时刻只能监听一个文件描述符的变化,而且创建多进程和多线程需要耗费大量的系统资源。 - 函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
为你要内核监听的最大的文件描述符 + 1,因为内核需要遍历fd_set
数组,只需要遍历到你所拥有的最大文件描述符即可。readfds
文件读集合,为传入传出参数,这个集合是你需要内核监听的文件描述符的集合,当内核监听到有文件描述符所对应的读缓存区有数据到达时,会将该位置1
,没有数据到达的文件描述符的标志位置0
,所以内核返回的是修改过的读集合。writefds
文件写集合,监听文件描述符对应的写缓冲区的,同上。exceptfds
异常文件描述符集合,委托内核监听哪些文件描述符发生了异常,同上。timeout
规定select
在多长时间内返回,NULL
为直到有文件描述符发生变化才返回timeval
设置为{0, 0}
表示不阻塞timeval
设置为{x, 0}
表示阻塞x秒
后返回
- 返回值
int
,表示发生状态改变的文件描述符的数量。
- 要创建
fd_set
集合,需要配合以下的小函数使用void FD_CLR(int fd, fd_set *set);
从集合中删除文件描述符int FD_ISSET(int fd, fd_set *set);
判断此文件描述符是否在集合中,在返回1
,不在返回0
void FD_SET(int fd, fd_set *set);
将文件描述符添加到集合中void FD_ZERO(fd_set *set);
将fd_set
集合的每一个标志位清0
- 例子
// // Created by liubin on 2020/11/18. // #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/select.h> #include <arpa/inet.h> #include <string.h> #define PORT 9999 int main() { int sfd = -1; int max_fd = -1; int cli_fd = -1; int ret = -1; int idx = -1; int num = -1; char buf[1024] = {0}; int fg = 1; struct sockaddr_in addr; struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); fd_set rd_set ; fd_set temp_set ; // 1- create socket sfd = socket(AF_INET, SOCK_STREAM, 0); // 2- setsockopt setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &fg, sizeof(fg)); // 2- bind addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); // 3- listen listen(sfd, 128); // 4- select FD_ZERO(&rd_set); FD_SET(sfd, &rd_set); max_fd = sfd; while (1) { temp_set = rd_set; ret = select(max_fd + 1, &temp_set, NULL, NULL, NULL); if (FD_ISSET(sfd, &temp_set)) { // have new connection cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len); FD_SET(cli_fd, &rd_set); max_fd = max_fd > cli_fd ? max_fd : cli_fd; } // have data for (idx = sfd + 1; idx <= max_fd; idx++) { if (FD_ISSET(idx, &temp_set)) { memset(buf, 0, 1024); num = read(idx, buf, sizeof(buf)); if (-1 == num) { perror("Error : read"); return -1; } if (0 == num) { // client close FD_CLR(idx, &rd_set); close(idx); printf("client close the connect!\n"); } if (num > 0) { printf("recv data from client [%s]\n", buf); } } } } }
- 原理:
-
2-
poll
- 原理:
poll
和select
原理相似,只是函数参数略有不同而已,poll
也是需要将委托内核监听的文件描述符传递给函数,当内核监听到委托的文件描述符集合中的状态发生变化时,就返回发生变化的文件描述符的个数。 - 数据结构:
poll
内部是通过链表实现的。因此可以突破1024
的限制,也就是说可以监听的文件描述符个数多余1024
个。 - 优点:可以突破
1024
的限制,非跨平台,只能在Linux
系统使用。 - 函数:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd { int fd; /* 需要内核监听的文件描述符 */ short events; /* 需要监听文件描述符的事件 POLLIN(读缓冲区)、POLLOUT(写缓冲区)、POLLERR(异常) */ short revents; /* 返回监听到的事件 */ };
fds
一个结构体数组,需要内核监听的文件描述符和需要监听的文件描述的事件。传入参数nfds
当前需要内核监听的最大文件描述符 + 1timeout
毫秒值-1
表示直到有文件描述符发生变化才解阻塞0
表示立即返回>0
表示阻塞x
秒后返回
- 返回值
int
表示要监听的文件描述符状态发生变化的数量。
- 例子
// // Created by liubin on 2020/11/18. // #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <poll.h> #include <string.h> #include <arpa/inet.h> #define PORT 9999 int main() { int ret = -1; int num = -1; int idx = -1; int sfd = -1; int cli_fd = -1; int fg = 1; int max_fd = -1; char buf[1024] = {0}; struct sockaddr_in addr; struct sockaddr_in cli_addr; struct pollfd p_fd[200]; socklen_t cli_len = sizeof(cli_addr); // 1- create socket sfd = socket(AF_INET, SOCK_STREAM, 0); // 2- setsockopt setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg)); // 3- bind addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); // 4- listen listen(sfd, 128); // 5- poll // init pollfd for (idx = 0; idx < 200; idx++) { p_fd[idx].fd = -1; p_fd[idx].events = POLLIN; } // insert sfd to array of pollfd p_fd[0].fd = sfd; max_fd = 0; while (1) { // -1 wait for change of fd ret = poll(p_fd, max_fd + 1, -1); if (-1 == ret) { perror("Error: poll"); return -1; } if (p_fd[0].revents & POLLIN) { // have new connection cli_fd = accept(p_fd[0].fd, (struct sockaddr *)&cli_addr, &cli_len); printf("new client connect.....IP=%s, port=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); for (idx = 1; idx < 200; idx++) { if (-1 == p_fd[idx].fd) { p_fd[idx].fd = cli_fd; max_fd = max_fd > idx ? max_fd : idx; break; } } } // hava data for (idx = 1; idx <= max_fd; idx++) { if (p_fd[idx].revents & POLLIN) { memset(buf, 0, 1024); num = recv(p_fd[idx].fd, buf, sizeof(buf), 0); if (-1 == num) { perror("Error: recv"); return -1; } if (0 == num) { // client close close(p_fd[idx].fd); p_fd[idx].fd = -1; printf("client close connect!\n"); } if (num > 0) { printf("recv data [%s]\n", buf); send(p_fd[idx].fd, buf, strlen(buf), 0); } } } } }
- 原理:
-
3 -
epoll
- 原理:同样是委托内核监听文件描述符状态的变化,然后返回给我们有几个文件描述符状态发生的变化,而且会告诉我们是哪几个,我们不再需要再去遍历监听的文件描述符集合去找到底是哪几个发生了变化。
- 数据结构:
epoll
内部是通过红黑树实现的,红黑树类似于平衡二叉树,它通过epoll_create
创建树的根节点,然后将一个个要监听的文件描述符挂在树上,不是挂文件描述符,而是挂struct epoll_event
结构体,这个结构体里面有要监听的文件描述符的事件和其他信息。当要监听的文件描述符状态发生变化时,epoll
会将发生变化的文件描述符所对应的struct epoll_event
结构体返回,因为是通过树实现的,因此遍历就比较快,比select
的数组实现和poll
的链表实现等遍历都要快很多。 - 优点:效率最高,比
select
的数组实现和poll
的链表实现等遍历都要快很多。 - 函数:
int epoll_create(int size);
size
参数为要监听的文件描述符数量,是个软限制,当文件描述符超过这个数量时会自动增加。- 返回值:返回树的根节点
epfd
,这个根节点用来在后续在树上挂要监听的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event { uint32_t events; /* 委托内核监听的事件 */ epoll_data_t data; /* 与要监听的文件描述符相关的信息 */ }; /* 可以只用fd或者用指针传递与该文件描述符绑定的更多信息 */ typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
epfd
即树的根节点op
对树进行的操作,在树中增加、修改、删除节点等操作fd
要操作的文件描述符event
要操作的文件描述符所对应的事件EPOLLIN(监听读缓冲区),EPOLLOUT(监听写缓冲区),EPOLLERR(文件描述符发生异常)
- 返回值:成功返回
0
,失败返回-1
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
epfd
树的根节点events
返回的一个数组的首地址,数组中是发生变化的文件描述符所对应的结构体,传出参数。maxevents
返回的发生状态变化的最大数量timeout
毫秒值-1
表示直到有文件描述符发生变化才解阻塞0
表示立即返回>0
表示阻塞x
秒后返回
- 返回值
int
表示要监听的文件描述符状态发生变化的数量。
- 实例
// // Created by liubin on 2020/11/19. // #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define PORT 9999 #define MAX_CONN 256 int main() { int ret = -1; int count = -1; int idx = -1; int num = -1; int sfd = -1; int epfd = -1; int cli_fd = -1; int fg = 1; char buf[1024] = {0}; struct sockaddr_in addr; struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); struct epoll_event ev = {0}; struct epoll_event cli_ev = {0}; struct epoll_event ev_array[MAX_CONN] = {0}; // 1- create socket sfd = socket(AF_INET, SOCK_STREAM, 0); // 2- setsockopt setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg)); // 3- bind addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = INADDR_ANY; bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); // 4- listen listen(sfd, 128); // 5- epoll epfd = epoll_create(MAX_CONN); ev.events = EPOLLIN; ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); while (1) { num = epoll_wait(epfd, ev_array, MAX_CONN, -1); for (idx = 0; idx < num; idx++) { if (sfd == ev_array[idx].data.fd) { // have new connection cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len); cli_ev.events = EPOLLIN; cli_ev.data.fd = cli_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev); printf("have new client connect.........IP=%s, port=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); } if (sfd != ev_array[idx].data.fd) { // hava data memset(buf, 0, sizeof(buf)); count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0); if (0 == count) { // client close ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL); printf("ret = %d\n", ret); close(ev_array[idx].data.fd); printf("client close the connect!\n"); } if (count > 0) { printf("recv data from client [%s]\n", buf); send(ev_array[idx].data.fd, buf, strlen(buf), 0); } } } } return 0; }
epoll
的三种工作模式(以读缓冲区举例)- 1 - 水平触发模式-LT
- 只要对应的文件描述符所对应的读缓冲区有数据,
epoll_wait
就返回,与客户端发送信息的次数无关。
// // Created by liubin on 2020/11/19. // #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define PORT 9999 #define MAX_CONN 256 int main() { int times = 0; int ret = -1; int count = -1; int idx = -1; int num = -1; int sfd = -1; int epfd = -1; int cli_fd = -1; int fg = 1; char buf[1] = {0}; struct sockaddr_in addr; struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); struct epoll_event ev = {0}; struct epoll_event cli_ev = {0}; struct epoll_event ev_array[MAX_CONN] = {0}; // 1- create socket sfd = socket(AF_INET, SOCK_STREAM, 0); // 2- setsockopt setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg)); // 3- bind addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = INADDR_ANY; bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); // 4- listen listen(sfd, 128); // 5- epoll epfd = epoll_create(MAX_CONN); ev.events = EPOLLIN; ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); while (1) { num = epoll_wait(epfd, ev_array, MAX_CONN, -1); for (idx = 0; idx < num; idx++) { if (sfd == ev_array[idx].data.fd) { // have new connection cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len); cli_ev.events = EPOLLIN; cli_ev.data.fd = cli_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev); printf("have new client connect.........IP=%s, port=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); } if (sfd != ev_array[idx].data.fd) { // hava data memset(buf, 0, sizeof(buf)); // read 1 bytes one time count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0); if (0 == count) { // client close ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL); printf("ret = %d\n", ret); close(ev_array[idx].data.fd); printf("client close the connect!\n"); } if (count > 0) { // printf("recv data from client [%s]\n", buf); times++; write(STDOUT_FILENO, buf, 1); printf("--------%d--------\n", times); send(ev_array[idx].data.fd, buf, 1, 0); } } } } return 0; }
- 只要对应的文件描述符所对应的读缓冲区有数据,
- 2 - 边沿触发模式-ET
- 客户端发送一次数据,服务器的
epoll_wait
就返回一次,不管你上一次的读缓冲区数据有灭有读完,没有读完的上次数据仍在读缓冲区内。将文件描述符设置为EPOLLET
即可。
// // Created by liubin on 2020/11/19. // #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #define PORT 9999 #define MAX_CONN 256 int main() { int times = 0; int ret = -1; int count = -1; int idx = -1; int num = -1; int sfd = -1; int epfd = -1; int cli_fd = -1; int fg = 1; char buf[1] = {0}; struct sockaddr_in addr; struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); struct epoll_event ev = {0}; struct epoll_event cli_ev = {0}; struct epoll_event ev_array[MAX_CONN] = {0}; // 1- create socket sfd = socket(AF_INET, SOCK_STREAM, 0); // 2- setsockopt setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg)); // 3- bind addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = INADDR_ANY; bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); // 4- listen listen(sfd, 128); // 5- epoll epfd = epoll_create(MAX_CONN); ev.events = EPOLLIN | EPOLLET; ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); while (1) { num = epoll_wait(epfd, ev_array, MAX_CONN, -1); for (idx = 0; idx < num; idx++) { if (sfd == ev_array[idx].data.fd) { // have new connection cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len); // Set ET Mode cli_ev.events = EPOLLIN | EPOLLET; cli_ev.data.fd = cli_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev); printf("have new client connect.........IP=%s, port=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); } if (sfd != ev_array[idx].data.fd) { // hava data memset(buf, 0, sizeof(buf)); // read 1 bytes one time count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0); if (0 == count) { // client close ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL); printf("ret = %d\n", ret); ret = close(ev_array[idx].data.fd); printf("close ret = %d\n", ret); printf("client close the connect!\n"); } if (count > 0) { // printf("recv data from client [%s]\n", buf); times++; write(STDOUT_FILENO, buf, 1); printf("--------%d--------\n", times); send(ev_array[idx].data.fd, buf, 1, 0); } } } } return 0; }
- 客户端发送一次数据,服务器的
- 3 - 边沿非阻塞模式
- 第二种方式在加上将文件描述符设置为
O_NONBLOCK
,为了一次读完缓冲区内的数据,可以循环读数据,返回-1和errno=EAGAIN
表示该次数据读完,返回0
表示客户端断开连接,这种模式工作效率最高。
// // Created by liubin on 2020/11/19. // #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/epoll.h> #include <errno.h> #include <fcntl.h> #define PORT 9999 #define MAX_CONN 256 int main() { int flag = -1; int times = 0; int ret = -1; int count = -1; int idx = -1; int num = -1; int sfd = -1; int epfd = -1; int cli_fd = -1; int fg = 1; char buf[1] = {0}; struct sockaddr_in addr; struct sockaddr_in cli_addr; socklen_t cli_len = sizeof(cli_addr); struct epoll_event ev = {0}; struct epoll_event cli_ev = {0}; struct epoll_event ev_array[MAX_CONN] = {0}; // 1- create socket sfd = socket(AF_INET, SOCK_STREAM, 0); // 2- setsockopt setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &fg, sizeof(fg)); // 3- bind addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = INADDR_ANY; bind(sfd, (struct sockaddr *)&addr, sizeof(addr)); // 4- listen listen(sfd, 128); // 5- epoll epfd = epoll_create(MAX_CONN); ev.events = EPOLLIN; ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); while (1) { num = epoll_wait(epfd, ev_array, MAX_CONN, -1); for (idx = 0; idx < num; idx++) { if (sfd == ev_array[idx].data.fd) { // have new connection cli_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len); flag = fcntl(cli_fd, F_GETFL); flag |= O_NONBLOCK; fcntl(cli_fd, F_SETFL, flag); // Set NonBlock Mode cli_ev.events = EPOLLIN | EPOLLET; cli_ev.data.fd = cli_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &cli_ev); printf("have new client connect.........IP=%s, port=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); } if (sfd != ev_array[idx].data.fd) { // hava data memset(buf, 0, sizeof(buf)); // read 1 bytes one time while ( (count = recv(ev_array[idx].data.fd, buf, sizeof(buf), 0)) > 0) { write(STDOUT_FILENO, buf, 1); send(ev_array[idx].data.fd, buf, 1, 0); } if (0 == count) { // client close ret = epoll_ctl(epfd, EPOLL_CTL_DEL, ev_array[idx].data.fd, NULL); printf("ret = %d\n", ret); ret = close(ev_array[idx].data.fd); printf("close ret = %d\n", ret); printf("client close the connect!\n"); } if (-1 == count && errno == EAGAIN) { continue; } } } } return 0; }
- 第二种方式在加上将文件描述符设置为
- 1 - 水平触发模式-LT