【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内容的补充。

相关推荐
QQ__176461982416 小时前
Ubuntu系统创建新用户与删除用户
linux·运维·服务器
渣渣盟16 小时前
Linux邮件服务器快速搭建指南
linux·服务器·开发语言
6极地诈唬16 小时前
【PG漫步】DELETE不会改变本地文件的大小,VACUUM也不会
linux·服务器·数据库
ArrebolJiuZhou16 小时前
00 arm开发环境的搭建
linux·arm开发·单片机·嵌入式硬件
谷雨不太卷16 小时前
Linux_文件权限
linux·运维·服务器
无泪无花月隐星沉17 小时前
uos server 1070e lvm格式磁盘扩容分区
linux·运维·uos
食咗未18 小时前
Linux USB HOST EXTERNAL STORAGE
linux·驱动开发
食咗未18 小时前
Linux USB HOST HID
linux·驱动开发·人机交互
Xの哲學18 小时前
Linux SLAB分配器深度解剖
linux·服务器·网络·算法·边缘计算
齐鲁大虾19 小时前
UOS(统信操作系统)如何更新CUPS(通用Unix打印系统)
linux·服务器·chrome·unix