C++开发学习——IO多路复用epoll代码分析
IO多路复用是指在一个线程中同时处理多个socket连接,其核心优势在于分离了等待数据和处理数据
参考文章https://www.cnblogs.com/horacle/p/17939568
1#include <iostream>2#include <vector>3#include <unistd.h>4#include <sys/socket.h>5#include <netinet/in.h>6#include <arpa/inet.h>7#include <sys/epoll.h> // 核心头文件8#include <cstring>9
10#define MAX_EVENTS 1011#define PORT 888812
13int main() {14 // 1. 创建监听 Socket15 int listen_fd = socket(AF_INET, SOCK_STREAM, 0);73 collapsed lines
16 sockaddr_in addr;17 addr.sin_family = AF_INET;18 addr.sin_addr.s_addr = INADDR_ANY;19 addr.sin_port = htons(PORT);20
21 bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));22 listen(listen_fd, 128);23
24 // 2. 创建 epoll 实例25 int epfd = epoll_create1(0);26 if (epfd == -1) { perror("epoll_create1"); return 1; }27
28 // 3. 将 listen_fd 添加到 epoll 中29 struct epoll_event ev;30 ev.events = EPOLLIN;31 ev.data.fd = listen_fd;32
33 if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {34 perror("epoll_ctl: listen_fd"); return 1;35 }36
37 struct epoll_event events[MAX_EVENTS];38
39 std::cout << "Epoll server running on port " << PORT << "..." << std::endl;40
41 while (true) {42 // 4. 等待事件发生43 int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);44
45 if (nfds == -1) { perror("epoll_wait"); break; }46
47 // 5. 只处理发生的事件48 for (int i = 0; i < nfds; ++i) {49
50 // 情况 A: 监听 socket 就绪 -> 有新连接51 if (events[i].data.fd == listen_fd) {52 sockaddr_in client_addr;53 socklen_t len = sizeof(client_addr);54 int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);55
56 std::cout << "New client connected: " << client_fd << std::endl;57
58 // 将新客户端加入 epoll 监控59 ev.events = EPOLLIN; // 默认是水平触发 (LT)60 ev.data.fd = client_fd;61 epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);62 }63 // 情况 B: 客户端 socket 就绪 -> 有数据发来64 else {65 int client_fd = events[i].data.fd;66 char buffer[1024] = { 0 };67 ssize_t count = read(client_fd, buffer, sizeof(buffer));68
69 if (count > 0) {70 std::cout << "Recv from " << client_fd << ": " << buffer << std::endl;71 // 回显数据72 write(client_fd, buffer, count);73 }74 else {75 std::cout << "Client disconnected: " << client_fd << std::endl;76
77 // 必须先从 epoll 中删除78 epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, nullptr);79 close(client_fd);80 }81 }82 }83 }84
85 close(listen_fd);86 close(epfd);87 return 0;88}我们一块一块分析代码,同时解释代码具体意义
创建监听socket
1 int listen_fd = socket(AF_INET, SOCK_STREAM, 0);AF_INET是指 IPv4协议族,如果是IPv6则写AF_INET6,SOCK_STREAM是指使用的TCP协议,0表示使用默认协议
1 sockaddr_in addr;2 addr.sin_family = AF_INET;3 addr.sin_addr.s_addr = INADDR_ANY;4 addr.sin_port = htons(PORT);AF_INET和上面的协议族应该是一样的,INADDR_ANY是指绑定本机所有网卡,htons(PORT)的作用是将端口转成大端序
1 bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));2 listen(listen_fd, 128);bind函数主要是将之前创建的listen_fd与addr绑定在一起,若端口被占用则返回-1,listen函数就是开启监听,128是指排队上限
创建epoll实例
1 int epfd = epoll_create1(0);2 if (epfd == -1) { perror("epoll_create1"); return 1; }epoll_create1是epoll_create函数的新版本,主要是将size变量替换成了flags变量,其参数0的意思是创建默认的epoll实例
这个函数的底层实现为红黑树,通过文件描述符管理
将listen_fd添加到epoll中
1 struct epoll_event ev;2 ev.events = EPOLLIN;3 ev.data.fd = listen_fd;4
5 if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {6 perror("epoll_ctl: listen_fd"); return 1;7 }程序通过epoll_ctl函数将epfd与待监听事件注册到epfd上,ev.events = EPOLLIN表示水平触发,一般用于读入事件,epoll_ctl函数中EPOLL_CTL_ADD是指注册一个新的fd到epoll中,这个函数的原型如下:
1int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);等待事件发生
1 int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);2 if (nfds == -1) { perror("epoll_wait"); break; }程序通过epoll_wait函数等待事件发生,这是个阻塞函数,它将返回值放在events变量中,同时设置了最多事件数为MAX_EVENTS,-1是超时时间
epoll_wait的返回值是有数据的事件个数,并将nfds个事件放在events的第0至nfds-1位处
给新设备分配fd
1 if (events[i].data.fd == listen_fd) {2 sockaddr_in client_addr;3 socklen_t len = sizeof(client_addr);4 int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);5
6 std::cout << "New client connected: " << client_fd << std::endl;7
8 // 将新客户端加入 epoll 监控9 ev.events = EPOLLIN; // 默认是水平触发 (LT)10 ev.data.fd = client_fd;11 epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);12 }当程序判断到events[i].data.fd == listen_fd时,此时就需要给连接上的设备创建client_fd以备后续的交互,同时通过epoll_ctl将client_fd上发生的数据读入事件注册在epoll中
已有fd进行交互
1 else {2 int client_fd = events[i].data.fd;3 char buffer[1024] = { 0 };4 ssize_t count = read(client_fd, buffer, sizeof(buffer));5
6 if (count > 0) {7 std::cout << "Recv from " << client_fd << ": " << buffer << std::endl;8 // 回显数据9 write(client_fd, buffer, count);10 }11 else {12 std::cout << "Client disconnected: " << client_fd << std::endl;13
14 // 必须先从 epoll 中删除15 epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, nullptr);3 collapsed lines
16 close(client_fd);17 }18 }当程序不满足events[i].data.fd == listen_fd这个条件时,意味着这个设备已经完成了连接可以开始正式交互了,这里就写交互逻辑,我这里是将用户的输入重新输出给用户。若读入失败则表示设备断开连接,则通过epoll_ctl将client_fd对应的事件删除,随后关闭client_fd
退出程序
1 close(listen_fd);2 close(epfd);程序的最后通过close关闭listen_fd与epfd