关于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
打印当前在线的所有客户端。
- 在主函数中,定义了一个链表头指针
这样,我们就实现了一个使用(无头)链表管理在线客户端的简单服务器模型。