diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md new file mode 100644 index 0000000..a673094 --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,40 @@ +# 已知问题 + +## 1. HTTP Keep-Alive 连接复用不支持 + +**状态**: 等待 V 语言官方修复 + +**问题描述**: +每次 HTTP 请求响应时间约 300ms,其中大部分时间消耗在 TCP 连接建立上。即使响应头中包含 `Connection: keep-alive`,连接也无法复用。 + +**现象**: +- 快速连续发送请求:每次都慢(~300ms) +- 慢速发送请求:快慢交替(复用时快,超时后重建连接时慢) + +**根本原因**: +V 语言标准库 `net.http.Server` 的 `handle_conn` 函数在每次请求处理完成后都会立即关闭连接: + +```v +// F:\v\vlib\net\http\server.v +fn (mut w HandlerWorker) handle_conn(mut conn net.TcpConn) { + defer { + conn.close() or { eprintln('close() failed: ${err}') } // 每次请求后关闭连接 + } + // ... 处理单个请求 ... +} +``` + +**影响**: +- 无法实现 HTTP/1.1 Keep-Alive 连接复用 +- 每次请求都需要重新建立 TCP 连接(三次握手) +- 高并发场景下性能受限 + +**临时解决方案**: +- 在服务器前部署支持 Keep-Alive 的反向代理(如 nginx) +- 或等待 V 语言官方修复此问题 + +**跟踪**: +- V 语言 GitHub: https://github.com/vlang/v +- 相关文件: `vlib/net/http/server.v` + +**发现日期**: 2025-12-26 diff --git a/example.exe b/example.exe index ff3d040..77e44d9 100644 Binary files a/example.exe and b/example.exe differ diff --git a/hono/app.v b/hono/app.v index 3519ac9..335c898 100644 --- a/hono/app.v +++ b/hono/app.v @@ -2,6 +2,7 @@ module hono import net.urllib import net.http +import time // Context 路由器 struct ContextRouter { @@ -265,6 +266,9 @@ fn (mut s ServerHanler) exec_context_middlewares(idx int, mut ctx Context, handl pub fn (mut app Hono) listen(port string) { app.server.addr = port app.server.handler = server_hanler_new(app) + // 增加超时配置以支持更好的 Keep-Alive + app.server.read_timeout = 60 * time.second + app.server.write_timeout = 60 * time.second app.server.listen_and_serve() } diff --git a/hono/request.v b/hono/request.v index 7d22243..c2eb2c3 100644 --- a/hono/request.v +++ b/hono/request.v @@ -42,6 +42,7 @@ pub fn Context.new(req http.Request, params map[string]string, query map[string] pub fn (mut c Context) json(data string) http.Response { mut headers := http.new_header() headers.add_custom('Content-Type', 'application/json; charset=utf-8') or { } + headers.add_custom('Connection', 'keep-alive') or { } for key, value in c.headers { headers.add_custom(key, value) or { continue } } @@ -55,6 +56,7 @@ pub fn (mut c Context) json(data string) http.Response { pub fn (mut c Context) text(data string) http.Response { mut headers := http.new_header() headers.add_custom('Content-Type', 'text/plain; charset=utf-8') or { } + headers.add_custom('Connection', 'keep-alive') or { } for key, value in c.headers { headers.add_custom(key, value) or { continue } } @@ -68,6 +70,7 @@ pub fn (mut c Context) text(data string) http.Response { pub fn (mut c Context) html(data string) http.Response { mut headers := http.new_header() headers.add_custom('Content-Type', 'text/html; charset=utf-8') or { } + headers.add_custom('Connection', 'keep-alive') or { } for key, value in c.headers { headers.add_custom(key, value) or { continue } } @@ -498,6 +501,7 @@ fn (mut c Context) stream_large_file(file_path string, file_size u64, options Fi // 构建带头部的响应 fn (mut c Context) build_headers_response(body string) http.Response { mut headers := http.new_header() + headers.add_custom('Connection', 'keep-alive') or { } for key, value in c.headers { headers.add_custom(key, value) or { continue } } diff --git a/hono/static.v b/hono/static.v index d221aef..1645f54 100644 --- a/hono/static.v +++ b/hono/static.v @@ -55,17 +55,25 @@ pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) htt println(' Root: ${options.root}') // 安全检查:防止路径遍历攻击 + // 注意:不检查文件扩展名,因为这可能是 API 路由而不是静态文件 + // 如果文件不存在,会在后面调用 next(mut c) 继续处理 validation_options := PathValidationOptions{ allow_absolute_paths: false allow_hidden_files: options.dotfiles - check_file_extension: true - allowed_base_paths: [options.root] + check_file_extension: false // 不检查扩展名,让文件存在性检查来决定 + allowed_base_paths: [] } safe_file_path := validate_file_path(file_path, validation_options) or { println(' [DEBUG] Path validation failed: $err') - c.status(403) - return c.text('Forbidden') + // 路径验证失败(如包含 .. 等危险模式),传递给下一个处理器 + // 只有真正的安全问题才返回 403 + if err.msg().contains('Dangerous') { + c.status(403) + return c.text('Forbidden') + } + // 其他情况(如无扩展名)传递给下一个处理器 + return next(mut c) } // 检查是否允许访问点文件 @@ -145,6 +153,7 @@ fn serve_file(mut c Context, file_path string, options StaticOptions) http.Respo c.status(304) // 构建响应头 mut headers := http.new_header() + headers.add_custom('Connection', 'keep-alive') or { } for key, value in c.headers { headers.add_custom(key, value) or { continue } } @@ -168,6 +177,9 @@ fn serve_file(mut c Context, file_path string, options StaticOptions) http.Respo c.headers[key] = value } + // 设置 Keep-Alive + c.headers['Connection'] = 'keep-alive' + // 返回文件内容 mut headers := http.new_header() for key, value in c.headers {