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:
parent
f868128b75
commit
4105aec916
@ -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 时自动编译 + 单元测试 + 集成测试
|
||||
|
||||
4
Makefile
4
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
|
||||
|
||||
26
platform.c
26
platform.c
@ -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(二进制模式)。
|
||||
*/
|
||||
|
||||
12
platform.h
12
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 逻辑核心数
|
||||
|
||||
@ -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 "=== 结果汇总 ==="
|
||||
|
||||
@ -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)
|
||||
|
||||
18
websocket.c
18
websocket.c
@ -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)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user