前言
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函數
|
函數分類
|
功能描述
|
用途説明
|
|
|
事件循環 |
獲取默認的事件循環實例 |
創建並返回libuv的默認事件循環,用於管理所有異步I/O操作 |
|
|
TCP通信 |
初始化TCP句柄 |
創建服務器端socket並初始化為非阻塞模式,準備進行網絡通信 |
|
|
TCP通信 |
綁定TCP服務器到指定地址和端口 |
將TCP socket與特定的網絡接口和端口號關聯 |
|
|
TCP通信 |
開始監聽連接請求 |
設置TCP socket為監聽狀態,並指定最大連接隊列長度 |
|
|
TCP通信 |
接受客户端連接 |
從連接隊列中取出已建立的連接,創建客户端socket |
|
|
數據讀寫 |
開始異步讀取數據 |
註冊讀取事件到事件循環,當有數據可讀時觸發回調 |
|
|
數據讀寫 |
異步發送數據 |
將數據寫入socket,非阻塞方式,通過回調通知完成狀態 |
|
|
資源管理 |
關閉句柄 |
安全關閉連接或定時器等資源,並在關閉後調用回調函數 |
|
|
TCP通信 |
獲取對端地址信息 |
獲取已連接客户端的IP地址和端口信息 |
|
|
定時器 |
初始化定時器句柄 |
創建定時器,用於在指定時間間隔執行回調函數 |
|
|
定時器 |
啓動定時器 |
設置定時器的首次延遲、重複間隔和回調函數 |
|
|
事件循環 |
運行事件循環 |
啓動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函數
|
|
|
套接字創建 |
創建套接字描述符 |
創建TCP通信端點,指定協議族和類型 |
|
|
|
地址綁定 |
綁定套接字地址 |
將服務器socket與特定IP和端口關聯 |
|
|
|
連接監聽 |
監聽連接請求 |
設置socket為監聽狀態,指定連接隊列長度 |
|
|
|
模式設置 |
設置屬性 |
將服務器端ocket設置為非阻塞模式 |
libuv自動處理非阻塞 |
|
|
多路複用 |
創建epoll實例 |
創建事件多路分離器,用於監控多個文件描述符 |
|
|
|
多路複用 |
控制epoll監控列表 |
添加、修改或移除被監控的文件描述符 |
libuv事件循環內部管理 |
|
|
多路複用 |
等待事件發生 |
阻塞等待被監控的文件描述符上事件發生 |
|
|
|
連接管理 |
接受客户端連接 |
從連接隊列中取出已建立的連接,創建客户端socket |
|
|
|
模式設置 |
設置屬性 |
將客户端socket設置為非阻塞模式 |
libuv自動處理非阻塞 |
|
|
數據讀寫 |
從套接字讀取數據 |
從客户端socket接收數據 |
|
|
|
數據讀寫 |
向套接字寫入數據 |
向客户端socket發送數據 |
|
|
|
資源管理 |
關閉文件描述符 |
釋放socket資源,終止連接 |
|
從以上流程中可看到,在原始的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內容的補充。