chore: 记录HTTP Keep-Alive 连接复用不支持的问题

This commit is contained in:
meiseayoung
2025-12-26 15:52:24 +08:00
parent 3355e274e2
commit 7c1aa656ab
5 changed files with 64 additions and 4 deletions
+40
View File
@@ -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
BIN
View File
Binary file not shown.
+4
View File
@@ -2,6 +2,7 @@ module hono
import net.urllib import net.urllib
import net.http import net.http
import time
// Context 路由器 // Context 路由器
struct ContextRouter { 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) { pub fn (mut app Hono) listen(port string) {
app.server.addr = port app.server.addr = port
app.server.handler = server_hanler_new(app) 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() app.server.listen_and_serve()
} }
+4
View File
@@ -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 { pub fn (mut c Context) json(data string) http.Response {
mut headers := http.new_header() mut headers := http.new_header()
headers.add_custom('Content-Type', 'application/json; charset=utf-8') or { } headers.add_custom('Content-Type', 'application/json; charset=utf-8') or { }
headers.add_custom('Connection', 'keep-alive') or { }
for key, value in c.headers { for key, value in c.headers {
headers.add_custom(key, value) or { continue } 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 { pub fn (mut c Context) text(data string) http.Response {
mut headers := http.new_header() mut headers := http.new_header()
headers.add_custom('Content-Type', 'text/plain; charset=utf-8') or { } headers.add_custom('Content-Type', 'text/plain; charset=utf-8') or { }
headers.add_custom('Connection', 'keep-alive') or { }
for key, value in c.headers { for key, value in c.headers {
headers.add_custom(key, value) or { continue } 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 { pub fn (mut c Context) html(data string) http.Response {
mut headers := http.new_header() mut headers := http.new_header()
headers.add_custom('Content-Type', 'text/html; charset=utf-8') or { } headers.add_custom('Content-Type', 'text/html; charset=utf-8') or { }
headers.add_custom('Connection', 'keep-alive') or { }
for key, value in c.headers { for key, value in c.headers {
headers.add_custom(key, value) or { continue } 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 { fn (mut c Context) build_headers_response(body string) http.Response {
mut headers := http.new_header() mut headers := http.new_header()
headers.add_custom('Connection', 'keep-alive') or { }
for key, value in c.headers { for key, value in c.headers {
headers.add_custom(key, value) or { continue } headers.add_custom(key, value) or { continue }
} }
+16 -4
View File
@@ -55,17 +55,25 @@ pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) htt
println(' Root: ${options.root}') println(' Root: ${options.root}')
// 安全检查:防止路径遍历攻击 // 安全检查:防止路径遍历攻击
// 注意:不检查文件扩展名,因为这可能是 API 路由而不是静态文件
// 如果文件不存在,会在后面调用 next(mut c) 继续处理
validation_options := PathValidationOptions{ validation_options := PathValidationOptions{
allow_absolute_paths: false allow_absolute_paths: false
allow_hidden_files: options.dotfiles allow_hidden_files: options.dotfiles
check_file_extension: true check_file_extension: false // 不检查扩展名,让文件存在性检查来决定
allowed_base_paths: [options.root] allowed_base_paths: []
} }
safe_file_path := validate_file_path(file_path, validation_options) or { safe_file_path := validate_file_path(file_path, validation_options) or {
println(' [DEBUG] Path validation failed: $err') 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) c.status(304)
// 构建响应头 // 构建响应头
mut headers := http.new_header() mut headers := http.new_header()
headers.add_custom('Connection', 'keep-alive') or { }
for key, value in c.headers { for key, value in c.headers {
headers.add_custom(key, value) or { continue } 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 c.headers[key] = value
} }
// 设置 Keep-Alive
c.headers['Connection'] = 'keep-alive'
// 返回文件内容 // 返回文件内容
mut headers := http.new_header() mut headers := http.new_header()
for key, value in c.headers { for key, value in c.headers {