阅读量:0
关于epoll在TCP Server处理多任务(多FD)的解释:
这段代码展示了如何使用 epoll
在 Linux 上进行高效的 I/O 多路复用,特别是在网络服务器中管理多个客户端连接。我逐步解释代码的工作原理和 epoll
的使用。
概览
- 创建套接字并监听: 使用
socket
、bind
、listen
创建并启动一个监听套接字。 - 创建
epoll
实例: 使用epoll_create
创建一个epoll
实例。 - 注册监听套接字到
epoll
: 使用epoll_ctl
将监听套接字添加到epoll
实例中。 - 等待事件: 使用
epoll_wait
等待文件描述符上的事件。 - 处理事件: 根据触发的事件类型(新的连接或现有连接上的数据)进行相应处理。
代码详解
套接字初始化
int sockfd_init() { int sockfd, ret; sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("socket"); return -1; } //设置套接字端口复用选项 int opt = 1; ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); if(ret < 0) { perror("setsockopt"); return -1; } struct sockaddr_in seraddr; seraddr.sin_family = AF_INET; seraddr.sin_port = htons(8888); inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr); ret = bind(sockfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr)); if(ret < 0) { perror("bind"); return -1; } ret = listen(sockfd, 5); if(ret < 0) { perror("listen"); return -1; } return sockfd; }
- 创建套接字: 使用
socket
创建一个 IPv4 流套接字。 - 设置端口复用: 使用
setsockopt
设置SO_REUSEADDR
选项,允许端口复用。 - 绑定地址和端口: 使用
bind
绑定本地地址和端口。 - 监听: 使用
listen
使套接字进入监听状态,准备接受连接。
主函数
int main() { int sockfd, ret, cfd, efd; struct sockaddr_in cliaddr; int addrlen = sizeof(struct sockaddr_in); //创建集合空间 efd = epoll_create(100); if(efd < 0) { perror("epoll_create"); return -1; } //创建监听套接字 sockfd = sockfd_init(); if(sockfd < 0) { return -1; } //将套接字加入集合 struct epoll_event ev, evs[10]; ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &ev); int count; char buff[1024]; //监听集合中所有的文件描述符 while(1) { printf("wait..\n"); count = epoll_wait(efd, evs, 10, -1); printf("wait over..\n"); if(count < 0) { perror("epoll_wait"); break; } for(int i=0; i<count; i++) { int tfd = evs[i].data.fd; if(tfd == sockfd) //有客户端请求连接 { //1、接收客户端 printf("accept...\n"); cfd = accept(sockfd, NULL, NULL); printf("accept over...\n"); if(cfd < 0) { perror("accept"); continue; } //2、cfd加入集合中 struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = cfd; epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev); } else //建立连接的客户端发来数据 { printf("read...\n"); ret = read(tfd, buff, 1024); printf("read over...\n"); if(ret < 0) { //1、打印错误信息 perror("read"); //2、关闭套接字 close(tfd); //3、从集合中移除 epoll_ctl(efd, EPOLL_CTL_DEL, tfd, NULL); continue; } else if(0 == ret) { //1、打印错误信息 printf("tcp broken...\n"); //2、关闭套接字 close(tfd); //3、从集合中移除 epoll_ctl(efd, EPOLL_CTL_DEL, tfd, NULL); continue; } buff[ret] = '\0'; printf("buff: %s\n", buff); } } } return 0; }
工作原理
- 创建
epoll
实例: 使用epoll_create
创建一个epoll
实例,该实例用于管理所有需要监视的文件描述符。 - 初始化监听套接字: 调用
sockfd_init
函数创建并配置监听套接字。 - 将监听套接字添加到
epoll
: 使用epoll_ctl
将监听套接字添加到epoll
实例中,监视EPOLLIN
事件(表示有新的连接请求)。 - 事件循环:
- 调用
epoll_wait
等待事件发生。 - 对于监听套接字上的
EPOLLIN
事件,调用accept
接受新的连接并将新连接的套接字添加到epoll
实例中。 - 对于客户端套接字上的
EPOLLIN
事件,调用read
读取数据。如果读取失败或连接关闭,将该套接字从epoll
实例中移除并关闭套接字。
- 调用
- 处理读事件: 当客户端发送数据到服务器,
read
函数读取数据并输出到控制台。
epoll
的优点
- 高效管理大量文件描述符:
epoll
可以处理成千上万的文件描述符,性能不会随着文件描述符数量的增加而线性下降。 - 边缘触发和水平触发:
epoll
支持两种事件触发模式,适应不同的应用场景。 - 避免忙等待: 通过
epoll_wait
等待事件发生,避免了传统select
或poll
的忙等待问题。
总的来说,这段代码展示了一个使用 epoll
进行高效 I/O 多路复用的小型服务器模型。
定义一个(无头)链表来装载在线客户端:
在这个示例中,我们将定义一个(无头)链表来管理在线客户端。链表节点将包含客户端的文件描述符和其他必要信息,如客户端的地址信息。我们还会编写相关的函数来管理这个链表,包括添加客户端、移除客户端和遍历客户端列表。
首先,定义链表节点的结构:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <sys/epoll.h> #include <string.h> // 链表节点结构 struct ClientNode { int fd; struct sockaddr_in addr; struct ClientNode* next; }; // 添加新客户端到链表 void add_client(struct ClientNode** head, int fd, struct sockaddr_in addr) { struct ClientNode* new_node = (struct ClientNode*)malloc(sizeof(struct ClientNode)); new_node->fd = fd; new_node->addr = addr; new_node->next = *head; *head = new_node; } // 从链表中移除客户端 void remove_client(struct ClientNode** head, int fd) { struct ClientNode* temp = *head, *prev = NULL; if (temp != NULL && temp->fd == fd) { *head = temp->next; free(temp); return; } while (temp != NULL && temp->fd != fd) { prev = temp; temp = temp->next; } if (temp == NULL) return; prev->next = temp->next; free(temp); } // 遍历链表并打印客户端信息 void print_clients(struct ClientNode* head) { struct ClientNode* current = head; while (current != NULL) { char ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, ¤t->addr.sin_addr, ip, INET_ADDRSTRLEN); printf("Client FD: %d, IP: %s, Port: %d\n", current->fd, ip, ntohs(current->addr.sin_port)); current = current->next; } } // 清理链表 void free_clients(struct ClientNode* head) { struct ClientNode* tmp; while (head != NULL) { tmp = head; head = head->next; close(tmp->fd); // 关闭套接字 free(tmp); } } int main() { int sockfd, ret, cfd, efd; struct sockaddr_in cliaddr; socklen_t addrlen = sizeof(struct sockaddr_in); struct ClientNode* clients = NULL; // 链表头指针 efd = epoll_create(100); if (efd < 0) { perror("epoll_create"); return -1; } sockfd = sockfd_init(); if (sockfd < 0) { return -1; } struct epoll_event ev, evs[10]; ev.events = EPOLLIN; ev.data.fd = sockfd; epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &ev); int count; char buff[1024]; while (1) { printf("wait..\n"); count = epoll_wait(efd, evs, 10, -1); printf("wait over..\n"); if (count < 0) { perror("epoll_wait"); break; } for (int i = 0; i < count; i++) { int tfd = evs[i].data.fd; if (tfd == sockfd) { printf("accept...\n"); cfd = accept(sockfd, (struct sockaddr*)&cliaddr, &addrlen); printf("accept over...\n"); if (cfd < 0) { perror("accept"); continue; } add_client(&clients, cfd, cliaddr); ev.events = EPOLLIN; ev.data.fd = cfd; epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev); } else { printf("read...\n"); ret = read(tfd, buff, 1024); printf("read over...\n"); if (ret < 0) { perror("read"); close(tfd); epoll_ctl(efd, EPOLL_CTL_DEL, tfd, NULL); remove_client(&clients, tfd); continue; } else if (ret == 0) { printf("tcp broken...\n"); close(tfd); epoll_ctl(efd, EPOLL_CTL_DEL, tfd, NULL); remove_client(&clients, tfd); continue; } buff[ret] = '\0'; printf("buff: %s\n", buff); } } // 打印当前在线的所有客户端 print_clients(clients); } free_clients(clients); return 0; }
代码解释
链表节点结构定义:
struct ClientNode
定义了链表节点结构,每个节点包含一个客户端的文件描述符 (fd
)、地址信息 (addr
) 和指向下一个节点的指针 (next
)。
添加新客户端到链表:
add_client
函数创建一个新节点并将其添加到链表头。
从链表中移除客户端:
remove_client
函数根据文件描述符从链表中移除相应节点。
遍历链表并打印客户端信息:
print_clients
函数遍历链表,打印每个节点中保存的客户端信息。
清理链表:
free_clients
函数释放链表中的所有节点并关闭相应的套接字。
主函数修改:
- 在主函数中,定义了一个链表头指针
clients
。 - 当接受到新的客户端连接时,将新客户端添加到链表。
- 当客户端连接断开时,从链表中移除该客户端。
- 在每次处理完事件后,调用
print_clients
打印当前在线的所有客户端。
- 在主函数中,定义了一个链表头指针
这样,我们就实现了一个使用(无头)链表管理在线客户端的简单服务器模型。