前言

        epoll模式涉及到系統底層的I/O多路複用機制,可以處理高併發的場景。本文使用開源的libuv庫以及原生的scoket來分享epoll的運作機制,方便更加深入的理解網絡編程。

libuv庫實現epoll

        這是一個C庫,之所以先分享libuv,是因為它風格與QT的信號-槽機制相似(適合對網絡編程不熟,但y又希望深入理解epoll功能的QT開發讀者閲讀),並且做了一定的封裝,比原生socket容易理解。

源碼編譯

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內容的補充。