目录
一、概念
二、语法
1.select
1.1 select函数的语法
1.2 文件描述符集合操作
1.3 select函数的优缺点
2.epoll
2.1 epoll语法
2.2 epoll的工作模式
2.3 epoll的优缺点
三、select服务端代码
四、epoll服务端代码
五、客户端代码
一、概念
IO多路复用是一种同步的I/O模型,它允许一个进程同时监视多个文件描述符,一旦某个文件描述符就绪(可以进行I/O操作),就能够通知程序进行相应的读写操作。
IO多路复用有三种实现方式:select、poll、epoll。
二、语法
1.select
select是最早的I/O多路复用技术之一,它使用fd_set数据结构来存储和跟踪文件描述符。
select是基于线性方式处理待检测集合的,因此每次都要遍历集合;对于返回的集合,还需要判断文件描述符是否就绪。
1.1 select函数的语法
select ( int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout );
- nfds:监控的文件描述符集里最大文件描述符加1。
- readfds:文件描述符集合,内核会监视此集合中的文件是否有数据可读,传入传出参数。
- writefds:文件描述符集合,内核会监视此集合中的文件是否有数据可写,传入传出参数。
- exceptfds:文件描述符集合,用于监视此集合中的文件是否有异常情况,传入传出参数。
- timeout:设置为NULL:阻塞,若检测到就绪的文件描述符则返回其数量;设置时间:等待固定时间,若没有就绪的文件描述符则返回0。
1.2 文件描述符集合操作
- FD_ZERO(fd_set *set):清空文件描述符集合。
- FD_SET(int fd, fd_set *set):将文件描述符fd添加到集合set中。
- FD_CLR(int fd, fd_set *set):从集合set中移除文件描述符fd。
- FD_ISSET(int fd, fd_set *set):检查文件描述符fd是否在集合set中。
1.3 select函数的优缺点
- 优点:适用于多种操作系统,具有较好的兼容性。
- 缺点:当文件描述符数量较多时,效率会比较低。此外,select有一个固定的文件描述符数量限制,通常为1024。
2.epoll
epoll是一种高效且可扩展的I/O多路复用技术。epoll是基于红黑树来处理待检测集合的,使用回调机制,处理效率高;其返回的集合中的文件描述符都是就绪的,无需再次进行检测。
epoll主要通过以下三个函数来实现其功能:epoll_create、epoll_ctl、epoll_wait。
2.1 epoll语法
①epoll_create函数
epoll_create (int size);
创建一个epoll对象,返回一个文件描述符,用于后续的操作,参数size大于0即可。
②epoll_ctl函数
epoll_ctl ( int epfd, int op, int fd,
struct epoll_event *event );
向epoll对象中添加、修改或删除文件描述符。
- epfd:epoll_create函数返回的epoll文件描述符。
- op:操作类型,EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或EPOLL_CTL_DEL(删除)。
- fd:要监听的文件描述符。
- event:指向epoll_event结构体的指针,用于指定要监听的事件类型。
epoll_event和epoll_data:
struct epoll_event
{
uint32_t events;
epoll_data_t data;
};typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
epoll_event结构体用于描述事件,成员events表示事件类型:
- EPOLLIN:读事件,接收数据,检测读缓冲区,如果可读则该文件描述符已就绪。
- EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写则该文件描述符已就绪。
- EPOLLET:设置为边沿模式,默认是水平模式。
epoll_data为联合体,通常使用int类型的fd,用于存储发生对应事件的文件描述符。
③epoll_wait函数
epoll_wait ( int epfd, struct epoll_event *events,
int maxevents, int timeout );
等待epoll对象中的事件发生,返回发生事件的文件描述符集合。
- epfd:epoll_create函数返回的epoll文件描述符。
- events:一个数组,用来存储就绪的事件信息。
- maxevents:数组events的大小,即最多可以返回多少个就绪的文件描述符。
- timeout:表示epoll_wait阻塞等待的时间(毫秒)。若为负数则会阻塞,直到有事件就绪;若为0则不阻塞,立即返回当前已就绪的事件。
2.2 epoll的工作模式
①水平触发(LT)模式:
水平触发是epoll的默认工作模式。在这种模式下,当文件描述符上的事件就绪时,epoll_wait函数会返回,并且如果该事件没有被处理,epoll_wait函数会在下一次调用时再次返回,直到该事件被处理。例如,当一个套接字上有数据可读时,epoll_wait会返回,并且如果数据没有被读取,epoll_wait会在下一次调用时再次返回,直到数据被读取。
②边沿触发(ET)模式:
边沿触发模式下,epoll_wait函数只会在文件描述符的状态发生变化时返回。例如,当一个套接字上有数据可读时,epoll_wait会返回,但是如果数据没有被读取,epoll_wait不会在下一次调用时再次返回,直到有新的数据到达。因此在边沿触发模式下,程序需要在一次epoll_wait调用返回后,立即处理所有就绪的事件。
③总结:
- 边沿触发模式通常比水平触发模式的性能更高,因为它调用epoll_wait函数的次数比较少。
- 水平触发模式更适合处理阻塞式I/O,而边沿触发模式更适合处理非阻塞式I/O。
epoll在边沿模式下,必须将套接字设置为非阻塞模式,此时需要循环读取读缓冲区的数据,读取完后recv函数会返回-1,此时需要特殊处理。
2.3 epoll的优缺点
- 优点:高效的事件驱动模型,支持大量并发连接,没有固定的文件描述符数量限制。
- 缺点:仅适用于Linux系统,不具备跨平台性。
三、select服务端代码
#include<iostream>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>using namespace std;int main(int argc, char* argv[])
{int lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1) {cerr << "error" << endl;return -1;}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(8080);local.sin_addr.s_addr = INADDR_ANY;if (bind(lfd, (struct sockaddr*)&local, sizeof(local)) == -1) {cerr << "error" << endl;return -1;}if (listen(lfd, 128) == -1) {cerr << "error" << endl;return -1;}fd_set readset;FD_ZERO(&readset);FD_SET(lfd, &readset);int maxfd = lfd;while (true) {fd_set tmp = readset;int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);if (FD_ISSET(lfd, &tmp)) {int cfd = accept(lfd, NULL, NULL);FD_SET(cfd, &readset);maxfd = max(maxfd, cfd);cout << "connect: " << cfd << endl;}for (int i = 0; i <= maxfd; i++) {if (i != lfd && FD_ISSET(i, &tmp)) {char buffer[1024] = { 0 };int len = recv(i, buffer, sizeof buffer, 0);if (len == -1) {cerr << "error" << endl;return -1;}else if (len == 0) {cout << "client disconnect: " << i << endl;FD_CLR(i, &readset);close(i);break;}cout << buffer << endl;for (int i = 0; i < len; i++) {buffer[i] = toupper(buffer[i]);}len = send(i, buffer, strlen(buffer) + 1, 0);if (len == -1) {cerr << "error" << endl;return -1;}}}}close(lfd);return 0;
}
四、epoll服务端代码
#include<iostream>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>using namespace std;int main(int argc, char* argv[])
{int lfd = socket(AF_INET, SOCK_STREAM, 0);if (lfd == -1) {cerr << "error" << endl;return -1;}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(8080);local.sin_addr.s_addr = INADDR_ANY;if (bind(lfd, (struct sockaddr*)&local, sizeof(local)) == -1) {cerr << "error" << endl;return -1;}if (listen(lfd, 128) == -1) {cerr << "error" << endl;return -1;}int epfd = epoll_create(1);struct epoll_event event;event.events = EPOLLIN;event.data.fd = lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &event);struct epoll_event events[1024];while (true) {int fds = epoll_wait(epfd, events, 1024, -1);for (int i = 0; i < fds; i++) {int fd = events[i].data.fd;if (fd == lfd) {int cfd = accept(lfd, NULL, NULL);struct epoll_event client_event;int flag = fcntl(cfd, F_GETFL);flag |= O_NONBLOCK;fcntl(cfd, F_SETFL, flag);client_event.events = EPOLLIN | EPOLLET;client_event.data.fd = cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &client_event);cout << "connect: " << cfd << endl;}else {while(true) {char buffer[128] = { 0 };int len = recv(fd, buffer, sizeof buffer, 0);if (len == 0) {cout << "client disconnect: " << fd << endl;epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);close(fd);break;}else if (len > 0) {cout << buffer << endl;for (int i = 0; i < len; i++) {buffer[i] = toupper(buffer[i]);}len = send(fd, buffer, strlen(buffer) + 1, 0);if (len == -1) {cerr << "error" << endl;return -1;}}else if (len == -1) {if (errno = EAGAIN) {cout << "Data read complete." << endl;break;}else {cerr << "error" << endl;return -1;}}}}}}close(lfd);return 0;
}
五、客户端代码
#include<iostream>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>using namespace std;int main(void)
{int client_socket = socket(AF_INET, SOCK_STREAM, 0);if (client_socket == -1) {cerr << "error" << endl;return -1;}struct sockaddr_in target;target.sin_family = AF_INET;target.sin_port = htons(8080);inet_pton(AF_INET, "127.0.0.1", &target.sin_addr.s_addr);if (connect(client_socket, (struct sockaddr*)&target, sizeof target) == -1) {cerr << "error" << endl;close(client_socket);return -1;}while (true) {char buffer1[1024] = { 0 };cout << "enter: ";cin >> buffer1;send(client_socket, buffer1, strlen(buffer1), 0);char buffer2[1024] = { 0 };int ret = recv(client_socket, buffer2, sizeof buffer2, 0);if (ret <= 0) {cout << "server disconnect." << endl;}cout << buffer2 << endl;}close(client_socket);return 0;
}
参考内容:
爱编程的大丙