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)**
|
- 2026-06-06: **本轮行动 — 基础设施完善(Makefile + CI + README)**
|
||||||
- `Makefile`: 添加 `integration-test` 别名和 `test-all` 目标(一键运行全部测试)
|
- `Makefile`: 添加 `integration-test` 别名和 `test-all` 目标(一键运行全部测试)
|
||||||
- `.github/workflows/ci.yml`: GitHub Actions 工作流,push/PR 时自动编译 + 单元测试 + 集成测试
|
- `.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)
|
$(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)
|
$(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)
|
$(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 $(UNITY_SRC) $(LDFLAGS)
|
$(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)
|
$(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
|
$(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 <sys/sendfile.h> /* sendfile */
|
||||||
#include <signal.h> /* sigaction */
|
#include <signal.h> /* sigaction */
|
||||||
#include <sys/socket.h> /* send */
|
#include <sys/socket.h> /* send */
|
||||||
|
#include <poll.h> /* poll */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POSIX socket 子系统无需显式初始化。
|
* 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);
|
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。
|
* POSIX 下文件操作直接映射标准 API。
|
||||||
*/
|
*/
|
||||||
@ -330,6 +342,20 @@ ssize_t cocoon_socket_recv(cocoon_socket_t fd, void *buf, size_t len) {
|
|||||||
return (ssize_t)r;
|
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(二进制模式)。
|
* 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));
|
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 逻辑核心数
|
* cocoon_cpu_count - 获取 CPU 逻辑核心数
|
||||||
|
|||||||
@ -920,13 +920,34 @@ echo "=== WebSocket 测试 ==="
|
|||||||
if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then
|
if python3 "$ROOT/../websocket_test.py" > "$TMPDIR/ws_test.log" 2>&1; then
|
||||||
echo " ✓ WebSocket 握手 + echo — 通过"
|
echo " ✓ WebSocket 握手 + echo — 通过"
|
||||||
pass
|
pass
|
||||||
pass
|
|
||||||
else
|
else
|
||||||
echo " ✗ WebSocket 测试失败"
|
echo " ✗ WebSocket 测试失败"
|
||||||
cat "$TMPDIR/ws_test.log"
|
cat "$TMPDIR/ws_test.log"
|
||||||
fail
|
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
|
fail
|
||||||
fi
|
fi
|
||||||
|
kill_server
|
||||||
|
|
||||||
|
# 恢复默认服务器用于后续测试
|
||||||
|
sleep 1
|
||||||
|
start_server
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== 结果汇总 ==="
|
echo "=== 结果汇总 ==="
|
||||||
|
|||||||
@ -227,19 +227,69 @@ def test_echo():
|
|||||||
return True
|
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__":
|
if __name__ == "__main__":
|
||||||
passed = 0
|
passed = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
|
|
||||||
if test_handshake():
|
# 支持命令行参数:--timeout-test 只运行超时测试(用于短超时服务器)
|
||||||
passed += 1
|
timeout_test_only = len(sys.argv) > 1 and sys.argv[1] == "--timeout-test"
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
|
|
||||||
if test_echo():
|
if not timeout_test_only:
|
||||||
passed += 1
|
if test_handshake():
|
||||||
else:
|
passed += 1
|
||||||
failed += 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}")
|
print(f"\n通过: {passed}, 失败: {failed}")
|
||||||
sys.exit(0 if failed == 0 else 1)
|
sys.exit(0 if failed == 0 else 1)
|
||||||
|
|||||||
18
websocket.c
18
websocket.c
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "websocket.h"
|
#include "websocket.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
#include "platform.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.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 ws_handle_connection(int fd, uint32_t timeout_ms, const char *path) {
|
||||||
(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;
|
||||||
@ -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)");
|
log_info("WebSocket 连接建立 fd=%d path=%s", fd, path ? path : "(default)");
|
||||||
|
|
||||||
while (!closed) {
|
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);
|
ssize_t n = ws_read_data(fd, buf + buf_len, sizeof(buf) - buf_len);
|
||||||
if (n <= 0) {
|
if (n <= 0) {
|
||||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
|
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user