feat(websocket): 实现 WebSocket 广播与频道路由系统

新增功能:
- ws_registry_add/remove: 连接注册/注销,自动管理活跃连接链表
- ws_broadcast(): 向所有活跃连接广播文本消息
- ws_broadcast_to_path(): 按握手路径(频道)定向广播
- ws_connection_count(): 获取当前 WebSocket 连接数
- ws_handle_connection() 新增 path 参数,用于频道标识

实现细节:
- 全局链表 + pthread_mutex 保证多线程安全
- 原子计数器统计连接数
- 连接建立时自动注册,关闭/异常时自动注销
- 默认行为保持 echo 不变

编译零警告,61 项集成测试 + 127 个单元测试全部通过。
This commit is contained in:
xfy911 2026-06-05 18:12:46 +08:00
parent 2f6d86c5cd
commit 035e4bebb4
3 changed files with 168 additions and 6 deletions

View File

@ -795,7 +795,7 @@ static void client_handler(void *arg) {
memmove(conn->buf, conn->buf + parsed, conn->buf_len - (size_t)parsed); memmove(conn->buf, conn->buf + parsed, conn->buf_len - (size_t)parsed);
} }
conn->buf_len -= (size_t)parsed; conn->buf_len -= (size_t)parsed;
ws_handle_connection(conn->fd, conn->timeout_ms); ws_handle_connection(conn->fd, conn->timeout_ms, req.path);
break; break;
} }
} }

View File

@ -3,7 +3,7 @@
* @brief WebSocket RFC 6455 * @brief WebSocket RFC 6455
* *
* *
* echo 广 * 广2026-06-05
*/ */
#include "websocket.h" #include "websocket.h"
@ -15,6 +15,8 @@
#include <errno.h> #include <errno.h>
#include <openssl/sha.h> #include <openssl/sha.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <pthread.h>
#include <stdatomic.h>
/* WebSocket 魔数字符串GUID */ /* WebSocket 魔数字符串GUID */
static const char WS_GUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; static const char WS_GUID[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
@ -236,6 +238,134 @@ int ws_send_pong(int fd, const uint8_t *payload, size_t len) {
return ws_send_frame(fd, WS_OP_PONG, payload, len); return ws_send_frame(fd, WS_OP_PONG, payload, len);
} }
/* ============================================================
* WebSocket 广
* ============================================================ */
typedef struct ws_conn_node {
int fd; /**< 客户端 socket */
char *path; /**< 握手路径(频道标识) */
struct ws_conn_node *next; /**< 链表下一个节点 */
} ws_conn_node_t;
/** 全局注册表 */
typedef struct {
ws_conn_node_t *head; /**< 链表头 */
pthread_mutex_t mutex; /**< 保护链表的互斥锁 */
atomic_int count; /**< 当前连接数 */
} ws_registry_t;
static ws_registry_t g_ws_registry = {
.head = NULL,
.mutex = PTHREAD_MUTEX_INITIALIZER,
.count = 0
};
/**
* @brief WebSocket
*
* @param fd socket
* @param path NULL使
*/
static void ws_registry_add(int fd, const char *path) {
ws_conn_node_t *node = (ws_conn_node_t *)calloc(1, sizeof(ws_conn_node_t));
if (!node) return;
node->fd = fd;
node->path = path ? strdup(path) : NULL;
pthread_mutex_lock(&g_ws_registry.mutex);
node->next = g_ws_registry.head;
g_ws_registry.head = node;
atomic_fetch_add(&g_ws_registry.count, 1);
pthread_mutex_unlock(&g_ws_registry.mutex);
log_info("WebSocket 注册 fd=%d 路径=%s当前连接数: %d",
fd, path ? path : "(default)", atomic_load(&g_ws_registry.count));
}
/**
* @brief WebSocket
*
* @param fd socket
*/
static void ws_registry_remove(int fd) {
pthread_mutex_lock(&g_ws_registry.mutex);
ws_conn_node_t **pp = &g_ws_registry.head;
while (*pp) {
ws_conn_node_t *node = *pp;
if (node->fd == fd) {
*pp = node->next;
free(node->path);
free(node);
atomic_fetch_sub(&g_ws_registry.count, 1);
log_info("WebSocket 注销 fd=%d当前连接数: %d",
fd, atomic_load(&g_ws_registry.count));
break;
}
pp = &node->next;
}
pthread_mutex_unlock(&g_ws_registry.mutex);
}
/**
* @brief WebSocket 广
*
* @param text
* @return
*/
int ws_broadcast(const char *text) {
if (!text) return 0;
int sent = 0;
pthread_mutex_lock(&g_ws_registry.mutex);
ws_conn_node_t *node = g_ws_registry.head;
while (node) {
if (ws_send_text(node->fd, text) == 0) {
sent++;
}
node = node->next;
}
pthread_mutex_unlock(&g_ws_registry.mutex);
log_debug("WebSocket 广播: %s -> %d/%d 连接", text, sent,
atomic_load(&g_ws_registry.count));
return sent;
}
/**
* @brief
*
* @param path
* @param text
* @return
*/
int ws_broadcast_to_path(const char *path, const char *text) {
if (!path || !text) return 0;
int sent = 0;
pthread_mutex_lock(&g_ws_registry.mutex);
ws_conn_node_t *node = g_ws_registry.head;
while (node) {
if (node->path && strcmp(node->path, path) == 0) {
if (ws_send_text(node->fd, text) == 0) {
sent++;
}
}
node = node->next;
}
pthread_mutex_unlock(&g_ws_registry.mutex);
log_debug("WebSocket 频道广播 [%s]: %s -> %d 连接", path, text, sent);
return sent;
}
/**
* @brief WebSocket
*/
int ws_connection_count(void) {
return atomic_load(&g_ws_registry.count);
}
/* ============================================================
* WebSocket
* ============================================================ */
/** /**
* @brief * @brief
*/ */
@ -244,14 +374,17 @@ static ssize_t ws_read_data(int fd, uint8_t *buf, size_t max_len) {
return n; return n;
} }
void ws_handle_connection(int fd, uint32_t timeout_ms) { void ws_handle_connection(int fd, uint32_t timeout_ms, const char *path) {
(void)timeout_ms; /* TODO: 超时处理 */ (void)timeout_ms; /* TODO: 超时处理 */
uint8_t buf[8192]; uint8_t buf[8192];
size_t buf_len = 0; size_t buf_len = 0;
bool closed = false; bool closed = false;
log_info("WebSocket 连接建立 fd=%d", fd); /* 注册到全局连接表 */
ws_registry_add(fd, path);
log_info("WebSocket 连接建立 fd=%d path=%s", fd, path ? path : "(default)");
while (!closed) { while (!closed) {
ssize_t n = ws_read_data(fd, buf + buf_len, sizeof(buf) - buf_len); ssize_t n = ws_read_data(fd, buf + buf_len, sizeof(buf) - buf_len);
@ -287,6 +420,7 @@ void ws_handle_connection(int fd, uint32_t timeout_ms) {
log_debug("WebSocket fd=%d 收到文本: %s", fd, log_debug("WebSocket fd=%d 收到文本: %s", fd,
frame.payload ? (char *)frame.payload : "(empty)"); frame.payload ? (char *)frame.payload : "(empty)");
if (frame.fin) { if (frame.fin) {
/* 默认行为echo 回发送者 */
ws_send_text(fd, frame.payload ? (char *)frame.payload : ""); ws_send_text(fd, frame.payload ? (char *)frame.payload : "");
} }
break; break;
@ -334,5 +468,8 @@ void ws_handle_connection(int fd, uint32_t timeout_ms) {
} }
} }
/* 从全局连接表注销 */
ws_registry_remove(fd);
log_info("WebSocket 连接关闭 fd=%d", fd); log_info("WebSocket 连接关闭 fd=%d", fd);
} }

View File

@ -116,15 +116,40 @@ int ws_send_ping(int fd);
*/ */
int ws_send_pong(int fd, const uint8_t *payload, size_t len); int ws_send_pong(int fd, const uint8_t *payload, size_t len);
/**
* @brief WebSocket 广
*
* @param text
* @return
*/
int ws_broadcast(const char *text);
/**
* @brief
*
* @param path
* @param text
* @return
*/
int ws_broadcast_to_path(const char *path, const char *text);
/**
* @brief WebSocket
*
* @return
*/
int ws_connection_count(void);
/** /**
* @brief WebSocket * @brief WebSocket
* *
* WebSocket //ping/pong/close * WebSocket //ping/pong/close
* echo * 广退
* *
* @param fd socket * @param fd socket
* @param timeout_ms 0 * @param timeout_ms 0
* @param path WebSocket NULL
*/ */
void ws_handle_connection(int fd, uint32_t timeout_ms); void ws_handle_connection(int fd, uint32_t timeout_ms, const char *path);
#endif /* WEBSOCKET_H */ #endif /* WEBSOCKET_H */