使用epoll管理多个客户端连接的TCP服务器

avatar
作者
筋斗云
阅读量:0

关于epoll在TCP Server处理多任务(多FD)的解释:

  这段代码展示了如何使用 epoll 在 Linux 上进行高效的 I/O 多路复用,特别是在网络服务器中管理多个客户端连接。我逐步解释代码的工作原理和 epoll 的使用。

概览

  • 创建套接字并监听: 使用 socketbindlisten 创建并启动一个监听套接字。
  • 创建 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; } 

工作原理

  1. 创建 epoll 实例: 使用 epoll_create 创建一个 epoll 实例,该实例用于管理所有需要监视的文件描述符。
  2. 初始化监听套接字: 调用 sockfd_init 函数创建并配置监听套接字。
  3. 将监听套接字添加到 epoll 使用 epoll_ctl 将监听套接字添加到 epoll 实例中,监视 EPOLLIN 事件(表示有新的连接请求)。
  4. 事件循环:
    • 调用 epoll_wait 等待事件发生。
    • 对于监听套接字上的 EPOLLIN 事件,调用 accept 接受新的连接并将新连接的套接字添加到 epoll 实例中。
    • 对于客户端套接字上的 EPOLLIN 事件,调用 read 读取数据。如果读取失败或连接关闭,将该套接字从 epoll 实例中移除并关闭套接字。
  5. 处理读事件: 当客户端发送数据到服务器,read 函数读取数据并输出到控制台。

epoll 的优点

  • 高效管理大量文件描述符:epoll 可以处理成千上万的文件描述符,性能不会随着文件描述符数量的增加而线性下降。
  • 边缘触发和水平触发:epoll 支持两种事件触发模式,适应不同的应用场景。
  • 避免忙等待: 通过 epoll_wait 等待事件发生,避免了传统 selectpoll 的忙等待问题。

总的来说,这段代码展示了一个使用 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, &current->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; } 

代码解释

  1. 链表节点结构定义

    • struct ClientNode 定义了链表节点结构,每个节点包含一个客户端的文件描述符 (fd)、地址信息 (addr) 和指向下一个节点的指针 (next)。
  2. 添加新客户端到链表

    • add_client 函数创建一个新节点并将其添加到链表头。
  3. 从链表中移除客户端

    • remove_client 函数根据文件描述符从链表中移除相应节点。
  4. 遍历链表并打印客户端信息

    • print_clients 函数遍历链表,打印每个节点中保存的客户端信息。
  5. 清理链表

    • free_clients 函数释放链表中的所有节点并关闭相应的套接字。
  6. 主函数修改

    • 在主函数中,定义了一个链表头指针 clients
    • 当接受到新的客户端连接时,将新客户端添加到链表。
    • 当客户端连接断开时,从链表中移除该客户端。
    • 在每次处理完事件后,调用 print_clients 打印当前在线的所有客户端。

这样,我们就实现了一个使用(无头)链表管理在线客户端的简单服务器模型。


广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!