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

关于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 打印当前在线的所有客户端。

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


  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值