【Linux C/C++开发】epoll模式的开源库及原生socket实现

前言

epoll模式涉及到系统底层的I/O多路复用机制,可以处理高并发的场景。本文使用开源的libuv库以及原生的scoket来分享epoll的运作机制,方便更加深入的理解网络编程。

libuv库实现epoll

这是一个C库,之所以先分享libuv,是因为它更像QT的信号-槽机制(适合对网络编程编程不熟,但有希望深入理解epoll功能的QT开发读者阅读),并且做了一定的封装,比原生socket容易理解。

下载源码

gitecode的libuv库https://gitcode.com/gh_mirrors/li/libuv?source_module=search_result_repo

源码编译

复制代码
cd libuv
sh autogen.sh      # 生成必要的脚本文件
./configure       # 配置你的系统环境,可以选择性地添加一些参数,例如--prefix=/usr/local来指定安装目录
make              # 编译库
sudo make install # 安装库到系统目录(安装之后,Linux在/usr/local/lib/可看到libuv.so 和 libuv.a)

gcc编译的时候需要加上-luv引用即可。

功能讲解

以下按照功能的引用顺序提供一个表格,方便直观的了解工作流程。

libuv函数 函数分类 功能描述 用途说明
uv_default_loop() 事件循环 获取默认的事件循环实例 创建并返回libuv的默认事件循环,用于管理所有异步I/O操作
uv_tcp_init() TCP通信 初始化TCP句柄 创建服务器端socket并初始化为非阻塞模式,准备进行网络通信
uv_tcp_bind() TCP通信 绑定TCP服务器到指定地址和端口 将TCP socket与特定的网络接口和端口号关联
uv_listen() TCP通信 开始监听连接请求 设置TCP socket为监听状态,并指定最大连接队列长度
uv_accept() TCP通信 接受客户端连接 从连接队列中取出已建立的连接,创建客户端socket
uv_read_start() 数据读写 开始异步读取数据 注册读取事件到事件循环,当有数据可读时触发回调
uv_write() 数据读写 异步发送数据 将数据写入socket,非阻塞方式,通过回调通知完成状态
uv_close() 资源管理 关闭句柄 安全关闭连接或定时器等资源,并在关闭后调用回调函数
uv_tcp_getpeername() TCP通信 获取对端地址信息 获取已连接客户端的IP地址和端口信息
uv_timer_init() 定时器 初始化定时器句柄 创建定时器,用于在指定时间间隔执行回调函数
uv_timer_start() 定时器 启动定时器 设置定时器的首次延迟、重复间隔和回调函数
uv_run() 事件循环 运行事件循环 启动I/O多路复用,阻塞等待事件发生并处理回调

可以看到,libuv还是保留了原始scoket服务器端的典型流程:

socket->bind->listen->accept->read->write->close

libuv库通过以下几步机制,封装引用了epoll及I/O多路复用机制:

(1)在开头引用uv_default_loop(),在结尾引用uv_run(),提供了封装的epoll事件循环;

(2)侦听客户端时,使用uv_listen()进入监听状态但不会阻塞主线程,事件循环通过uv_run持续运行并处理各种I/O事件,当新的客户端连接请求到达时,libuv会在事件循环中触发预先注册的回调函数on_new_connection;

(3)uv_accept并非传统的阻塞式accept,而是处理已经被操作系统接受并放入完成队列的连接,整个过程都是在事件驱动的异步框架下完成的;

(4)使用uv_read_start()注册读取参照到事件循环中,监听可读事件,通过调用回调函数on_read()方式实现了异步读取数据的方法;

(5)使用uv_close()进行资源清理,使用回调函数on_client_closed释放需要释放的资源(比如业务功能涉及的内存资源)。

以上的uv_listen()、uv_read_start()、uv_close()都由回调函数进行响应,这个机制与QT的信号和槽机制非常像,加上事件循环的机制,就更加像了。

服务端源码

复制代码
//uv_epollserver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>

#define PORT 8080           //服务器监听端口号
#define MAX_CLIENTS 10000   //最大支持的客户端连接数
#define BUFFER_SIZE 1024    //数据缓冲区大小(字节)


typedef struct {
    uv_tcp_t handle;         //libuv TCP句柄,用于管理TCP连接
    struct sockaddr_in addr; //客户端网络地址信息
    char buffer[BUFFER_SIZE]; //数据接收缓冲区
    int client_id;          //客户端唯一标识符
    time_t connect_time;    //客户端连接时间戳
} client_t;

uv_loop_t *loop;                    //libuv事件循环实例
client_t *clients[MAX_CLIENTS];     //客户端指针数组
int client_count = 0;                //当前连接客户端计数

//分配客户端ID
int allocate_client_id() {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i] == NULL) {     //查找空闲槽位
            return i;                //返回可用的客户端ID
        }
    }
    return -1;  //无可用ID,返回错误码
}


//释放客户端资源
void free_client(client_t *client) {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i] == client) {    //定位客户端在数组中的位置
            clients[i] = NULL;        //清空数组对应位置
            client_count--;           //减少客户端计数
            break;
        }
    }
    free(client);  //释放客户端结构体内存
}


//数据发送回调
void on_write_end(uv_write_t *req, int status) {
    if (status) {  //检查发送是否出错
        fprintf(stderr, "Write error: %s\n", uv_strerror(status));
    }
    free(req);  //释放写请求结构体内存
}


//读取客户端数据
void on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
    client_t *client = (client_t*)stream;//转换类型
    //
    char client_ip[INET_ADDRSTRLEN];
    const char *ip_str = "Unknown";
    if (client != NULL) {
        uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
        ip_str = client_ip;
    }

    if (nread > 0) {//成功读取到数据
        //处理接收到的数据
        buf->base[nread] = '\0';//添加字符串终止符
        printf("Received from client: %s\n", buf->base);

        //回显数据给客户端
        uv_write_t *req = (uv_write_t*)malloc(sizeof(uv_write_t));//分配写请求内存
        uv_buf_t wrbuf = uv_buf_init(buf->base, nread);//初始化写缓冲区
        uv_write(req, stream, &wrbuf, 1, on_write_end);//异步发送数据
    } else if (nread < 0) {//读取发生错误
        if (nread != UV_EOF) {//非正常断开连接
            fprintf(stderr, "Read error: %s\n", uv_strerror(nread));
        }else {//客户端正常断开连接
            printf("[%s] Client disconnected\n", ip_str);
        }
        uv_close((uv_handle_t*)stream, NULL);//关闭连接句柄
    }

    free(buf->base);//释放读取缓冲区内存
}

//分配读取缓冲区
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
    buf->base = (char*)malloc(suggested_size);//动态分配内存
    buf->len = suggested_size;//设置缓冲区长度
}

//客户端关闭回调
void on_client_closed(uv_handle_t *handle) {
    client_t *client = (client_t*)handle;
    free_client(client);//调用资源释放函数
}

//新连接处理
void on_new_connection(uv_stream_t *server, int status) {
    if (status < 0) {//检查连接状态是否异常
        fprintf(stderr, "New connection error: %s\n", uv_strerror(status));
        return;
    }

    //分配客户端结构体内存
    client_t *client = (client_t*)malloc(sizeof(client_t));
    int client_id = allocate_client_id();//获取客户端ID

    if (client_id == -1) {//为-1时,表示已经达到最大连接数
        fprintf(stderr, "Too many clients\n");
        free(client);//释放已分配的内存
        return;
    }

    clients[client_id] = client;//将客户端指针存入数组
    client_count++;//增加客户端计数

    //初始化TCP句柄-创建客户端socket
    uv_tcp_init(loop, &client->handle);
    //非阻塞方式建立客户端连接
    if (uv_accept(server, (uv_stream_t*)&client->handle) == 0) {
        //获取客户端地址
        int addr_len = sizeof(client->addr);
        uv_tcp_getpeername(&client->handle, (struct sockaddr*)&client->addr, &addr_len);

        client->connect_time = time(NULL);//记录连接建立时间
        client->client_id = client_id;//记录客户端ID

        char client_ip[INET_ADDRSTRLEN];
        uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
        printf("New client connected: ID=%d, IP=%s, Total clients: %d\n",
               client_id, client_ip, client_count);

        //开始异步读取数据--多路复用注册
        uv_read_start((uv_stream_t*)&client->handle, alloc_buffer, on_read);
    } else {//接受连接失败
        //资源清理,使用回调函数on_client_closed
        uv_close((uv_handle_t*)&client->handle, on_client_closed);
    }
}

//定时器回调 - 定期统计
void on_timer(uv_timer_t *handle) {
    printf("Server status - Connected clients: %d\n", client_count);
    printf("Memory usage: %ld KB\n", (long)(client_count * sizeof(client_t) / 1024));
    printf("========================\n");
}

int main() {
    loop = uv_default_loop();//获取默认事件循环,在系统底层关联上I/O多路复用器

    //创建TCP服务器
    uv_tcp_t server;
    //初始化TCP句柄-创建服务器端socket
    uv_tcp_init(loop, &server);

    //绑定服务器地址和端口
    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", PORT, &addr);
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);//绑定到指定地址

    //listen创建连接请求队列,队列长度为SOMAXCONN(通常128),同时将服务器socket注册到I/O多路复用器(epoll/IOCP等)
    //当客户端发起连接时,已完成三次握手的连接会放入这个队列,然后由I/O多路复用器激活调用on_new_connection
    int r = uv_listen((uv_stream_t*)&server, SOMAXCONN, on_new_connection);
    if (r) {//监听失败处理
        fprintf(stderr, "Listen error: %s\n", uv_strerror(r));
        return 1;
    }

    printf("Server listening on port %d\n", PORT);
    printf("Maximum clients: %d\n", MAX_CLIENTS);

    //初始化客户端数组
    memset(clients, 0, sizeof(clients));

    //启动统计定时器--显示全局信息
    uv_timer_t timer;
    uv_timer_init(loop, &timer);
    uv_timer_start(&timer, on_timer, 0, 5000);

    //启动事件循环,调用epoll_wait等函数阻塞等待
    return uv_run(loop, UV_RUN_DEFAULT);
}

编译方法:gcc uv_epollserver.c -oserver

客户端源码

这是配套测试的,不详细讲解了

复制代码
//uv_epollclient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <uv.h>

#define PORT 8080           //服务器监听端口号
#define MAX_CLIENTS 10000   //最大支持的客户端连接数
#define BUFFER_SIZE 1024    //数据缓冲区大小(字节)


typedef struct {
    uv_tcp_t handle;         //libuv TCP句柄,用于管理TCP连接
    struct sockaddr_in addr; //客户端网络地址信息
    char buffer[BUFFER_SIZE]; //数据接收缓冲区
    int client_id;          //客户端唯一标识符
    time_t connect_time;    //客户端连接时间戳
} client_t;

uv_loop_t *loop;                    //libuv事件循环实例
client_t *clients[MAX_CLIENTS];     //客户端指针数组
int client_count = 0;                //当前连接客户端计数

//分配客户端ID
int allocate_client_id() {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i] == NULL) {     //查找空闲槽位
            return i;                //返回可用的客户端ID
        }
    }
    return -1;  //无可用ID,返回错误码
}


//释放客户端资源
void free_client(client_t *client) {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i] == client) {    //定位客户端在数组中的位置
            clients[i] = NULL;        //清空数组对应位置
            client_count--;           //减少客户端计数
            break;
        }
    }
    free(client);  //释放客户端结构体内存
}


//数据发送回调
void on_write_end(uv_write_t *req, int status) {
    if (status) {  //检查发送是否出错
        fprintf(stderr, "Write error: %s\n", uv_strerror(status));
    }
    free(req);  //释放写请求结构体内存
}


//读取客户端数据
void on_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
    client_t *client = (client_t*)stream;//转换类型
    //
    char client_ip[INET_ADDRSTRLEN];
    const char *ip_str = "Unknown";
    if (client != NULL) {
        uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
        ip_str = client_ip;
    }

    if (nread > 0) {//成功读取到数据
        //处理接收到的数据
        buf->base[nread] = '\0';//添加字符串终止符
        printf("Received from client: %s\n", buf->base);

        //回显数据给客户端
        uv_write_t *req = (uv_write_t*)malloc(sizeof(uv_write_t));//分配写请求内存
        uv_buf_t wrbuf = uv_buf_init(buf->base, nread);//初始化写缓冲区
        uv_write(req, stream, &wrbuf, 1, on_write_end);//异步发送数据
    } else if (nread < 0) {//读取发生错误
        if (nread != UV_EOF) {//非正常断开连接
            fprintf(stderr, "Read error: %s\n", uv_strerror(nread));
        }else {//客户端正常断开连接
            printf("[%s] Client disconnected\n", ip_str);
        }
        uv_close((uv_handle_t*)stream, NULL);//关闭连接句柄
    }

    free(buf->base);//释放读取缓冲区内存
}

//分配读取缓冲区
void alloc_buffer(uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf) {
    buf->base = (char*)malloc(suggested_size);//动态分配内存
    buf->len = suggested_size;//设置缓冲区长度
}

//客户端关闭回调
void on_client_closed(uv_handle_t *handle) {
    client_t *client = (client_t*)handle;
    free_client(client);//调用资源释放函数
}

//新连接处理
void on_new_connection(uv_stream_t *server, int status) {
    if (status < 0) {//检查连接状态是否异常
        fprintf(stderr, "New connection error: %s\n", uv_strerror(status));
        return;
    }

    //分配客户端结构体内存
    client_t *client = (client_t*)malloc(sizeof(client_t));
    int client_id = allocate_client_id();//获取客户端ID

    if (client_id == -1) {//为-1时,表示已经达到最大连接数
        fprintf(stderr, "Too many clients\n");
        free(client);//释放已分配的内存
        return;
    }

    clients[client_id] = client;//将客户端指针存入数组
    client_count++;//增加客户端计数

    //初始化TCP句柄-创建客户端socket
    uv_tcp_init(loop, &client->handle);
    //非阻塞方式建立客户端连接
    if (uv_accept(server, (uv_stream_t*)&client->handle) == 0) {
        //获取客户端地址
        int addr_len = sizeof(client->addr);
        uv_tcp_getpeername(&client->handle, (struct sockaddr*)&client->addr, &addr_len);

        client->connect_time = time(NULL);//记录连接建立时间
        client->client_id = client_id;//记录客户端ID

        char client_ip[INET_ADDRSTRLEN];
        uv_ip4_name(&client->addr, client_ip, sizeof(client_ip));
        printf("New client connected: ID=%d, IP=%s, Total clients: %d\n",
               client_id, client_ip, client_count);

        //开始异步读取数据--多路复用注册
        uv_read_start((uv_stream_t*)&client->handle, alloc_buffer, on_read);
    } else {//接受连接失败
        //资源清理,使用回调函数on_client_closed
        uv_close((uv_handle_t*)&client->handle, on_client_closed);
    }
}

//定时器回调 - 定期统计
void on_timer(uv_timer_t *handle) {
    printf("Server status - Connected clients: %d\n", client_count);
    printf("Memory usage: %ld KB\n", (long)(client_count * sizeof(client_t) / 1024));
    printf("========================\n");
}

int main() {
    loop = uv_default_loop();//获取默认事件循环,在系统底层关联上I/O多路复用器

    //创建TCP服务器
    uv_tcp_t server;
    //初始化TCP句柄-创建服务器端socket
    uv_tcp_init(loop, &server);

    //绑定服务器地址和端口
    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", PORT, &addr);
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);//绑定到指定地址

    //listen创建连接请求队列,队列长度为SOMAXCONN(通常128),同时将服务器socket注册到I/O多路复用器(epoll/IOCP等)
    //当客户端发起连接时,已完成三次握手的连接会放入这个队列,然后由I/O多路复用器激活调用on_new_connection
    int r = uv_listen((uv_stream_t*)&server, SOMAXCONN, on_new_connection);
    if (r) {//监听失败处理
        fprintf(stderr, "Listen error: %s\n", uv_strerror(r));
        return 1;
    }

    printf("Server listening on port %d\n", PORT);
    printf("Maximum clients: %d\n", MAX_CLIENTS);

    //初始化客户端数组
    memset(clients, 0, sizeof(clients));

    //启动统计定时器--显示全局信息
    uv_timer_t timer;
    uv_timer_init(loop, &timer);
    uv_timer_start(&timer, on_timer, 0, 5000);

    //启动事件循环,调用epoll_wait等函数阻塞等待
    return uv_run(loop, UV_RUN_DEFAULT);
}

原生socket实现epoll

不需要额外下载开发包。

功能讲解

以下按照功能的引用顺序提供一个表格,方便直观的了解工作流程。

函数 函数分类 功能描述 用途说明 对应libuv函数
socket() 套接字创建 创建套接字描述符 创建TCP通信端点,指定协议族和类型 uv_tcp_init()
bind() 地址绑定 绑定套接字地址 将服务器socket与特定IP和端口关联 uv_tcp_bind()
listen() 连接监听 监听连接请求 设置socket为监听状态,指定连接队列长度 uv_listen()
fcntl() 模式设置 设置属性 将服务器端ocket设置为非阻塞模式 libuv自动处理非阻塞
epoll_create1() 多路复用 创建epoll实例 创建事件多路分离器,用于监控多个文件描述符 uv_default_loop()内部封装
epoll_ctl() 多路复用 控制epoll监控列表 添加、修改或移除被监控的文件描述符 libuv事件循环内部管理
epoll_wait() 多路复用 等待事件发生 阻塞等待被监控的文件描述符上事件发生 uv_run()内部封装
accept() 连接管理 接受客户端连接 从连接队列中取出已建立的连接,创建客户端socket uv_accept()
fcntl() 模式设置 设置属性 将客户端socket设置为非阻塞模式 libuv自动处理非阻塞
read()/recv() 数据读写 从套接字读取数据 从客户端socket接收数据 uv_read_start()回调机制
write()/send() 数据读写 向套接字写入数据 向客户端socket发送数据 uv_write()
close() 资源管理 关闭文件描述符 释放socket资源,终止连接 uv_close()

从以上流程中可看到,在原始的socket->bind->listen->accept->read->write->close流程中,在listen()侦听之后,加入了fcntl()->epoll_create1()->epoll_ctl()->epoll_wait()流程,此流程是引入非阻塞的服务端接收连接机制,以及将服务端文件描述符添加到epoll实例中进行监控,这样当socket缓存区中有数据时,会触发epoll_wait()通知事件。

另外,在accept连接上客户端之后,需要将客户端socket设置为非阻塞模式,才能达到异步的效果。

服务端源码

以下代码中有详细的注释

复制代码
//src_scoketepoll.c
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>

#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096 //数据缓冲区大小(字节)
#define SERVER_PORT 8080 //服务器监听端口号

#define PRINTF_ERR_MSG(format, ...) fprintf(stderr, "[ERROR]<%s:%d>:" format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#define SCREEN_PRINTF(format,args...)    printf("%s-%s-%d:" format "\n",__FILE__,__FUNCTION__,__LINE__,##args)

// 设置文件描述符为非阻塞模式
static int set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main() {
    int server_fd;

    // 创建服务器socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置socket选项,允许端口重用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 绑定服务器地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;//所有可用的网络接口
    //server_addr.sin_addr.s_addr = inet_addr("192.168.1.123");//固定IP
    //绑定服务端地址
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 开始监听,listen创建连接请求队列,队列长度为SOMAXCONN(通常128)
    //当客户端发起连接时,已完成三次握手的连接会放入这个队列
    if (listen(server_fd, SOMAXCONN) == -1) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 设置服务器socket为非阻塞
    if (set_nonblocking(server_fd) == -1) {
        perror("set_nonblocking server_fd failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    SCREEN_PRINTF("Epoll server started on port %d\n", SERVER_PORT);

    struct epoll_event event, events[MAX_EVENTS];

    // 创建epoll实例
    int epoll_fd= epoll_create1(0);//老方法是int epoll_fd = epoll_create(1)
    if (epoll_fd  == -1) {
        perror("epoll_create1 failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    //EPOLLIN默认是水平方式读事件,只要socket缓存区中有数据,就会一直触发epoll_wait通知(下文while循环中)
    //EPOLLIN | EPOLLET边缘触发读事件,socket缓存区由空->非空时,只触发一次epoll_wait通知(下文while循环中)
    event.events = EPOLLIN | EPOLLET; //边缘触发的可读事件
    event.data.fd = server_fd;
    //epoll_ctl 将服务端文件描述符添加到epoll实例中进行监控
    //EPOLL_CTL_ADD - 添加新的文件描述符到监控列表
    //EPOLL_CTL_MOD - 修改已监控文件描述符的事件设置
    //EPOLL_CTL_DEL - 从监控列表中移除文件描述符
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
        perror("epoll_ctl add server_fd failed");
        close(server_fd);
        close(epoll_fd);
        exit(EXIT_FAILURE);
    }

    // 主事件循环
    while (1) {
        //如果成功,nfds接收返回的事件个数,把就绪的事件放在events数组中
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait failed");
            break;
        }
        //事件处理
        for (int i = 0; i < nfds; i++) {
            // 处理新连接
            if (events[i].data.fd == server_fd) {//处理服务端描述符事件
                SCREEN_PRINTF("接收到epoll_wait推送的服务端链接事件\n");
                while (1) {
                    struct sockaddr_in client_addr;
                    socklen_t client_len = sizeof(client_addr);
                    //从已完成连接的队列里面,获取一个客户端信息,生成一个新的文件描述符,这是与客户端通信的文件描述符
                    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
                    if (client_fd == -1) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break; // 已处理所有待处理连接
                        } else {
                            perror("accept failed");
                            break;
                        }
                    }

                    // 设置客户端socket为非阻塞
                    if (set_nonblocking(client_fd) == -1) {
                        perror("set_nonblocking failed");
                        close(client_fd);
                        continue;
                    }

                    SCREEN_PRINTF("[%s:%d] 客户端连接成功 client_fd=%d \n",\
                                  inet_ntoa(client_addr.sin_addr),
                                  ntohs(client_addr.sin_port), client_fd);

                    // 添加客户端socket到epoll监控
                    //EPOLLIN‌:当客户端发送数据到服务器时触发
                    //‌EPOLLET‌:只在socket缓冲区从空变为非空时通知一次
                    //EPOLLRDHUP‌:当客户端断开连接时触发
                    event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
                    event.data.fd = client_fd;//保存客户端文件描述符
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                        perror("epoll_ctl add client_fd failed");
                        close(client_fd);
                    }
                }
            }
            // 处理客户端数据
            else {
                //SCREEN_PRINTF("接收到epoll_wait推送的客户端交互事件\n");
                int client_fd = events[i].data.fd;
                // 检查连接是否关闭
                //EPOLLRDHUP‌:当客户端断开连接时触发
                //EPOLLHUP:当客户端强制终止连接时
                if (events[i].events & (EPOLLRDHUP | EPOLLHUP)) {
                    printf("Client disconnected (fd: %d)\n", client_fd);
                    close(client_fd);
                    continue;
                }

                // 处理可读事件
                if (events[i].events & EPOLLIN) {
                    char buffer[BUFFER_SIZE];
                    ssize_t bytes_read;
                    int total_bytes = 0;

                    // 读取客户端数据--读取socket缓存区中的数据
                    while (1) {
                        //可以用bytes_read = recv(client_fd, buffer, BUFFER_SIZE - 1, 0),多了一个参数
                        bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);
                        if (bytes_read > 0) {
                            buffer[bytes_read] = '\0';
                            total_bytes += bytes_read;
                            SCREEN_PRINTF("Received from client %d: %s", client_fd, buffer);

                            // 回显数据给客户端--测试发送是否成功
                            if (write(client_fd, buffer, bytes_read) != bytes_read) {
                                perror("write failed");
                                break;
                            }
                        }

                        if (bytes_read == 0) {
                            SCREEN_PRINTF("Client disconnected (fd: %d)", client_fd);
                            //epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);// 从epoll监控中移除client_fd
                            //当调用 close(fd) 时,内核会自动将fd文件描述符从所有epoll实例中移除,以上代码不需要显示EPOLL_CTL_DEL,只是展示有这个动作
                            close(client_fd);
                        }
                        else {
                            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                                // 没有更多数据可读,这是正常情况
                                if (total_bytes > 0) {
                                    printf("Finished reading from client %d, total: %d bytes\n",
                                           client_fd, total_bytes);
                                } else {
                                    perror("read failed");
                                    close(client_fd);
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    close(server_fd);
    close(epoll_fd);
    return 0;
}

使用gcc src_socketepoll.c -oserver编译即可。

客户端可以使用libuv中的客户端源码来测试。

篇尾

以上的epoll服务端可以处理万级以上的高并发需求场景,本篇也是进程间通信(IPC)-socket内容的补充。

相关推荐
落羽的落羽4 小时前
【Linux系统】从零掌握make与Makefile:高效自动化构建项目的工具
linux·服务器·开发语言·c++·人工智能·机器学习·1024程序员节
小小小糖果人4 小时前
Linux云计算基础篇(24)-PXE批量安装和Kickstart工具
linux·运维·php
Sylvia@8885 小时前
19.管理基本存储
linux·运维·1024程序员节
文火冰糖的硅基工坊5 小时前
[嵌入式系统-150]:智能机器人(具身智能)内部的嵌入式系统以及各自的功能、硬件架构、操作系统、软件架构
android·linux·算法·ubuntu·机器人·硬件架构
susu10830189116 小时前
ubuntu 查看文件夹占了多大
linux·运维·ubuntu
小涵6 小时前
第 01 天:Linux 是什么?内核、发行版及其生态系统
linux·运维·devops·1024程序员节·sre
IvanCodes6 小时前
十六、Linux网络配置
linux·运维·网络
j_xxx404_6 小时前
Linux:权限(完结)|权限管理|修改权限chmod chown charp|文件类型|拓展
linux·运维·服务器
报错小能手7 小时前
项目——基于C/S架构的预约系统平台(2)
linux·c语言·笔记·学习·架构