From 4105aec91691c8b57c1e8eac24bd5941c6048819 Mon Sep 17 00:00:00 2001 From: xfy911 Date: Sat, 6 Jun 2026 21:16:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(websocket):=20=E5=AE=9E=E7=8E=B0=20WebSock?= =?UTF-8?q?et=20=E7=A9=BA=E9=97=B2=E8=B6=85=E6=97=B6=E5=A4=84=E7=90=86=20+?= =?UTF-8?q?=20=E8=B7=A8=E5=B9=B3=E5=8F=B0=20poll/select=20=E6=8A=BD?= =?UTF-8?q?=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - platform.h / platform.c: 新增 cocoon_socket_poll_readable() 跨平台 socket 可读等待 - POSIX: poll() + POLLIN - Windows: select() + fd_set(readfds) - websocket.c: ws_handle_connection() 实现 timeout_ms 空闲超时处理 - 每次循环前调用 poll_readable() 等待数据或超时 - 超时时发送 1001 Idle timeout 关闭帧,清理连接资源 - 解决遗留 TODO: /* TODO: 超时处理 */ - tests/websocket_test.py: 新增 test_timeout() 测试 - 连接后不发帧,验证服务端 2 秒后发送 1001 关闭帧 - tests/integration_test.sh: 新增 WebSocket 空闲超时集成测试 - 启动 -o 2000 短超时服务器验证超时行为 - Makefile: 补全 test_websocket 编译规则,加入 platform.c - 修复 undefined reference 错误 - 全部 142 单元测试 + 77 集成测试通过,编译零警告 --- .cocoon-plan.md | 14 +++++++++ Makefile | 4 +-- platform.c | 26 +++++++++++++++ platform.h | 12 ++++++- tests/integration_test.sh | 23 +++++++++++++- tests/websocket_test.py | 66 ++++++++++++++++++++++++++++++++++----- websocket.c | 18 +++++++++-- 7 files changed, 149 insertions(+), 14 deletions(-) diff --git a/.cocoon-plan.md b/.cocoon-plan.md index e1b8b36..2edf7ac 100644 --- a/.cocoon-plan.md +++ b/.cocoon-plan.md @@ -82,6 +82,20 @@ ## 最近行动记录 +- 2026-06-06: **本轮行动 — WebSocket 空闲超时 + 跨平台 poll/select 抽象** + - `platform.h` / `platform.c`: 新增 `cocoon_socket_poll_readable()` 跨平台 socket 可读等待 + - POSIX: `poll()` + `POLLIN` + - Windows: `select()` + `fd_set`(readfds) + - `websocket.c`: `ws_handle_connection()` 实现 `timeout_ms` 空闲超时处理 + - 每次循环前调用 `cocoon_socket_poll_readable()` 等待数据或超时 + - 超时时发送 `1001 Idle timeout` 关闭帧,清理连接资源 + - 解决 TODO: `/* TODO: 超时处理 */` + - `tests/websocket_test.py`: 新增 `test_timeout()` 测试(Python 标准库,零依赖) + - 连接后不发帧,验证服务端 2 秒后发送 1001 关闭帧 + - `tests/integration_test.sh`: 新增 WebSocket 空闲超时集成测试(启动 `-o 2000` 短超时服务器) + - `Makefile`: 补全 `test_websocket` 单元测试编译规则,加入 `platform.c`(修复 undefined reference) + - 全部 142 个单元测试 + 77 项集成测试通过,编译零警告 + - 推送到 main(待提交) - 2026-06-06: **本轮行动 — 基础设施完善(Makefile + CI + README)** - `Makefile`: 添加 `integration-test` 别名和 `test-all` 目标(一键运行全部测试) - `.github/workflows/ci.yml`: GitHub Actions 工作流,push/PR 时自动编译 + 单元测试 + 集成测试 diff --git a/Makefile b/Makefile index 6942674..b144242 100644 --- a/Makefile +++ b/Makefile @@ -102,8 +102,8 @@ $(UNIT_TEST_DIR)/test_http: $(UNIT_TEST_DIR)/test_http.c http.c log.c $(UNITY_SR $(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c access_log.c platform.c $(UNITY_SRC) $(LDFLAGS) -$(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c $(UNITY_SRC) - $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c $(UNITY_SRC) $(LDFLAGS) +$(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c platform.c $(UNITY_SRC) + $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c platform.c $(UNITY_SRC) $(LDFLAGS) $(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) $(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm diff --git a/platform.c b/platform.c index d686e9c..f7f3048 100644 --- a/platform.c +++ b/platform.c @@ -27,6 +27,7 @@ #include /* sendfile */ #include /* sigaction */ #include /* send */ +#include /* poll */ /** * POSIX socket 子系统无需显式初始化。 @@ -73,6 +74,17 @@ ssize_t cocoon_socket_recv(cocoon_socket_t fd, void *buf, size_t len) { return read(fd, buf, len); } +/** + * POSIX 下使用 poll() 等待 socket 可读或超时。 + */ +int cocoon_socket_poll_readable(cocoon_socket_t fd, int timeout_ms) { + struct pollfd pfd = { .fd = fd, .events = POLLIN }; + int ret = poll(&pfd, 1, timeout_ms); + if (ret > 0) return 1; + if (ret == 0) return 0; + return -1; +} + /** * POSIX 下文件操作直接映射标准 API。 */ @@ -330,6 +342,20 @@ ssize_t cocoon_socket_recv(cocoon_socket_t fd, void *buf, size_t len) { return (ssize_t)r; } +/** + * Windows 下使用 select() 等待 socket 可读或超时。 + */ +int cocoon_socket_poll_readable(cocoon_socket_t fd, int timeout_ms) { + fd_set fds; + FD_ZERO(&fds); + FD_SET(fd, &fds); + struct timeval tv = { timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; + int ret = select(0, &fds, NULL, NULL, timeout_ms < 0 ? NULL : &tv); + if (ret > 0) return 1; + if (ret == 0) return 0; + return -1; +} + /** * Windows 下文件操作用 _open/_read/_lseek/_close(二进制模式)。 */ diff --git a/platform.h b/platform.h index ab57f51..6cc44e2 100644 --- a/platform.h +++ b/platform.h @@ -345,7 +345,17 @@ void cocoon_dir_close(cocoon_dir_iter_t *iter); */ void cocoon_signal_setup(void (*handler)(int)); -/* ========== 系统信息 API ========== */ +/** + * cocoon_socket_poll_readable - 等待 socket 可读或超时 + * + * POSIX: poll() + POLLIN + * Windows: select() + fd_set(readfds) + * + * @param fd socket 描述符 + * @param timeout_ms 超时毫秒(-1 表示无限等待) + * @return 1 可读,0 超时,-1 错误 + */ +int cocoon_socket_poll_readable(cocoon_socket_t fd, int timeout_ms); /** * cocoon_cpu_count - 获取 CPU 逻辑核心数 diff --git a/tests/integration_test.sh b/tests/integration_test.sh index 799f38b..26e2d90 100755 --- a/tests/integration_test.sh +++ b/tests/integration_test.sh @@ -920,13 +920,34 @@ echo "=== WebSocket 测试 ===" if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then echo " ✓ WebSocket 握手 + echo — 通过" pass - pass else echo " ✗ WebSocket 测试失败" cat "$TMPDIR/ws_test.log" fail +fi + +# WebSocket 空闲超时测试(启动短超时服务器) +kill_server +sleep 1 +$SERVER -r "$ROOT" -p 9999 -o 2000 > "$TMPDIR/server_ws_timeout.log" 2>&1 & +for i in {1..30}; do + if nc -z localhost 9999 2>/dev/null; then break; fi + sleep 0.1 +done + +if python3 "$ROOT/../websocket_test.py" --timeout-test > "$TMPDIR/ws_timeout.log" 2>&1; then + echo " ✓ WebSocket 空闲超时 (2s) — 通过" + pass +else + echo " ✗ WebSocket 空闲超时测试失败" + cat "$TMPDIR/ws_timeout.log" fail fi +kill_server + +# 恢复默认服务器用于后续测试 +sleep 1 +start_server echo "" echo "=== 结果汇总 ===" diff --git a/tests/websocket_test.py b/tests/websocket_test.py index 623c702..2ca09a4 100644 --- a/tests/websocket_test.py +++ b/tests/websocket_test.py @@ -227,19 +227,69 @@ def test_echo(): return True +def test_timeout(port=PORT, timeout=2): + """测试 WebSocket 空闲超时 — 连接后不发帧,等待服务端关闭""" + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout + 3) + sock.connect((HOST, port)) + + req, key = build_handshake() + sock.send(req.encode()) + + data = b"" + while b"\r\n\r\n" not in data: + chunk = sock.recv(1024) + if not chunk: + break + data += chunk + + status, headers = parse_response(data) + if "101" not in status: + print(f"FAIL: 握手失败: {status}") + sock.close() + return False + + # 等待服务端超时关闭(不发任何帧) + try: + frame, _ = recv_frame(sock) + if frame is not None and frame["opcode"] == 0x08: + print("PASS: WebSocket 空闲超时后收到服务端关闭帧") + sock.close() + return True + else: + print(f"FAIL: 超时后未收到关闭帧,opcode={frame['opcode'] if frame else 'None'}") + sock.close() + return False + except socket.timeout: + print("FAIL: 等待超时关闭时本地 socket 超时") + sock.close() + return False + + if __name__ == "__main__": passed = 0 failed = 0 - if test_handshake(): - passed += 1 - else: - failed += 1 + # 支持命令行参数:--timeout-test 只运行超时测试(用于短超时服务器) + timeout_test_only = len(sys.argv) > 1 and sys.argv[1] == "--timeout-test" - if test_echo(): - passed += 1 - else: - failed += 1 + if not timeout_test_only: + if test_handshake(): + passed += 1 + else: + failed += 1 + + if test_echo(): + passed += 1 + else: + failed += 1 + + # 超时测试只在 --timeout-test 模式下运行(需要服务器配置短超时) + if timeout_test_only: + if test_timeout(): + passed += 1 + else: + failed += 1 print(f"\n通过: {passed}, 失败: {failed}") sys.exit(0 if failed == 0 else 1) diff --git a/websocket.c b/websocket.c index 4ba7abe..00ded7e 100644 --- a/websocket.c +++ b/websocket.c @@ -8,6 +8,7 @@ #include "websocket.h" #include "log.h" +#include "platform.h" #include #include #include @@ -375,8 +376,6 @@ static ssize_t ws_read_data(int fd, uint8_t *buf, size_t max_len) { } void ws_handle_connection(int fd, uint32_t timeout_ms, const char *path) { - (void)timeout_ms; /* TODO: 超时处理 */ - uint8_t buf[8192]; size_t buf_len = 0; bool closed = false; @@ -387,6 +386,21 @@ void ws_handle_connection(int fd, uint32_t timeout_ms, const char *path) { log_info("WebSocket 连接建立 fd=%d path=%s", fd, path ? path : "(default)"); while (!closed) { + /* 等待数据可读或超时 */ + if (timeout_ms > 0) { + int ret = cocoon_socket_poll_readable(fd, (int)timeout_ms); + if (ret == 0) { + log_info("WebSocket fd=%d 空闲超时 (%u ms),关闭连接", fd, timeout_ms); + ws_send_close(fd, 1001, "Idle timeout"); + closed = true; + break; + } + if (ret < 0) { + log_debug("WebSocket fd=%d poll 错误: %s", fd, cocoon_strerror(cocoon_get_last_error())); + break; + } + } + ssize_t n = ws_read_data(fd, buf + buf_len, sizeof(buf) - buf_len); if (n <= 0) { if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {