feat(websocket): 实现 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 空闲超时处理
  - 每次循环前调用 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 集成测试通过,编译零警告
This commit is contained in:
xfy911 2026-06-06 21:16:54 +08:00
parent f868128b75
commit 4105aec916
7 changed files with 149 additions and 14 deletions

View File

@ -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 时自动编译 + 单元测试 + 集成测试

View File

@ -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

View File

@ -27,6 +27,7 @@
#include <sys/sendfile.h> /* sendfile */
#include <signal.h> /* sigaction */
#include <sys/socket.h> /* send */
#include <poll.h> /* 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
*/

View File

@ -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_setreadfds
*
* @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

View File

@ -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 "=== 结果汇总 ==="

View File

@ -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)

View File

@ -8,6 +8,7 @@
#include "websocket.h"
#include "log.h"
#include "platform.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
@ -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)) {