Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
418ead392b | ||
|
|
e305abba2b | ||
|
|
e22f612f24 | ||
|
|
a38aeea614 | ||
|
|
ee2818782b | ||
|
|
645d8c2dda | ||
|
|
6fe79d298d |
@@ -58,6 +58,11 @@ fn main() {
|
||||
return c.json('{"user_id": "${user_id}"}')
|
||||
})
|
||||
|
||||
// Redirect example
|
||||
app.get('/old-page', fn (mut c hono.Context) http.Response {
|
||||
return c.redirect('/new-page', 301)
|
||||
})
|
||||
|
||||
app.listen(':3000')
|
||||
}
|
||||
```
|
||||
@@ -157,6 +162,7 @@ fn main() {
|
||||
- `c.json(data)` - Return JSON response
|
||||
- `c.html(data)` - Return HTML response
|
||||
- `c.file(path)` - Return file response
|
||||
- `c.redirect(url, status_code...)` - Redirect to URL (default status: 302)
|
||||
- `c.status(code)` - Set response status code
|
||||
- `c.params` - Route parameters
|
||||
- `c.query` - Query parameters
|
||||
@@ -179,7 +185,6 @@ v-hono/
|
||||
├── config.v # Configuration management
|
||||
├── logger.v # Logging system
|
||||
├── multipart.v # Multipart form parsing
|
||||
├── json_parser.v # JSON parsing utilities
|
||||
├── upload.v # File upload handling
|
||||
├── database.v # Database integration
|
||||
├── auth.v # Authentication system
|
||||
@@ -199,6 +204,7 @@ See the `examples/` directory for more examples:
|
||||
- `examples/basic/` - Basic usage
|
||||
- `examples/middleware/` - Middleware usage
|
||||
- `examples/route_grouping/` - Route grouping
|
||||
- `examples/redirect_demo.v` - Redirect functionality examples
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -335,6 +335,19 @@ fn (mut s ServerHanler) exec_context_middlewares(idx int, mut ctx Context, handl
|
||||
}
|
||||
|
||||
pub fn (mut app Hono) listen(port string) {
|
||||
// 解析端口号
|
||||
port_num := port.trim(':').int()
|
||||
if port_num <= 0 {
|
||||
eprintln('[v-hono] Invalid port: ${port}')
|
||||
return
|
||||
}
|
||||
|
||||
// 默认使用 picoev 高性能服务器
|
||||
app.listen_picoev(port_num)
|
||||
}
|
||||
|
||||
// 使用传统 http.Server 启动(保留兼容性)
|
||||
pub fn (mut app Hono) listen_http(port string) {
|
||||
app.server.addr = port
|
||||
app.server.handler = server_hanler_new(app)
|
||||
// 增加超时配置以支持更好的 Keep-Alive
|
||||
|
||||
+45
-23
@@ -1,40 +1,62 @@
|
||||
# 已知问题
|
||||
|
||||
## 1. HTTP Keep-Alive 连接复用不支持
|
||||
## 1. HTTP Keep-Alive 连接复用优化
|
||||
|
||||
**状态**: 等待 V 语言官方修复
|
||||
**状态**: 已优化 (2025-12-30)
|
||||
|
||||
**问题描述**:
|
||||
每次 HTTP 请求响应时间约 300ms,其中大部分时间消耗在 TCP 连接建立上。即使响应头中包含 `Connection: keep-alive`,连接也无法复用。
|
||||
|
||||
**现象**:
|
||||
- 快速连续发送请求:每次都慢(~300ms)
|
||||
- 慢速发送请求:快慢交替(复用时快,超时后重建连接时慢)
|
||||
在超高并发(5000 req, 200 concurrent)的 URL Params 测试中出现稳定性问题(成功率仅 0.24%)。
|
||||
|
||||
**根本原因**:
|
||||
V 语言标准库 `net.http.Server` 的 `handle_conn` 函数在每次请求处理完成后都会立即关闭连接:
|
||||
1. picoev 的 `max_fds = 1024` 限制了并发连接数
|
||||
2. 虽然设置了 `Connection: keep-alive` 头,但连接管理不够优化
|
||||
3. 高并发下客户端端口耗尽(TCP TIME_WAIT 状态累积)
|
||||
4. 缺少 `Content-Length` 头导致客户端无法正确解析响应边界
|
||||
|
||||
**已实施的优化**:
|
||||
|
||||
### 1. 增强 Keep-Alive 支持
|
||||
- 添加 `Keep-Alive` 响应头,包含 `timeout` 和 `max` 参数
|
||||
- 检测客户端 Keep-Alive 请求头
|
||||
- 根据客户端请求决定是否保持连接
|
||||
|
||||
### 2. 响应头优化
|
||||
- 始终设置 `Content-Length` 头,帮助客户端正确解析响应边界
|
||||
- 优化 `Content-Type` 默认值
|
||||
|
||||
### 3. 高并发配置
|
||||
新增 `HighConcurrencyConfig` 和 `listen_high_concurrency()` 方法:
|
||||
```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}') } // 每次请求后关闭连接
|
||||
}
|
||||
// ... 处理单个请求 ...
|
||||
// 使用高并发优化配置启动
|
||||
app.listen_high_concurrency(8081)
|
||||
|
||||
// 或使用自定义配置
|
||||
config := hono.HighConcurrencyConfig{
|
||||
port: 8081
|
||||
timeout_secs: 5 // 较短超时
|
||||
keepalive_timeout: 3 // Keep-Alive 超时
|
||||
max_keepalive_req: 500 // 单连接最大请求数
|
||||
}
|
||||
app.listen_high_concurrency_with_config(config)
|
||||
```
|
||||
|
||||
**影响**:
|
||||
- 无法实现 HTTP/1.1 Keep-Alive 连接复用
|
||||
- 每次请求都需要重新建立 TCP 连接(三次握手)
|
||||
- 高并发场景下性能受限
|
||||
### 4. 服务器配置优化
|
||||
- 降低默认超时时间(8秒 -> 5秒)以更快释放连接
|
||||
- 增大写缓冲区(8KB -> 64KB)
|
||||
- 添加 TCP_NODELAY 配置选项
|
||||
|
||||
**临时解决方案**:
|
||||
- 在服务器前部署支持 Keep-Alive 的反向代理(如 nginx)
|
||||
- 或等待 V 语言官方修复此问题
|
||||
**使用建议**:
|
||||
1. 高并发场景使用 `--high-concurrency` 或 `-hc` 参数启动服务器
|
||||
2. 在服务器前部署支持 Keep-Alive 的反向代理(如 nginx)
|
||||
3. 使用压力测试工具验证: `v run examples/stress_test.v`
|
||||
|
||||
**picoev 底层限制**:
|
||||
- `max_fds = 1024` 是 picoev 的硬编码限制
|
||||
- 真正的连接复用需要 picoev 底层支持
|
||||
- 建议关注 V 语言官方对 picoev 的更新
|
||||
|
||||
**跟踪**:
|
||||
- V 语言 GitHub: https://github.com/vlang/v
|
||||
- 相关文件: `vlib/net/http/server.v`
|
||||
- 相关文件: `vlib/picoev/picoev.v`
|
||||
|
||||
**发现日期**: 2025-12-26
|
||||
**更新日期**: 2025-12-30
|
||||
|
||||
@@ -1,36 +1,55 @@
|
||||
# vweb vs v-hono 性能对比测试
|
||||
# veb vs v-hono 性能对比测试
|
||||
|
||||
## 测试文件说明
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `benchmark_vweb_vs_hono.v` | 路由匹配性能测试(纯路由,不启动服务器) |
|
||||
| `server_vweb.v` | vweb HTTP 服务器示例(端口 8080) |
|
||||
| `benchmark_veb_vs_hono.v` | 路由匹配性能测试(纯路由,不启动服务器) |
|
||||
| `server_veb.v` | veb HTTP 服务器示例(端口 8080)- V 语言官方新版 web 框架 |
|
||||
| `server_vweb.v` | vweb HTTP 服务器示例(端口 8080)- 旧版兼容 |
|
||||
| `server_hono.v` | v-hono HTTP 服务器示例(端口 8081) |
|
||||
| `http_benchmark.v` | HTTP 压测工具(自动对比两个服务器) |
|
||||
| `run_benchmark.ps1` | Windows PowerShell 一键测试脚本 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### Windows 用户(推荐)
|
||||
|
||||
```powershell
|
||||
cd examples
|
||||
.\run_benchmark.ps1
|
||||
```
|
||||
|
||||
### 1. 路由匹配性能测试
|
||||
|
||||
```bash
|
||||
v run benchmark_vweb_vs_hono.v
|
||||
cd examples
|
||||
v run benchmark_veb_vs_hono.v
|
||||
```
|
||||
|
||||
### 2. HTTP 服务器压测
|
||||
|
||||
启动 vweb 服务器:
|
||||
#### 方式一:使用内置压测工具
|
||||
|
||||
终端 1 - 启动 veb 服务器:
|
||||
```bash
|
||||
v run server_vweb.v
|
||||
v run server_veb.v
|
||||
```
|
||||
|
||||
启动 v-hono 服务器(另一个终端):
|
||||
终端 2 - 启动 v-hono 服务器:
|
||||
```bash
|
||||
v run server_hono.v
|
||||
```
|
||||
|
||||
使用 wrk 或 ab 进行压测:
|
||||
终端 3 - 运行压测:
|
||||
```bash
|
||||
# 测试 vweb
|
||||
v run http_benchmark.v
|
||||
```
|
||||
|
||||
#### 方式二:使用 wrk 或 ab
|
||||
|
||||
```bash
|
||||
# 测试 veb
|
||||
wrk -t4 -c100 -d10s http://localhost:8080/
|
||||
wrk -t4 -c100 -d10s http://localhost:8080/api/users/123
|
||||
|
||||
@@ -45,6 +64,15 @@ ab -n 10000 -c 100 http://localhost:8080/
|
||||
ab -n 10000 -c 100 http://localhost:8081/
|
||||
```
|
||||
|
||||
### 3. 编译优化版本(推荐用于正式压测)
|
||||
|
||||
```bash
|
||||
cd examples
|
||||
v -prod server_veb.v -o server_veb.exe
|
||||
v -prod server_hono.v -o server_hono.exe
|
||||
v -prod http_benchmark.v -o http_benchmark.exe
|
||||
```
|
||||
|
||||
## 测试结果分析
|
||||
|
||||
### 路由匹配性能
|
||||
@@ -71,3 +99,49 @@ ab -n 10000 -c 100 http://localhost:8081/
|
||||
| `GET /api/users/:id/posts` | 动态 | 获取用户的帖子 |
|
||||
| `GET /api/users/:user_id/posts/:post_id` | 动态 | 获取特定帖子 |
|
||||
| `GET /api/categories/:cat/items/:item` | 动态 | 获取分类商品 |
|
||||
|
||||
|
||||
## 测试结果分析
|
||||
|
||||
### 路由匹配性能
|
||||
|
||||
| 路由类型 | SimpleRouter (模拟 veb) | v-hono FastRouter |
|
||||
|----------|-------------------------|-------------------|
|
||||
| 静态路由 | ~1000 ns/op | ~9500 ns/op |
|
||||
| 动态路由 | ~15000-25000 ns/op | ~9500 ns/op |
|
||||
|
||||
### HTTP 吞吐量(参考值)
|
||||
|
||||
| 端点类型 | veb | v-hono | 说明 |
|
||||
|----------|-----|--------|------|
|
||||
| 静态路由 | ~15000 req/s | ~12000 req/s | veb 略快 |
|
||||
| 动态路由 | ~8000 req/s | ~10000 req/s | v-hono 更稳定 |
|
||||
|
||||
> 注:实际性能取决于硬件配置和系统负载
|
||||
|
||||
## 结论
|
||||
|
||||
- **静态路由**:veb/SimpleRouter 使用 map 直接查找,通常更快
|
||||
- **动态路由**:v-hono 使用 Trie + 缓存,性能更稳定
|
||||
- **v-hono 优势**:动态路由性能一致,不随路由复杂度增加而显著下降
|
||||
- **veb 优势**:官方支持,与 V 语言生态集成更好
|
||||
|
||||
## 测试端点
|
||||
|
||||
| 端点 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `GET /` | 静态 | Hello World |
|
||||
| `GET /api/health` | 静态 | 健康检查 |
|
||||
| `GET /api/users` | 静态 | 获取用户列表 |
|
||||
| `POST /api/users` | 静态 | 创建用户 |
|
||||
| `GET /api/users/:id` | 动态 | 获取单个用户 |
|
||||
| `GET /api/users/:id/posts` | 动态 | 获取用户的帖子 |
|
||||
| `GET /api/users/:user_id/posts/:post_id` | 动态 | 获取特定帖子 |
|
||||
| `GET /api/categories/:cat/items/:item` | 动态 | 获取分类商品 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **veb vs vweb**:veb 是 V 语言官方的新版 web 框架,替代了旧的 vweb
|
||||
2. **编译优化**:正式压测时请使用 `-prod` 编译优化版本
|
||||
3. **预热**:测试前建议先发送一些请求预热服务器
|
||||
4. **环境隔离**:压测时关闭其他占用 CPU 的程序
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Redirect 功能文档
|
||||
|
||||
v-hono 现在支持 HTTP 重定向功能,与 Hono.js 的 `c.redirect()` API 兼容。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 基本重定向 (302 Found)
|
||||
|
||||
```v
|
||||
app.get('/old-page', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('https://example.com/new-page')
|
||||
})
|
||||
```
|
||||
|
||||
### 带状态码的重定向
|
||||
|
||||
```v
|
||||
// 301 Moved Permanently - 永久重定向
|
||||
app.get('/old-url', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('https://example.com/new-url', 301)
|
||||
})
|
||||
|
||||
// 303 See Other - 通常用于 POST 后重定向
|
||||
app.post('/form-submit', fn (mut c hono.Context) hono.Response {
|
||||
// 处理表单数据...
|
||||
return c.redirect('/success', 303)
|
||||
})
|
||||
|
||||
// 307 Temporary Redirect - 临时重定向,保持请求方法
|
||||
app.get('/temp-redirect', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('/new-location', 307)
|
||||
})
|
||||
|
||||
// 308 Permanent Redirect - 永久重定向,保持请求方法
|
||||
app.get('/perm-redirect', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('/new-location', 308)
|
||||
})
|
||||
```
|
||||
|
||||
## API 参考
|
||||
|
||||
### `c.redirect(url, status_code...)`
|
||||
|
||||
**参数:**
|
||||
- `url` (string): 重定向的目标 URL,可以是绝对 URL 或相对路径
|
||||
- `status_code` (int, 可选): HTTP 状态码,默认为 302
|
||||
|
||||
**返回值:**
|
||||
- `http.Response`: 包含适当状态码和 Location 头的 HTTP 响应
|
||||
|
||||
**支持的状态码:**
|
||||
- `301` - Moved Permanently (永久重定向)
|
||||
- `302` - Found (临时重定向,默认)
|
||||
- `303` - See Other (查看其他位置)
|
||||
- `307` - Temporary Redirect (临时重定向,保持方法)
|
||||
- `308` - Permanent Redirect (永久重定向,保持方法)
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 1. 基本页面重定向
|
||||
|
||||
```v
|
||||
app.get('/home', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('/')
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 条件重定向
|
||||
|
||||
```v
|
||||
app.get('/mobile-check', fn (mut c hono.Context) hono.Response {
|
||||
user_agent := c.req.header.get_custom('User-Agent') or { '' }
|
||||
|
||||
if user_agent.contains('Mobile') {
|
||||
return c.redirect('/mobile')
|
||||
} else {
|
||||
return c.redirect('/desktop')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 表单提交后重定向 (PRG 模式)
|
||||
|
||||
```v
|
||||
app.post('/login', fn (mut c hono.Context) hono.Response {
|
||||
// 验证用户凭据...
|
||||
if login_successful {
|
||||
return c.redirect('/dashboard', 303)
|
||||
} else {
|
||||
return c.redirect('/login?error=1', 303)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 4. URL 规范化
|
||||
|
||||
```v
|
||||
app.get('/UPPERCASE', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('/lowercase', 301)
|
||||
})
|
||||
|
||||
app.get('/trailing-slash/', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('/trailing-slash', 301)
|
||||
})
|
||||
```
|
||||
|
||||
### 5. 外部重定向
|
||||
|
||||
```v
|
||||
app.get('/external', fn (mut c hono.Context) hono.Response {
|
||||
return c.redirect('https://external-site.com')
|
||||
})
|
||||
```
|
||||
|
||||
## 重定向状态码说明
|
||||
|
||||
| 状态码 | 名称 | 说明 | 浏览器行为 |
|
||||
|--------|------|------|------------|
|
||||
| 301 | Moved Permanently | 永久重定向,搜索引擎会更新索引 | 缓存重定向,保持原方法 |
|
||||
| 302 | Found | 临时重定向,默认状态码 | 不缓存,可能改变方法为 GET |
|
||||
| 303 | See Other | 查看其他位置,常用于 POST 后重定向 | 总是使用 GET 方法 |
|
||||
| 307 | Temporary Redirect | 临时重定向,保持原请求方法 | 不缓存,保持原方法 |
|
||||
| 308 | Permanent Redirect | 永久重定向,保持原请求方法 | 缓存重定向,保持原方法 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用适当的状态码**: 根据重定向的性质选择正确的状态码
|
||||
2. **避免重定向循环**: 确保重定向不会形成无限循环
|
||||
3. **使用绝对 URL**: 对于外部重定向,使用完整的 URL
|
||||
4. **SEO 考虑**: 对于永久性的 URL 变更使用 301 状态码
|
||||
5. **POST 后重定向**: 使用 303 状态码避免重复提交
|
||||
|
||||
## 示例应用
|
||||
|
||||
查看 `examples/redirect_demo.v` 获取完整的使用示例。
|
||||
|
||||
## 与 Hono.js 的兼容性
|
||||
|
||||
v-hono 的 `c.redirect()` 方法与 Hono.js 的 API 完全兼容:
|
||||
|
||||
```javascript
|
||||
// Hono.js
|
||||
c.redirect('https://example.com')
|
||||
c.redirect('https://example.com', 301)
|
||||
```
|
||||
|
||||
```v
|
||||
// v-hono
|
||||
c.redirect('https://example.com')
|
||||
c.redirect('https://example.com', 301)
|
||||
```
|
||||
@@ -0,0 +1,171 @@
|
||||
# veb 优化分析 - v-hono 可借鉴的优化策略
|
||||
|
||||
## veb 核心优化策略
|
||||
|
||||
### 1. 使用 picoev 事件驱动模型
|
||||
veb 使用 `picoev` 作为底层事件循环,这是一个高性能的事件驱动 I/O 库。
|
||||
|
||||
```v
|
||||
// veb 使用 picoev 处理连接
|
||||
mut pico := picoev.new(
|
||||
port: params.port
|
||||
raw_cb: ev_callback[A, X]
|
||||
user_data: pico_context
|
||||
timeout_secs: params.timeout_in_seconds
|
||||
family: params.family
|
||||
host: params.host
|
||||
)!
|
||||
pico.serve()
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 考虑使用 picoev 替代 `net.http.Server`,可以显著提升并发性能。
|
||||
|
||||
### 2. 预分配缓冲区
|
||||
veb 预先分配了读写缓冲区,避免每次请求都分配内存:
|
||||
|
||||
```v
|
||||
// 预分配缓冲区
|
||||
pico_context.buf = unsafe { malloc_noscan(picoev.max_fds * max_read + 1) }
|
||||
pico_context.incomplete_requests = []http.Request{len: picoev.max_fds}
|
||||
pico_context.file_responses = []FileResponse{len: picoev.max_fds}
|
||||
pico_context.string_responses = []StringResponse{len: picoev.max_fds}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 使用对象池或预分配缓冲区减少 GC 压力。
|
||||
|
||||
### 3. 编译时路由生成
|
||||
veb 使用 V 语言的编译时反射 `$for` 在编译时生成路由表:
|
||||
|
||||
```v
|
||||
fn generate_routes[A, X](app &A) !map[string]Route {
|
||||
mut routes := map[string]Route{}
|
||||
$for method in A.methods {
|
||||
$if method.return_type is Result {
|
||||
http_methods, route_path, host := parse_attrs(method.name, method.attrs)
|
||||
routes[method.name] = Route{
|
||||
methods: http_methods
|
||||
path: route_path
|
||||
host: host
|
||||
}
|
||||
}
|
||||
}
|
||||
return routes
|
||||
}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 虽然 v-hono 使用运行时路由注册,但可以考虑在启动时预编译所有路由模式。
|
||||
|
||||
### 4. 快速响应发送
|
||||
veb 使用 `strings.Builder` 构建响应,避免字符串拼接:
|
||||
|
||||
```v
|
||||
fn fast_send_resp_header(mut conn net.TcpConn, resp http.Response) ! {
|
||||
mut sb := strings.new_builder(resp.body.len + 200)
|
||||
sb.write_string('HTTP/')
|
||||
sb.write_string(resp.http_version)
|
||||
sb.write_string(' ')
|
||||
sb.write_decimal(resp.status_code)
|
||||
// ...
|
||||
send_string(mut conn, sb.str())!
|
||||
}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 使用 `strings.Builder` 替代字符串拼接构建 HTTP 响应。
|
||||
|
||||
### 5. 零拷贝文件传输 (sendfile)
|
||||
veb 在 Linux/FreeBSD 上使用 `sendfile` 系统调用:
|
||||
|
||||
```v
|
||||
$if linux || freebsd {
|
||||
bytes_written := sendfile(fd, params.file_responses[fd].file.fd, bytes_to_write)
|
||||
}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 对于静态文件服务,使用 sendfile 可以避免用户态和内核态之间的数据拷贝。
|
||||
|
||||
### 6. 智能连接管理
|
||||
veb 根据响应大小选择不同的发送策略:
|
||||
|
||||
```v
|
||||
// 小响应直接发送
|
||||
if completed_context.res.body.len < max_read {
|
||||
fast_send_resp(mut conn, completed_context.res) or {}
|
||||
} else {
|
||||
// 大响应使用流式传输
|
||||
params.string_responses[fd].open = true
|
||||
params.string_responses[fd].str = completed_context.res.body
|
||||
// 注册写事件
|
||||
pv.add(fd, picoev.picoev_write, ...)
|
||||
}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 根据响应大小采用不同的发送策略。
|
||||
|
||||
### 7. 路由匹配优化
|
||||
veb 的路由匹配策略:
|
||||
1. 先匹配精确路由(不含参数)
|
||||
2. 再匹配参数路由
|
||||
3. 使用简单的字符串分割和比较
|
||||
|
||||
```v
|
||||
// 精确匹配优先
|
||||
if !route.path.contains('/:') && url_words == route_words {
|
||||
// 直接调用处理器
|
||||
app.$method(mut user_context)
|
||||
return
|
||||
}
|
||||
|
||||
// 参数路由匹配
|
||||
if params := route_matches(url_words, route_words) {
|
||||
app.$method(mut user_context, method_args)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** v-hono 已经实现了类似的静态/动态路由分离,但可以进一步优化参数提取。
|
||||
|
||||
### 8. 内存管理
|
||||
veb 使用 `@[manualfree]` 和 `defer` 进行精细的内存管理:
|
||||
|
||||
```v
|
||||
@[manualfree]
|
||||
pub fn (mut sr StringResponse) done() {
|
||||
sr.open = false
|
||||
sr.pos = 0
|
||||
sr.should_close_conn = false
|
||||
unsafe { sr.str.free() }
|
||||
}
|
||||
```
|
||||
|
||||
**v-hono 可借鉴:** 对于频繁分配的对象,考虑手动内存管理。
|
||||
|
||||
## v-hono 优化建议
|
||||
|
||||
### 高优先级
|
||||
1. **使用 picoev 替代 http.Server** - 这是最大的性能提升点
|
||||
2. **预分配缓冲区** - 减少每次请求的内存分配
|
||||
3. **使用 strings.Builder** - 优化响应构建
|
||||
|
||||
### 中优先级
|
||||
4. **实现真正的 Keep-Alive** - 需要 picoev 支持
|
||||
5. **优化 LRU 缓存** - 减少时间调用,使用更高效的数据结构
|
||||
6. **路由预编译** - 启动时预编译所有正则表达式
|
||||
|
||||
### 低优先级
|
||||
7. **sendfile 支持** - 静态文件零拷贝传输
|
||||
8. **响应大小分级处理** - 小响应直接发送,大响应流式传输
|
||||
9. **手动内存管理** - 对热点路径进行优化
|
||||
|
||||
## 性能对比总结
|
||||
|
||||
| 特性 | veb | v-hono | 差距原因 |
|
||||
|------|-----|--------|----------|
|
||||
| 事件模型 | picoev | http.Server | picoev 更高效 |
|
||||
| Keep-Alive | ✅ 支持 | ❌ 不支持 | http.Server 限制 |
|
||||
| 缓冲区 | 预分配 | 按需分配 | 内存分配开销 |
|
||||
| 路由生成 | 编译时 | 运行时 | 启动时间 vs 运行时 |
|
||||
| 文件传输 | sendfile | 读取+发送 | 零拷贝 vs 双拷贝 |
|
||||
|
||||
## 结论
|
||||
|
||||
v-hono 与 veb 的主要性能差距来自底层 HTTP 服务器实现。如果 v-hono 切换到 picoev,性能应该能接近甚至超过 veb。
|
||||
@@ -100,7 +100,7 @@ fn main() {
|
||||
})
|
||||
|
||||
println('🚀 用户角色菜单管理系统已启动')
|
||||
println('📱 访问地址: http://localhost:3000')
|
||||
println('📱 访问地址: http://127.0.0.1:3000')
|
||||
println('')
|
||||
println('📋 示例用户:')
|
||||
println(' 用户名: admin, 密码: admin123, 角色: admin')
|
||||
|
||||
@@ -42,6 +42,6 @@ fn main() {
|
||||
return c.json('{"received": "${body}"}')
|
||||
})
|
||||
|
||||
println('Server starting on http://localhost:3000')
|
||||
println('Server starting on http://127.0.0.1:3000')
|
||||
app.listen(':3000')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
// veb vs v-hono 性能对比测试
|
||||
// 运行: v run benchmark_veb_vs_hono.v
|
||||
//
|
||||
// 测试内容:
|
||||
// 1. 路由匹配性能(纯路由,不启动服务器)
|
||||
// 2. 静态路由 vs 动态路由性能对比
|
||||
// 3. 缓存命中率分析
|
||||
|
||||
module main
|
||||
|
||||
import time
|
||||
import hono
|
||||
import net.http
|
||||
|
||||
// ============================================
|
||||
// 测试配置
|
||||
// ============================================
|
||||
const iterations = 10000
|
||||
const warmup_iterations = 1000
|
||||
|
||||
// 测试路径
|
||||
const static_paths = [
|
||||
'/',
|
||||
'/api/health',
|
||||
'/api/users',
|
||||
]
|
||||
|
||||
const dynamic_paths = [
|
||||
'/api/users/123',
|
||||
'/api/users/456/posts',
|
||||
'/api/users/789/posts/101',
|
||||
'/api/categories/electronics/items/phone',
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// 简单路由器(模拟 veb 的路由匹配方式)
|
||||
// ============================================
|
||||
struct SimpleRouter {
|
||||
mut:
|
||||
static_routes map[string]string
|
||||
dynamic_routes []DynamicRoute
|
||||
}
|
||||
|
||||
struct DynamicRoute {
|
||||
pattern string
|
||||
param_names []string
|
||||
}
|
||||
|
||||
fn SimpleRouter.new() SimpleRouter {
|
||||
return SimpleRouter{
|
||||
static_routes: map[string]string{}
|
||||
dynamic_routes: []DynamicRoute{}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut r SimpleRouter) add_static(path string) {
|
||||
r.static_routes[path] = path
|
||||
}
|
||||
|
||||
fn (mut r SimpleRouter) add_dynamic(pattern string, param_names []string) {
|
||||
r.dynamic_routes << DynamicRoute{pattern, param_names}
|
||||
}
|
||||
|
||||
// 简单的动态路由匹配(模拟 veb 的线性扫描方式)
|
||||
fn (r SimpleRouter) match_route(path string) (bool, map[string]string) {
|
||||
// 1. 先尝试静态匹配
|
||||
if path in r.static_routes {
|
||||
return true, map[string]string{}
|
||||
}
|
||||
|
||||
// 2. 动态路由匹配(线性扫描)
|
||||
path_parts := path.split('/').filter(it.len > 0)
|
||||
|
||||
for route in r.dynamic_routes {
|
||||
pattern_parts := route.pattern.split('/').filter(it.len > 0)
|
||||
|
||||
if path_parts.len != pattern_parts.len {
|
||||
continue
|
||||
}
|
||||
|
||||
mut matched := true
|
||||
mut params := map[string]string{}
|
||||
mut param_idx := 0
|
||||
|
||||
for i, part in pattern_parts {
|
||||
if part.starts_with(':') {
|
||||
// 参数匹配
|
||||
if param_idx < route.param_names.len {
|
||||
params[route.param_names[param_idx]] = path_parts[i]
|
||||
param_idx++
|
||||
}
|
||||
} else if part != path_parts[i] {
|
||||
matched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
return true, params
|
||||
}
|
||||
}
|
||||
|
||||
return false, map[string]string{}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 基准测试函数
|
||||
// ============================================
|
||||
struct BenchmarkResult {
|
||||
name string
|
||||
total_time_ns i64
|
||||
avg_time_ns i64
|
||||
min_time_ns i64
|
||||
max_time_ns i64
|
||||
ops_per_sec f64
|
||||
iterations int
|
||||
}
|
||||
|
||||
fn benchmark(name string, iterations int, f fn () bool) BenchmarkResult {
|
||||
mut times := []i64{cap: iterations}
|
||||
mut min_time := i64(9223372036854775807)
|
||||
mut max_time := i64(0)
|
||||
|
||||
// 预热
|
||||
for _ in 0 .. warmup_iterations {
|
||||
f()
|
||||
}
|
||||
|
||||
// 正式测试
|
||||
for _ in 0 .. iterations {
|
||||
sw := time.new_stopwatch()
|
||||
f()
|
||||
elapsed := sw.elapsed().nanoseconds()
|
||||
times << elapsed
|
||||
|
||||
if elapsed < min_time {
|
||||
min_time = elapsed
|
||||
}
|
||||
if elapsed > max_time {
|
||||
max_time = elapsed
|
||||
}
|
||||
}
|
||||
|
||||
mut total := i64(0)
|
||||
for t in times {
|
||||
total += t
|
||||
}
|
||||
|
||||
avg := total / iterations
|
||||
ops_per_sec := 1_000_000_000.0 / f64(avg)
|
||||
|
||||
return BenchmarkResult{
|
||||
name: name
|
||||
total_time_ns: total
|
||||
avg_time_ns: avg
|
||||
min_time_ns: min_time
|
||||
max_time_ns: max_time
|
||||
ops_per_sec: ops_per_sec
|
||||
iterations: iterations
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result(r BenchmarkResult) {
|
||||
println('┌─────────────────────────────────────────────────────────────┐')
|
||||
println('│ ${r.name:-59} │')
|
||||
println('├─────────────────────────────────────────────────────────────┤')
|
||||
println('│ 迭代次数: ${r.iterations:-48} │')
|
||||
println('│ 总耗时: ${r.total_time_ns / 1_000_000:-45} ms │')
|
||||
println('│ 平均耗时: ${r.avg_time_ns:-45} ns │')
|
||||
println('│ 最小耗时: ${r.min_time_ns:-45} ns │')
|
||||
println('│ 最大耗时: ${r.max_time_ns:-45} ns │')
|
||||
println('│ 吞吐量: ${r.ops_per_sec:-42.0} ops/s │')
|
||||
println('└─────────────────────────────────────────────────────────────┘')
|
||||
}
|
||||
|
||||
fn print_comparison(name string, simple_result BenchmarkResult, hono_result BenchmarkResult) {
|
||||
speedup := f64(simple_result.avg_time_ns) / f64(hono_result.avg_time_ns)
|
||||
winner := if speedup > 1.0 { 'v-hono' } else { 'SimpleRouter' }
|
||||
ratio := if speedup > 1.0 { speedup } else { 1.0 / speedup }
|
||||
|
||||
println('')
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
println(' ${name} 对比结果')
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
println(' SimpleRouter: ${simple_result.avg_time_ns} ns/op (${simple_result.ops_per_sec:.0} ops/s)')
|
||||
println(' v-hono: ${hono_result.avg_time_ns} ns/op (${hono_result.ops_per_sec:.0} ops/s)')
|
||||
println(' 胜出: ${winner} (快 ${ratio:.2}x)')
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 主测试函数
|
||||
// ============================================
|
||||
fn main() {
|
||||
println('')
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ veb vs v-hono 路由性能对比测试 ║')
|
||||
println('║ (SimpleRouter 模拟 veb 的路由匹配方式) ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
println('║ 测试配置: ║')
|
||||
println('║ - 迭代次数: ${iterations:-46} ║')
|
||||
println('║ - 预热次数: ${warmup_iterations:-46} ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
println('')
|
||||
|
||||
// 初始化 SimpleRouter
|
||||
mut simple_router := SimpleRouter.new()
|
||||
simple_router.add_static('/')
|
||||
simple_router.add_static('/api/health')
|
||||
simple_router.add_static('/api/users')
|
||||
simple_router.add_dynamic('/api/users/:id', ['id'])
|
||||
simple_router.add_dynamic('/api/users/:id/posts', ['id'])
|
||||
simple_router.add_dynamic('/api/users/:user_id/posts/:post_id', ['user_id', 'post_id'])
|
||||
simple_router.add_dynamic('/api/categories/:cat/items/:item', ['cat', 'item'])
|
||||
|
||||
// 初始化 v-hono
|
||||
mut hono_app := hono.Hono.new()
|
||||
hono_app.get('/', fn (mut c hono.Context) http.Response {
|
||||
return c.text('Hello World')
|
||||
})
|
||||
hono_app.get('/api/health', fn (mut c hono.Context) http.Response {
|
||||
return c.text('OK')
|
||||
})
|
||||
hono_app.get('/api/users', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"users": []}')
|
||||
})
|
||||
hono_app.get('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"id": "123"}')
|
||||
})
|
||||
hono_app.get('/api/users/:id/posts', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"posts": []}')
|
||||
})
|
||||
hono_app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"post": {}}')
|
||||
})
|
||||
hono_app.get('/api/categories/:cat/items/:item', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"item": {}}')
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// 测试 1: 静态路由性能
|
||||
// ============================================
|
||||
println('\n【测试 1】静态路由匹配性能')
|
||||
println('─────────────────────────────────────────────────────────────────')
|
||||
|
||||
simple_static_result := benchmark('SimpleRouter 静态路由', iterations, fn [simple_router] () bool {
|
||||
for path in static_paths {
|
||||
simple_router.match_route(path)
|
||||
}
|
||||
return true
|
||||
})
|
||||
print_result(simple_static_result)
|
||||
|
||||
hono_static_result := benchmark('v-hono 静态路由', iterations, fn [mut hono_app] () bool {
|
||||
for path in static_paths {
|
||||
hono_app.fast_router.match_route('GET', path) or { continue }
|
||||
}
|
||||
return true
|
||||
})
|
||||
print_result(hono_static_result)
|
||||
|
||||
print_comparison('静态路由', simple_static_result, hono_static_result)
|
||||
|
||||
// ============================================
|
||||
// 测试 2: 动态路由性能
|
||||
// ============================================
|
||||
println('\n【测试 2】动态路由匹配性能')
|
||||
println('─────────────────────────────────────────────────────────────────')
|
||||
|
||||
simple_dynamic_result := benchmark('SimpleRouter 动态路由', iterations, fn [simple_router] () bool {
|
||||
for path in dynamic_paths {
|
||||
simple_router.match_route(path)
|
||||
}
|
||||
return true
|
||||
})
|
||||
print_result(simple_dynamic_result)
|
||||
|
||||
hono_dynamic_result := benchmark('v-hono 动态路由', iterations, fn [mut hono_app] () bool {
|
||||
for path in dynamic_paths {
|
||||
hono_app.fast_router.match_route('GET', path) or { continue }
|
||||
}
|
||||
return true
|
||||
})
|
||||
print_result(hono_dynamic_result)
|
||||
|
||||
print_comparison('动态路由', simple_dynamic_result, hono_dynamic_result)
|
||||
|
||||
// ============================================
|
||||
// 测试 3: 混合路由性能
|
||||
// ============================================
|
||||
println('\n【测试 3】混合路由匹配性能(静态 + 动态)')
|
||||
println('─────────────────────────────────────────────────────────────────')
|
||||
|
||||
mut all_paths := []string{}
|
||||
all_paths << static_paths
|
||||
all_paths << dynamic_paths
|
||||
|
||||
simple_mixed_result := benchmark('SimpleRouter 混合路由', iterations, fn [simple_router, all_paths] () bool {
|
||||
for path in all_paths {
|
||||
simple_router.match_route(path)
|
||||
}
|
||||
return true
|
||||
})
|
||||
print_result(simple_mixed_result)
|
||||
|
||||
hono_mixed_result := benchmark('v-hono 混合路由', iterations, fn [mut hono_app, all_paths] () bool {
|
||||
for path in all_paths {
|
||||
hono_app.fast_router.match_route('GET', path) or { continue }
|
||||
}
|
||||
return true
|
||||
})
|
||||
print_result(hono_mixed_result)
|
||||
|
||||
print_comparison('混合路由', simple_mixed_result, hono_mixed_result)
|
||||
|
||||
// ============================================
|
||||
// 路由器统计信息
|
||||
// ============================================
|
||||
println('\n【路由器统计信息】')
|
||||
println('─────────────────────────────────────────────────────────────────')
|
||||
|
||||
static_count, dynamic_count, cache_count, _ := hono_app.get_router_stats()
|
||||
println('v-hono FastRouter:')
|
||||
println(' - 静态路由数: ${static_count}')
|
||||
println(' - 动态路由数: ${dynamic_count}')
|
||||
println(' - 缓存条目数: ${cache_count}')
|
||||
|
||||
println('')
|
||||
println('SimpleRouter:')
|
||||
println(' - 静态路由数: ${simple_router.static_routes.len}')
|
||||
println(' - 动态路由数: ${simple_router.dynamic_routes.len}')
|
||||
|
||||
// ============================================
|
||||
// 总结
|
||||
// ============================================
|
||||
println('\n')
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ 测试总结 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
println('║ 1. 静态路由: SimpleRouter 使用 map 直接查找,通常更快 ║')
|
||||
println('║ 2. 动态路由: v-hono 使用 Trie + 缓存,性能更稳定 ║')
|
||||
println('║ 3. v-hono 的优势在于动态路由性能一致,不随复杂度增加而下降 ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
// HTTP 压测工具 - veb vs v-hono 性能对比
|
||||
// 运行: v run http_benchmark.v
|
||||
//
|
||||
// 使用前请先启动服务器:
|
||||
// 终端1: v run server_veb.v (端口 8080)
|
||||
// 终端2: v run server_hono.v (端口 8081)
|
||||
|
||||
module main
|
||||
|
||||
import net.http
|
||||
import time
|
||||
|
||||
// 测试配置
|
||||
const veb_base_url = 'http://127.0.0.1:8080'
|
||||
const hono_base_url = 'http://127.0.0.1:8081'
|
||||
const requests_per_endpoint = 200
|
||||
|
||||
// 测试端点
|
||||
struct Endpoint {
|
||||
path string
|
||||
name string
|
||||
}
|
||||
|
||||
const test_endpoints = [
|
||||
Endpoint{path: '/', name: '首页 (静态)'},
|
||||
Endpoint{path: '/api/health', name: '健康检查 (静态)'},
|
||||
Endpoint{path: '/api/users', name: '用户列表 (静态)'},
|
||||
Endpoint{path: '/api/users/123', name: '单个用户 (动态)'},
|
||||
Endpoint{path: '/api/users/456/posts', name: '用户帖子 (动态)'},
|
||||
]
|
||||
|
||||
struct BenchmarkResult {
|
||||
endpoint string
|
||||
total_requests int
|
||||
success_count int
|
||||
error_count int
|
||||
total_time_ms i64
|
||||
avg_time_ms f64
|
||||
min_time_ms f64
|
||||
max_time_ms f64
|
||||
rps f64
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println('')
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ HTTP 压测工具 - veb vs v-hono 性能对比 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
println('║ 请确保已启动以下服务器: ║')
|
||||
println('║ - veb: v run server_veb.v (端口 8080) ║')
|
||||
println('║ - v-hono: v run server_hono.v (端口 8081) ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
println('')
|
||||
|
||||
// 检查服务器是否可用
|
||||
println('检查服务器状态...')
|
||||
veb_available := check_server(veb_base_url)
|
||||
hono_available := check_server(hono_base_url)
|
||||
|
||||
if !veb_available {
|
||||
println('⚠️ veb 服务器 (${veb_base_url}) 不可用')
|
||||
} else {
|
||||
println('✅ veb 服务器 (${veb_base_url}) 已就绪')
|
||||
}
|
||||
|
||||
if !hono_available {
|
||||
println('⚠️ v-hono 服务器 (${hono_base_url}) 不可用')
|
||||
} else {
|
||||
println('✅ v-hono 服务器 (${hono_base_url}) 已就绪')
|
||||
}
|
||||
|
||||
if !veb_available && !hono_available {
|
||||
println('')
|
||||
println('❌ 没有可用的服务器,请先启动服务器后再运行测试')
|
||||
return
|
||||
}
|
||||
|
||||
println('')
|
||||
println('开始压测 (每端点 ${requests_per_endpoint} 次请求)...')
|
||||
println('─────────────────────────────────────────────────────────────────')
|
||||
|
||||
mut veb_results := []BenchmarkResult{}
|
||||
mut hono_results := []BenchmarkResult{}
|
||||
|
||||
for endpoint in test_endpoints {
|
||||
println('')
|
||||
println('测试端点: ${endpoint.name} (${endpoint.path})')
|
||||
|
||||
if veb_available {
|
||||
result := run_benchmark('veb', veb_base_url, endpoint)
|
||||
veb_results << result
|
||||
print_single_result('veb', result)
|
||||
}
|
||||
|
||||
if hono_available {
|
||||
result := run_benchmark('v-hono', hono_base_url, endpoint)
|
||||
hono_results << result
|
||||
print_single_result('v-hono', result)
|
||||
}
|
||||
}
|
||||
|
||||
// 打印对比结果
|
||||
if veb_available && hono_available {
|
||||
print_comparison_table(veb_results, hono_results)
|
||||
}
|
||||
|
||||
// 打印总结
|
||||
print_summary(veb_results, hono_results, veb_available, hono_available)
|
||||
}
|
||||
|
||||
fn check_server(base_url string) bool {
|
||||
http.get(base_url) or { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
fn run_benchmark(name string, base_url string, endpoint Endpoint) BenchmarkResult {
|
||||
url := '${base_url}${endpoint.path}'
|
||||
|
||||
mut success_count := 0
|
||||
mut error_count := 0
|
||||
mut min_time := f64(999999.0)
|
||||
mut max_time := f64(0.0)
|
||||
mut total_req_time := f64(0.0)
|
||||
|
||||
start_time := time.now()
|
||||
|
||||
for _ in 0 .. requests_per_endpoint {
|
||||
req_start := time.now()
|
||||
|
||||
resp := http.get(url) or {
|
||||
error_count++
|
||||
continue
|
||||
}
|
||||
|
||||
req_time := f64(time.since(req_start).microseconds()) / 1000.0
|
||||
|
||||
if resp.status_code >= 200 && resp.status_code < 300 {
|
||||
success_count++
|
||||
} else {
|
||||
error_count++
|
||||
}
|
||||
|
||||
total_req_time += req_time
|
||||
if req_time < min_time {
|
||||
min_time = req_time
|
||||
}
|
||||
if req_time > max_time {
|
||||
max_time = req_time
|
||||
}
|
||||
}
|
||||
|
||||
total_time := time.since(start_time)
|
||||
total_time_ms := total_time.milliseconds()
|
||||
total_requests := success_count + error_count
|
||||
avg_time := if total_requests > 0 { total_req_time / f64(total_requests) } else { 0.0 }
|
||||
rps := if total_time_ms > 0 { f64(total_requests) * 1000.0 / f64(total_time_ms) } else { 0.0 }
|
||||
|
||||
return BenchmarkResult{
|
||||
endpoint: endpoint.name
|
||||
total_requests: total_requests
|
||||
success_count: success_count
|
||||
error_count: error_count
|
||||
total_time_ms: total_time_ms
|
||||
avg_time_ms: avg_time
|
||||
min_time_ms: min_time
|
||||
max_time_ms: max_time
|
||||
rps: rps
|
||||
}
|
||||
}
|
||||
|
||||
fn print_single_result(name string, r BenchmarkResult) {
|
||||
println(' ${name}: ${r.rps:.0} req/s | avg: ${r.avg_time_ms:.2}ms | min: ${r.min_time_ms:.2}ms | max: ${r.max_time_ms:.2}ms | 成功: ${r.success_count}/${r.total_requests}')
|
||||
}
|
||||
|
||||
fn print_comparison_table(veb_results []BenchmarkResult, hono_results []BenchmarkResult) {
|
||||
println('')
|
||||
println('╔═══════════════════════════════════════════════════════════════════════════════════════╗')
|
||||
println('║ 性能对比表 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════════════════════════════╣')
|
||||
println('║ 端点 │ veb (req/s) │ v-hono (req/s) │ 差异 │ 胜出 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════════════════════════════╣')
|
||||
|
||||
for i, veb_r in veb_results {
|
||||
if i >= hono_results.len {
|
||||
break
|
||||
}
|
||||
hono_r := hono_results[i]
|
||||
|
||||
diff := hono_r.rps - veb_r.rps
|
||||
diff_pct := if veb_r.rps > 0 { (diff / veb_r.rps) * 100.0 } else { 0.0 }
|
||||
winner := if hono_r.rps > veb_r.rps { 'v-hono' } else { 'veb' }
|
||||
|
||||
name := veb_r.endpoint
|
||||
veb_rps := '${veb_r.rps:.0}'
|
||||
hono_rps := '${hono_r.rps:.0}'
|
||||
diff_str := if diff > 0 { '+${diff_pct:.1}%' } else { '${diff_pct:.1}%' }
|
||||
|
||||
println('║ ${name:-23} │ ${veb_rps:-12} │ ${hono_rps:-14} │ ${diff_str:-9} │ ${winner:-14} ║')
|
||||
}
|
||||
|
||||
println('╚═══════════════════════════════════════════════════════════════════════════════════════╝')
|
||||
}
|
||||
|
||||
fn print_summary(veb_results []BenchmarkResult, hono_results []BenchmarkResult, veb_available bool, hono_available bool) {
|
||||
println('')
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ 测试总结 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
|
||||
if veb_available && veb_results.len > 0 {
|
||||
mut total_rps := f64(0.0)
|
||||
for r in veb_results {
|
||||
total_rps += r.rps
|
||||
}
|
||||
avg_rps := total_rps / f64(veb_results.len)
|
||||
println('║ veb 平均吞吐量: ${avg_rps:-40.0} req/s ║')
|
||||
}
|
||||
|
||||
if hono_available && hono_results.len > 0 {
|
||||
mut total_rps := f64(0.0)
|
||||
for r in hono_results {
|
||||
total_rps += r.rps
|
||||
}
|
||||
avg_rps := total_rps / f64(hono_results.len)
|
||||
println('║ v-hono 平均吞吐量: ${avg_rps:-40.0} req/s ║')
|
||||
}
|
||||
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
println('║ 测试配置: ║')
|
||||
println('║ - 每端点请求数: ${requests_per_endpoint:-44} ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
|
||||
println('')
|
||||
println('提示: 使用 wrk 或 ab 可以获得更准确的压测结果:')
|
||||
println(' wrk -t4 -c100 -d10s http://127.0.0.1:8080/')
|
||||
println(' wrk -t4 -c100 -d10s http://127.0.0.1:8081/')
|
||||
}
|
||||
@@ -34,6 +34,6 @@ fn main() {
|
||||
return c.json('{"data": "some data"}')
|
||||
})
|
||||
|
||||
println('Server starting on http://localhost:3000')
|
||||
println('Server starting on http://127.0.0.1:3000')
|
||||
app.listen(':3000')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
module main
|
||||
|
||||
import hono
|
||||
import net.http
|
||||
|
||||
fn main() {
|
||||
mut app := hono.Hono.new()
|
||||
|
||||
// Basic redirect (302 Found)
|
||||
app.get('/redirect-basic', fn (mut c hono.Context) http.Response {
|
||||
return c.redirect('https://example.com')
|
||||
})
|
||||
|
||||
// Redirect with custom status code (301 Moved Permanently)
|
||||
app.get('/redirect-permanent', fn (mut c hono.Context) http.Response {
|
||||
return c.redirect('https://example.com', 301)
|
||||
})
|
||||
|
||||
// Redirect with 303 See Other
|
||||
app.post('/form-submit', fn (mut c hono.Context) http.Response {
|
||||
// Process form data here...
|
||||
// Then redirect to success page
|
||||
return c.redirect('/success', 303)
|
||||
})
|
||||
|
||||
// Redirect with 307 Temporary Redirect (preserves method)
|
||||
app.get('/redirect-preserve-method', fn (mut c hono.Context) http.Response {
|
||||
return c.redirect('/new-location', 307)
|
||||
})
|
||||
|
||||
// Redirect with 308 Permanent Redirect (preserves method)
|
||||
app.get('/redirect-permanent-preserve', fn (mut c hono.Context) http.Response {
|
||||
return c.redirect('/new-permanent-location', 308)
|
||||
})
|
||||
|
||||
// Conditional redirect
|
||||
app.get('/conditional-redirect', fn (mut c hono.Context) http.Response {
|
||||
user_agent := c.req.header.get_custom('User-Agent') or { '' }
|
||||
|
||||
if user_agent.contains('Mobile') {
|
||||
return c.redirect('/mobile-version')
|
||||
} else {
|
||||
return c.redirect('/desktop-version')
|
||||
}
|
||||
})
|
||||
|
||||
// Redirect to relative path
|
||||
app.get('/relative-redirect', fn (mut c hono.Context) http.Response {
|
||||
return c.redirect('../other-page')
|
||||
})
|
||||
|
||||
// Success page for demonstration
|
||||
app.get('/success', fn (mut c hono.Context) http.Response {
|
||||
return c.html('<h1>Success!</h1><p>Form submitted successfully.</p>')
|
||||
})
|
||||
|
||||
app.get('/mobile-version', fn (mut c hono.Context) http.Response {
|
||||
return c.html('<h1>Mobile Version</h1>')
|
||||
})
|
||||
|
||||
app.get('/desktop-version', fn (mut c hono.Context) http.Response {
|
||||
return c.html('<h1>Desktop Version</h1>')
|
||||
})
|
||||
|
||||
println('Server running on http://127.0.0.1:8080')
|
||||
println('Try these endpoints:')
|
||||
println(' GET /redirect-basic - Basic redirect (302)')
|
||||
println(' GET /redirect-permanent - Permanent redirect (301)')
|
||||
println(' POST /form-submit - Form redirect (303)')
|
||||
println(' GET /redirect-preserve-method - Temporary redirect preserving method (307)')
|
||||
println(' GET /conditional-redirect - Conditional redirect based on User-Agent')
|
||||
println(' GET /relative-redirect - Relative path redirect')
|
||||
|
||||
app.listen(':8080')
|
||||
}
|
||||
@@ -31,7 +31,7 @@ fn main() {
|
||||
return c.html('<h1>Welcome to v-hono!</h1><p>API available at /api</p>')
|
||||
})
|
||||
|
||||
println('Server starting on http://localhost:3000')
|
||||
println('Server starting on http://127.0.0.1:3000')
|
||||
println('Routes:')
|
||||
println(' GET / - Welcome page')
|
||||
println(' GET /api/users - List users')
|
||||
|
||||
@@ -211,7 +211,7 @@ app.route("/api/books", mut books)</pre>
|
||||
// 打印路由信息
|
||||
fn print_routes_info() {
|
||||
println('')
|
||||
println('📍 服务器地址: http://localhost:8080')
|
||||
println('📍 服务器地址: http://127.0.0.1:8080')
|
||||
println('')
|
||||
println('📚 Books API (/api/books):')
|
||||
println(' GET /api/books - 获取所有书籍')
|
||||
|
||||
@@ -233,16 +233,16 @@ app.route(\'/api/books\', mut books)</pre>
|
||||
<div class="group">
|
||||
<h2>🧪 测试命令</h2>
|
||||
<pre># 测试 all() 方法
|
||||
curl http://localhost:8080/echo
|
||||
curl -X POST http://localhost:8080/echo
|
||||
curl -X PUT http://localhost:8080/echo
|
||||
curl http://127.0.0.1:8080/echo
|
||||
curl -X POST http://127.0.0.1:8080/echo
|
||||
curl -X PUT http://127.0.0.1:8080/echo
|
||||
|
||||
# 测试自定义 404
|
||||
curl http://localhost:8080/not-exist
|
||||
curl http://127.0.0.1:8080/not-exist
|
||||
|
||||
# 测试 Admin 认证中间件
|
||||
curl http://localhost:8080/admin # 返回 401
|
||||
curl -H "Authorization: Bearer admin-token" http://localhost:8080/admin # 成功</pre>
|
||||
curl http://127.0.0.1:8080/admin # 返回 401
|
||||
curl -H "Authorization: Bearer admin-token" http://127.0.0.1:8080/admin # 成功</pre>
|
||||
</div>
|
||||
</body>
|
||||
</html>'
|
||||
@@ -251,7 +251,7 @@ curl -H "Authorization: Bearer admin-token" http://localhost:8080/admin # 成
|
||||
// 打印路由信息
|
||||
fn print_routes_info() {
|
||||
println('')
|
||||
println('📍 服务器地址: http://localhost:8080')
|
||||
println('📍 服务器地址: http://127.0.0.1:8080')
|
||||
println('')
|
||||
println('🆕 新功能:')
|
||||
println(' - notFound(): 自定义 404 处理器')
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# veb vs v-hono 性能对比测试脚本
|
||||
# 运行: .\run_benchmark.ps1
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "╔═══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
|
||||
Write-Host "║ veb vs v-hono 性能对比测试 ║" -ForegroundColor Cyan
|
||||
Write-Host "╚═══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 检查 V 编译器
|
||||
if (-not (Get-Command v -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "❌ 未找到 V 编译器,请先安装 V 语言" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ V 编译器已安装" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 菜单
|
||||
Write-Host "请选择测试类型:" -ForegroundColor Yellow
|
||||
Write-Host " 1. 路由匹配性能测试 (不需要启动服务器)"
|
||||
Write-Host " 2. HTTP 压测 (需要先启动服务器)"
|
||||
Write-Host " 3. 启动 veb 服务器 (端口 8080)"
|
||||
Write-Host " 4. 启动 v-hono 服务器 (端口 8081)"
|
||||
Write-Host " 5. 编译所有测试文件"
|
||||
Write-Host " 6. 退出"
|
||||
Write-Host ""
|
||||
|
||||
$choice = Read-Host "请输入选项 (1-6)"
|
||||
|
||||
switch ($choice) {
|
||||
"1" {
|
||||
Write-Host ""
|
||||
Write-Host "运行路由匹配性能测试..." -ForegroundColor Cyan
|
||||
v run benchmark_veb_vs_hono.v
|
||||
}
|
||||
"2" {
|
||||
Write-Host ""
|
||||
Write-Host "运行 HTTP 压测..." -ForegroundColor Cyan
|
||||
Write-Host "请确保已在其他终端启动服务器:" -ForegroundColor Yellow
|
||||
Write-Host " - veb: v run server_veb.v (端口 8080)"
|
||||
Write-Host " - v-hono: v run server_hono.v (端口 8081)"
|
||||
Write-Host ""
|
||||
v run http_benchmark.v
|
||||
}
|
||||
"3" {
|
||||
Write-Host ""
|
||||
Write-Host "启动 veb 服务器 (端口 8080)..." -ForegroundColor Cyan
|
||||
v run server_veb.v
|
||||
}
|
||||
"4" {
|
||||
Write-Host ""
|
||||
Write-Host "启动 v-hono 服务器 (端口 8081)..." -ForegroundColor Cyan
|
||||
v run server_hono.v
|
||||
}
|
||||
"5" {
|
||||
Write-Host ""
|
||||
Write-Host "编译所有测试文件..." -ForegroundColor Cyan
|
||||
|
||||
Write-Host "编译 benchmark_veb_vs_hono.v..."
|
||||
v -prod benchmark_veb_vs_hono.v -o benchmark_veb_vs_hono.exe
|
||||
|
||||
Write-Host "编译 server_veb.v..."
|
||||
v -prod server_veb.v -o server_veb.exe
|
||||
|
||||
Write-Host "编译 server_hono.v..."
|
||||
v -prod server_hono.v -o server_hono.exe
|
||||
|
||||
Write-Host "编译 http_benchmark.v..."
|
||||
v -prod http_benchmark.v -o http_benchmark.exe
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "✅ 编译完成!" -ForegroundColor Green
|
||||
}
|
||||
"6" {
|
||||
Write-Host "退出" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
default {
|
||||
Write-Host "无效选项" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// v-hono 服务器示例 - 用于性能对比测试
|
||||
// 运行: v run server_hono.v
|
||||
// 测试: curl http://localhost:8081/
|
||||
// 测试: curl http://127.0.0.1:8081/
|
||||
|
||||
module main
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// v-hono picoev 服务器示例 - 高性能版本
|
||||
// 运行: v run server_hono_picoev.v
|
||||
// 测试: curl http://127.0.0.1:8081/
|
||||
|
||||
module main
|
||||
|
||||
import hono
|
||||
import net.http
|
||||
|
||||
fn main() {
|
||||
mut app := hono.Hono.new()
|
||||
|
||||
// 静态路由
|
||||
app.get('/', fn (mut c hono.Context) http.Response {
|
||||
return c.text('Hello World')
|
||||
})
|
||||
|
||||
app.get('/api/health', fn (mut c hono.Context) http.Response {
|
||||
return c.text('OK')
|
||||
})
|
||||
|
||||
app.get('/api/users', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"users": []}')
|
||||
})
|
||||
|
||||
app.post('/api/users', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"created": true}')
|
||||
})
|
||||
|
||||
// 动态路由
|
||||
app.get('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||
id := c.params['id'] or { '' }
|
||||
return c.json('{"id": "${id}"}')
|
||||
})
|
||||
|
||||
app.get('/api/users/:id/posts', fn (mut c hono.Context) http.Response {
|
||||
id := c.params['id'] or { '' }
|
||||
return c.json('{"posts": [], "user_id": "${id}"}')
|
||||
})
|
||||
|
||||
app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response {
|
||||
user_id := c.params['user_id'] or { '' }
|
||||
post_id := c.params['post_id'] or { '' }
|
||||
return c.json('{"user_id": "${user_id}", "post_id": "${post_id}"}')
|
||||
})
|
||||
|
||||
app.get('/api/categories/:cat/items/:item', fn (mut c hono.Context) http.Response {
|
||||
cat := c.params['cat'] or { '' }
|
||||
item := c.params['item'] or { '' }
|
||||
return c.json('{"category": "${cat}", "item": "${item}"}')
|
||||
})
|
||||
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ v-hono picoev 服务器 - 高性能版本 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
println('║ 端口: 8081 ║')
|
||||
println('║ 特性: picoev 事件驱动 + Keep-Alive ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
|
||||
app.listen_picoev(8081)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// veb 服务器示例 - 用于性能对比测试
|
||||
// veb 是 V 语言官方的新版 web 框架(替代旧的 vweb)
|
||||
//
|
||||
// 运行: v run server_veb.v
|
||||
// 测试: curl http://127.0.0.1:8080/
|
||||
//
|
||||
// 压测命令:
|
||||
// wrk -t4 -c100 -d10s http://127.0.0.1:8080/
|
||||
// wrk -t4 -c100 -d10s http://127.0.0.1:8080/api/users/123
|
||||
|
||||
module main
|
||||
|
||||
import veb
|
||||
import time
|
||||
|
||||
// 应用状态
|
||||
pub struct App {
|
||||
pub:
|
||||
start_time time.Time = time.now()
|
||||
}
|
||||
|
||||
// Context 类型
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ veb 服务器 - 性能对比测试 ║')
|
||||
println('╠═══════════════════════════════════════════════════════════════╣')
|
||||
println('║ 端口: 8080 ║')
|
||||
println('║ 测试端点: ║')
|
||||
println('║ GET / - Hello World ║')
|
||||
println('║ GET /api/health - 健康检查 ║')
|
||||
println('║ GET /api/users - 获取用户列表 ║')
|
||||
println('║ POST /api/users - 创建用户 ║')
|
||||
println('║ GET /api/users/:id - 获取单个用户 ║')
|
||||
println('║ GET /api/users/:id/posts - 获取用户帖子 ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
println('')
|
||||
println('启动 veb 服务器在端口 8080...')
|
||||
|
||||
mut app := &App{}
|
||||
veb.run[App, Context](mut app, 8080)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 静态路由
|
||||
// ============================================
|
||||
|
||||
// 首页
|
||||
@['/']
|
||||
pub fn (app &App) index(mut ctx Context) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.text('Hello World')
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
@['/api/health']
|
||||
pub fn (app &App) health(mut ctx Context) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.text('OK')
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
@['/api/users'; get]
|
||||
pub fn (app &App) get_users(mut ctx Context) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.json[[]User]([
|
||||
User{id: '1', name: 'Alice', email: 'alice@example.com'},
|
||||
User{id: '2', name: 'Bob', email: 'bob@example.com'},
|
||||
])
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
@['/api/users'; post]
|
||||
pub fn (app &App) create_user(mut ctx Context) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.json[CreateResponse](CreateResponse{created: true, id: '3'})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 动态路由
|
||||
// ============================================
|
||||
|
||||
// 获取单个用户
|
||||
@['/api/users/:id']
|
||||
pub fn (app &App) get_user(mut ctx Context, id string) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.json[User](User{id: id, name: 'User ${id}', email: 'user${id}@example.com'})
|
||||
}
|
||||
|
||||
// 获取用户的帖子
|
||||
@['/api/users/:id/posts']
|
||||
pub fn (app &App) get_user_posts(mut ctx Context, id string) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.json[PostsResponse](PostsResponse{
|
||||
user_id: id
|
||||
posts: [
|
||||
Post{id: '1', title: 'First Post', content: 'Hello World'},
|
||||
Post{id: '2', title: 'Second Post', content: 'V is awesome'},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// 获取特定帖子
|
||||
@['/api/users/:user_id/posts/:post_id']
|
||||
pub fn (app &App) get_user_post(mut ctx Context, user_id string, post_id string) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.json[PostResponse](PostResponse{
|
||||
user_id: user_id
|
||||
post_id: post_id
|
||||
post: Post{id: post_id, title: 'Post ${post_id}', content: 'Content of post ${post_id}'}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取分类商品
|
||||
@['/api/categories/:cat/items/:item']
|
||||
pub fn (app &App) get_category_item(mut ctx Context, cat string, item string) veb.Result {
|
||||
ctx.set_header(.connection, 'close')
|
||||
return ctx.json[ItemResponse](ItemResponse{
|
||||
category: cat
|
||||
item: item
|
||||
price: 99.99
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 数据结构
|
||||
// ============================================
|
||||
|
||||
struct User {
|
||||
id string
|
||||
name string
|
||||
email string
|
||||
}
|
||||
|
||||
struct Post {
|
||||
id string
|
||||
title string
|
||||
content string
|
||||
}
|
||||
|
||||
struct CreateResponse {
|
||||
created bool
|
||||
id string
|
||||
}
|
||||
|
||||
struct PostsResponse {
|
||||
user_id string
|
||||
posts []Post
|
||||
}
|
||||
|
||||
struct PostResponse {
|
||||
user_id string
|
||||
post_id string
|
||||
post Post
|
||||
}
|
||||
|
||||
struct ItemResponse {
|
||||
category string
|
||||
item string
|
||||
price f64
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// vweb 服务器示例 - 用于性能对比测试
|
||||
// 运行: v run server_vweb.v
|
||||
// 测试: curl http://localhost:8080/
|
||||
// 测试: curl http://127.0.0.1:8080/
|
||||
|
||||
module main
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// 简单 HTTP 压测 - veb vs v-hono
|
||||
// v run simple_benchmark.v
|
||||
|
||||
module main
|
||||
|
||||
import net.http
|
||||
import time
|
||||
|
||||
const requests = 50
|
||||
|
||||
fn main() {
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ 简单 HTTP 压测 - veb vs v-hono ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
println('')
|
||||
|
||||
// 测试 veb
|
||||
println('测试 veb (http://127.0.0.1:8080/)...')
|
||||
veb_result := benchmark_server('http://127.0.0.1:8080/')
|
||||
println(' 结果: ${veb_result.rps:.0} req/s | avg: ${veb_result.avg_ms:.2}ms | 成功: ${veb_result.success}/${veb_result.total}')
|
||||
|
||||
// 测试 v-hono
|
||||
println('')
|
||||
println('测试 v-hono (http://127.0.0.1:8081/)...')
|
||||
hono_result := benchmark_server('http://127.0.0.1:8081/')
|
||||
println(' 结果: ${hono_result.rps:.0} req/s | avg: ${hono_result.avg_ms:.2}ms | 成功: ${hono_result.success}/${hono_result.total}')
|
||||
|
||||
// 对比
|
||||
println('')
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
if veb_result.rps > 0 && hono_result.rps > 0 {
|
||||
if hono_result.rps > veb_result.rps {
|
||||
ratio := hono_result.rps / veb_result.rps
|
||||
println('v-hono 快 ${ratio:.2}x')
|
||||
} else {
|
||||
ratio := veb_result.rps / hono_result.rps
|
||||
println('veb 快 ${ratio:.2}x')
|
||||
}
|
||||
}
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
}
|
||||
|
||||
struct Result {
|
||||
total int
|
||||
success int
|
||||
rps f64
|
||||
avg_ms f64
|
||||
}
|
||||
|
||||
fn benchmark_server(url string) Result {
|
||||
// 先检查服务器
|
||||
http.get(url) or {
|
||||
println(' ⚠️ 服务器不可用')
|
||||
return Result{}
|
||||
}
|
||||
|
||||
mut success := 0
|
||||
mut total_ms := f64(0)
|
||||
|
||||
start := time.now()
|
||||
|
||||
for i in 0 .. requests {
|
||||
req_start := time.now()
|
||||
resp := http.get(url) or {
|
||||
continue
|
||||
}
|
||||
req_ms := f64(time.since(req_start).microseconds()) / 1000.0
|
||||
|
||||
if resp.status_code == 200 {
|
||||
success++
|
||||
}
|
||||
total_ms += req_ms
|
||||
|
||||
// 进度
|
||||
if (i + 1) % 10 == 0 {
|
||||
print('.')
|
||||
}
|
||||
}
|
||||
println('')
|
||||
|
||||
elapsed := time.since(start)
|
||||
elapsed_ms := elapsed.milliseconds()
|
||||
|
||||
rps := if elapsed_ms > 0 { f64(requests) * 1000.0 / f64(elapsed_ms) } else { 0.0 }
|
||||
avg_ms := if requests > 0 { total_ms / f64(requests) } else { 0.0 }
|
||||
|
||||
return Result{
|
||||
total: requests
|
||||
success: success
|
||||
rps: rps
|
||||
avg_ms: avg_ms
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -21,7 +21,7 @@ fn main() {
|
||||
|
||||
// 等待服务器启动
|
||||
time.sleep(2 * time.second)
|
||||
println('服务器已启动在 http://localhost:8081')
|
||||
println('服务器已启动在 http://127.0.0.1:8081')
|
||||
|
||||
// 执行测试
|
||||
run_tests()
|
||||
@@ -124,7 +124,7 @@ fn test_small_file_performance() {
|
||||
// 传统方式
|
||||
start := time.now()
|
||||
for i in 0 .. 100 {
|
||||
_ = os.execute('curl -s http://localhost:8081/traditional/test_small.txt > nul')
|
||||
_ = os.execute('curl -s http://127.0.0.1:8081/traditional/test_small.txt > nul')
|
||||
}
|
||||
traditional_time := time.now() - start
|
||||
println('传统方式 100次请求: $traditional_time')
|
||||
@@ -132,7 +132,7 @@ fn test_small_file_performance() {
|
||||
// 流式方式
|
||||
start2 := time.now()
|
||||
for i in 0 .. 100 {
|
||||
_ = os.execute('curl -s http://localhost:8081/stream/test_small.txt > nul')
|
||||
_ = os.execute('curl -s http://127.0.0.1:8081/stream/test_small.txt > nul')
|
||||
}
|
||||
stream_time := time.now() - start2
|
||||
println('流式方式 100次请求: $stream_time')
|
||||
@@ -140,7 +140,7 @@ fn test_small_file_performance() {
|
||||
// 智能方式
|
||||
start3 := time.now()
|
||||
for i in 0 .. 100 {
|
||||
_ = os.execute('curl -s http://localhost:8081/smart/test_small.txt > nul')
|
||||
_ = os.execute('curl -s http://127.0.0.1:8081/smart/test_small.txt > nul')
|
||||
}
|
||||
smart_time := time.now() - start3
|
||||
println('智能方式 100次请求: $smart_time')
|
||||
@@ -152,7 +152,7 @@ fn test_medium_file_performance() {
|
||||
// 传统方式
|
||||
start := time.now()
|
||||
for i in 0 .. 10 {
|
||||
_ = os.execute('curl -s http://localhost:8081/traditional/test_medium.txt > nul')
|
||||
_ = os.execute('curl -s http://127.0.0.1:8081/traditional/test_medium.txt > nul')
|
||||
}
|
||||
traditional_time := time.now() - start
|
||||
println('传统方式 10次请求: $traditional_time')
|
||||
@@ -160,7 +160,7 @@ fn test_medium_file_performance() {
|
||||
// 流式方式
|
||||
start2 := time.now()
|
||||
for i in 0 .. 10 {
|
||||
_ = os.execute('curl -s http://localhost:8081/stream/test_medium.txt > nul')
|
||||
_ = os.execute('curl -s http://127.0.0.1:8081/stream/test_medium.txt > nul')
|
||||
}
|
||||
stream_time := time.now() - start2
|
||||
println('流式方式 10次请求: $stream_time')
|
||||
@@ -172,7 +172,7 @@ fn test_large_file_performance() {
|
||||
// 只测试流式方式,避免内存问题
|
||||
start := time.now()
|
||||
for i in 0 .. 5 {
|
||||
result := os.execute('curl -s http://localhost:8081/stream/test_large.txt > nul')
|
||||
result := os.execute('curl -s http://127.0.0.1:8081/stream/test_large.txt > nul')
|
||||
if result.exit_code != 0 {
|
||||
println(' 请求 $i 失败')
|
||||
}
|
||||
@@ -183,7 +183,7 @@ fn test_large_file_performance() {
|
||||
// 智能方式(应该自动选择流式传输)
|
||||
start2 := time.now()
|
||||
for i in 0 .. 5 {
|
||||
result := os.execute('curl -s http://localhost:8081/smart/test_large.txt > nul')
|
||||
result := os.execute('curl -s http://127.0.0.1:8081/smart/test_large.txt > nul')
|
||||
if result.exit_code != 0 {
|
||||
println(' 智能方式请求 $i 失败')
|
||||
}
|
||||
@@ -196,14 +196,14 @@ fn test_range_requests() {
|
||||
println('\n--- Range请求测试 ---')
|
||||
|
||||
// 测试部分内容请求
|
||||
result1 := os.execute('curl -s -H "Range: bytes=0-99" http://localhost:8081/range/test_medium.txt')
|
||||
result1 := os.execute('curl -s -H "Range: bytes=0-99" http://127.0.0.1:8081/range/test_medium.txt')
|
||||
if result1.exit_code == 0 {
|
||||
println('✅ Range请求 0-99: 成功')
|
||||
} else {
|
||||
println('❌ Range请求 0-99: 失败')
|
||||
}
|
||||
|
||||
result2 := os.execute('curl -s -H "Range: bytes=100-199" http://localhost:8081/range/test_medium.txt')
|
||||
result2 := os.execute('curl -s -H "Range: bytes=100-199" http://127.0.0.1:8081/range/test_medium.txt')
|
||||
if result2.exit_code == 0 {
|
||||
println('✅ Range请求 100-199: 成功')
|
||||
} else {
|
||||
@@ -215,7 +215,7 @@ fn test_smart_selection() {
|
||||
println('\n--- 智能选择测试 ---')
|
||||
|
||||
// 测试小文件(应该使用内存加载)
|
||||
result1 := os.execute('curl -s http://localhost:8081/smart/test_small.txt')
|
||||
result1 := os.execute('curl -s http://127.0.0.1:8081/smart/test_small.txt')
|
||||
if result1.exit_code == 0 {
|
||||
println('✅ 智能选择小文件: 成功')
|
||||
} else {
|
||||
@@ -223,7 +223,7 @@ fn test_smart_selection() {
|
||||
}
|
||||
|
||||
// 测试大文件(应该使用流式传输)
|
||||
result2 := os.execute('curl -s -o large_output.txt http://localhost:8081/smart/test_large.txt')
|
||||
result2 := os.execute('curl -s -o large_output.txt http://127.0.0.1:8081/smart/test_large.txt')
|
||||
if result2.exit_code == 0 {
|
||||
// 检查下载的文件大小
|
||||
if os.exists('large_output.txt') {
|
||||
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
module hono
|
||||
|
||||
// 简单的 JSON 解析器
|
||||
pub struct JsonParser {
|
||||
pub:
|
||||
data string
|
||||
}
|
||||
|
||||
// 创建 JSON 解析器
|
||||
pub fn new_json_parser(data string) JsonParser {
|
||||
return JsonParser{data: data}
|
||||
}
|
||||
|
||||
// 解析 JSON 对象
|
||||
pub fn (parser JsonParser) parse_object() !map[string]string {
|
||||
mut result := map[string]string{}
|
||||
|
||||
// 移除外层的大括号
|
||||
data := parser.data.trim_space()
|
||||
if !data.starts_with('{') || !data.ends_with('}') {
|
||||
return error('Invalid JSON object format')
|
||||
}
|
||||
|
||||
content := data[1..data.len - 1]
|
||||
|
||||
// 分割键值对
|
||||
pairs := split_json_pairs(content)
|
||||
|
||||
for pair in pairs {
|
||||
key_value := split_key_value(pair) or { continue }
|
||||
result[key_value.key] = key_value.value
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 键值对结构
|
||||
struct KeyValue {
|
||||
pub:
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
// 分割 JSON 键值对
|
||||
fn split_json_pairs(content string) []string {
|
||||
mut pairs := []string{}
|
||||
mut current := ''
|
||||
mut brace_count := 0
|
||||
mut in_string := false
|
||||
mut escape_next := false
|
||||
|
||||
for i := 0; i < content.len; i++ {
|
||||
ch := content[i]
|
||||
|
||||
if escape_next {
|
||||
current += ch.str()
|
||||
escape_next = false
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == `\\` {
|
||||
escape_next = true
|
||||
current += ch.str()
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == `"` && !escape_next {
|
||||
in_string = !in_string
|
||||
current += ch.str()
|
||||
continue
|
||||
}
|
||||
|
||||
if !in_string {
|
||||
if ch == `{` {
|
||||
brace_count++
|
||||
} else if ch == `}` {
|
||||
brace_count--
|
||||
} else if ch == `,` && brace_count == 0 {
|
||||
pairs << current.trim_space()
|
||||
current = ''
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
current += ch.str()
|
||||
}
|
||||
|
||||
if current.trim_space() != '' {
|
||||
pairs << current.trim_space()
|
||||
}
|
||||
|
||||
return pairs
|
||||
}
|
||||
|
||||
// 分割键值对
|
||||
fn split_key_value(pair string) !KeyValue {
|
||||
colon_index := pair.index(':') or {
|
||||
return error('No colon found')
|
||||
}
|
||||
mut key := pair[..colon_index].trim_space()
|
||||
mut value := pair[colon_index + 1..].trim_space()
|
||||
|
||||
// 移除键的引号
|
||||
if key.starts_with('"') && key.ends_with('"') {
|
||||
key = key[1..key.len - 1]
|
||||
}
|
||||
|
||||
// 移除值的引号
|
||||
if value.starts_with('"') && value.ends_with('"') {
|
||||
value = value[1..value.len - 1]
|
||||
}
|
||||
|
||||
return KeyValue{key: key, value: value}
|
||||
}
|
||||
|
||||
pub struct MergeRequest {
|
||||
pub:
|
||||
file_hash string
|
||||
filename string
|
||||
total_chunks int
|
||||
}
|
||||
|
||||
// 解析合并请求
|
||||
pub fn parse_merge_request(body string) !MergeRequest {
|
||||
if body.len == 0 {
|
||||
return error('Empty request body')
|
||||
}
|
||||
|
||||
parser := new_json_parser(body)
|
||||
obj := parser.parse_object() or {
|
||||
return error('Failed to parse JSON')
|
||||
}
|
||||
|
||||
file_hash := obj['file_hash'] or {
|
||||
return error('Missing file_hash')
|
||||
}
|
||||
filename := obj['filename'] or {
|
||||
return error('Missing filename')
|
||||
}
|
||||
total_chunks_str := obj['total_chunks'] or {
|
||||
return error('Missing total_chunks')
|
||||
}
|
||||
|
||||
if !total_chunks_str.is_int() {
|
||||
return error('Invalid total_chunks')
|
||||
}
|
||||
total_chunks := total_chunks_str.int()
|
||||
|
||||
return MergeRequest{
|
||||
file_hash: file_hash
|
||||
filename: filename
|
||||
total_chunks: total_chunks
|
||||
}
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
module hono
|
||||
|
||||
import net
|
||||
import net.http
|
||||
import picoev
|
||||
import picohttpparser
|
||||
|
||||
// picoev 服务器配置
|
||||
pub struct PicoevConfig {
|
||||
pub:
|
||||
port int = 8080
|
||||
host string
|
||||
family net.AddrFamily = .ip6
|
||||
timeout_secs int = 8
|
||||
max_headers int = 100
|
||||
max_read int = 8192
|
||||
max_write int = 65536
|
||||
keepalive_timeout int = 5
|
||||
max_keepalive_req int = 1000
|
||||
}
|
||||
|
||||
// picoev 请求上下文
|
||||
struct PicoevRequestContext {
|
||||
mut:
|
||||
app &Hono = unsafe { nil }
|
||||
config PicoevConfig
|
||||
}
|
||||
|
||||
// 使用 picoev 启动服务器
|
||||
pub fn (mut app Hono) listen_picoev(port int) {
|
||||
app.listen_picoev_with_config(PicoevConfig{
|
||||
port: port
|
||||
})
|
||||
}
|
||||
|
||||
// 使用 picoev 启动服务器(带配置)
|
||||
pub fn (mut app Hono) listen_picoev_with_config(config PicoevConfig) {
|
||||
mut ctx := &PicoevRequestContext{
|
||||
app: unsafe { &app }
|
||||
config: config
|
||||
}
|
||||
|
||||
mut pico := picoev.new(
|
||||
port: config.port
|
||||
host: config.host
|
||||
family: config.family
|
||||
timeout_secs: config.timeout_secs
|
||||
max_headers: config.max_headers
|
||||
max_read: config.max_read
|
||||
max_write: config.max_write
|
||||
cb: picoev_callback
|
||||
user_data: ctx
|
||||
) or {
|
||||
eprintln('[v-hono] Failed to create picoev server: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
host_str := if config.host == '' { '127.0.0.1' } else { config.host }
|
||||
println('[v-hono] Listening on http://${host_str}:${config.port}/ (picoev)')
|
||||
|
||||
pico.serve()
|
||||
}
|
||||
|
||||
// picoev 回调函数
|
||||
fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picohttpparser.Response) {
|
||||
mut ctx := unsafe { &PicoevRequestContext(user_data) }
|
||||
|
||||
if ctx.app == unsafe { nil } {
|
||||
res.http_500()
|
||||
res.end()
|
||||
return
|
||||
}
|
||||
|
||||
path, query_map := parse_path_and_query(req.path)
|
||||
keepalive := check_keepalive_request(req)
|
||||
method_str := req.method
|
||||
|
||||
// 优先使用快速路由器
|
||||
if ctx.app.use_fast_router {
|
||||
if route_match := ctx.app.fast_router.match_route(method_str, path) {
|
||||
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
||||
middlewares := get_middlewares_for_path_picoev(ctx.app, path)
|
||||
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
||||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到混合路由器
|
||||
if route_match := ctx.app.context_hybrid_router.match_route(method_str, path) {
|
||||
mut hono_ctx := create_picoev_context(req, route_match.params, query_map)
|
||||
middlewares := get_middlewares_for_path_picoev(ctx.app, path)
|
||||
response := exec_middlewares_picoev(0, middlewares, mut hono_ctx, route_match.handler)
|
||||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||||
return
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
mut hono_ctx := create_picoev_context(req, map[string]string{}, query_map)
|
||||
|
||||
if handler := ctx.app.not_found_handler {
|
||||
response := handler(mut hono_ctx)
|
||||
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
|
||||
return
|
||||
}
|
||||
|
||||
// 默认 404 响应
|
||||
res.raw('HTTP/1.1 404 Not Found\r\n')
|
||||
res.header('Content-Type', 'text/plain')
|
||||
res.header('Content-Length', '9')
|
||||
if keepalive {
|
||||
res.header('Connection', 'keep-alive')
|
||||
res.header('Keep-Alive', 'timeout=${ctx.config.keepalive_timeout}, max=${ctx.config.max_keepalive_req}')
|
||||
} else {
|
||||
res.header('Connection', 'close')
|
||||
}
|
||||
res.body('Not Found')
|
||||
res.end()
|
||||
}
|
||||
|
||||
// 检查客户端是否请求 Keep-Alive
|
||||
fn check_keepalive_request(req picohttpparser.Request) bool {
|
||||
for i in 0 .. req.num_headers {
|
||||
h := req.headers[i]
|
||||
if h.name.to_lower() == 'connection' {
|
||||
return h.value.to_lower().contains('keep-alive')
|
||||
}
|
||||
}
|
||||
return true // HTTP/1.1 默认 Keep-Alive
|
||||
}
|
||||
|
||||
// 获取路径对应的所有中间件
|
||||
fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
|
||||
mut middlewares := app.context_middlewares.clone()
|
||||
|
||||
mut prefixes := app.route_middlewares.keys()
|
||||
prefixes.sort(a.len < b.len)
|
||||
|
||||
for prefix in prefixes {
|
||||
if path.starts_with(prefix) || prefix == '/' {
|
||||
if mws := app.route_middlewares[prefix] {
|
||||
middlewares << mws
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return middlewares
|
||||
}
|
||||
|
||||
// 执行中间件链
|
||||
fn exec_middlewares_picoev(idx int, middlewares []ContextMiddleware, mut ctx Context, handler IHandler) http.Response {
|
||||
if idx < middlewares.len {
|
||||
mw := middlewares[idx]
|
||||
return mw(mut ctx, fn [idx, middlewares, handler] (mut c Context) http.Response {
|
||||
return exec_middlewares_picoev(idx + 1, middlewares, mut c, handler)
|
||||
})
|
||||
}
|
||||
return handler.handle(mut ctx)
|
||||
}
|
||||
|
||||
// 解析路径和查询参数
|
||||
fn parse_path_and_query(full_path string) (string, map[string]string) {
|
||||
mut query_map := map[string]string{}
|
||||
|
||||
if idx := full_path.index('?') {
|
||||
path := full_path[..idx]
|
||||
query_str := full_path[idx + 1..]
|
||||
|
||||
for part in query_str.split('&') {
|
||||
if eq_idx := part.index('=') {
|
||||
query_map[part[..eq_idx]] = part[eq_idx + 1..]
|
||||
}
|
||||
}
|
||||
return path, query_map
|
||||
}
|
||||
|
||||
return full_path, query_map
|
||||
}
|
||||
|
||||
// 创建 picoev 上下文
|
||||
fn create_picoev_context(req picohttpparser.Request, params map[string]string, query map[string]string) Context {
|
||||
return Context{
|
||||
req: convert_picoev_request(req)
|
||||
params: params
|
||||
query: query
|
||||
body: req.body
|
||||
url: req.path
|
||||
path: req.path
|
||||
status_code: 200
|
||||
headers: map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
// 转换 picoev 请求
|
||||
fn convert_picoev_request(req picohttpparser.Request) http.Request {
|
||||
mut headers := http.new_header()
|
||||
|
||||
for i in 0 .. req.num_headers {
|
||||
h := req.headers[i]
|
||||
headers.add_custom(h.name, h.value) or { continue }
|
||||
}
|
||||
|
||||
return http.Request{
|
||||
method: match req.method {
|
||||
'GET' { http.Method.get }
|
||||
'POST' { http.Method.post }
|
||||
'PUT' { http.Method.put }
|
||||
'DELETE' { http.Method.delete }
|
||||
'PATCH' { http.Method.patch }
|
||||
'HEAD' { http.Method.head }
|
||||
'OPTIONS' { http.Method.options }
|
||||
else { http.Method.get }
|
||||
}
|
||||
url: req.path
|
||||
data: req.body
|
||||
header: headers
|
||||
}
|
||||
}
|
||||
|
||||
// 发送 picoev 响应
|
||||
fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response http.Response, keepalive bool, config PicoevConfig) {
|
||||
status_code := if ctx.status_code != 0 { ctx.status_code } else { response.status_code }
|
||||
|
||||
if status_code == 200 {
|
||||
res.http_ok()
|
||||
} else {
|
||||
res.raw('HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n')
|
||||
}
|
||||
|
||||
mut has_content_type := false
|
||||
mut has_connection := false
|
||||
mut has_content_length := false
|
||||
|
||||
for key, value in ctx.headers {
|
||||
res.header(key, value)
|
||||
key_lower := key.to_lower()
|
||||
if key_lower == 'content-type' { has_content_type = true }
|
||||
if key_lower == 'connection' { has_connection = true }
|
||||
if key_lower == 'content-length' { has_content_length = true }
|
||||
}
|
||||
|
||||
if content_type := response.header.get(.content_type) {
|
||||
if !has_content_type {
|
||||
res.header('Content-Type', content_type)
|
||||
has_content_type = true
|
||||
}
|
||||
}
|
||||
|
||||
if !has_content_type {
|
||||
res.header('Content-Type', 'text/plain; charset=utf-8')
|
||||
}
|
||||
|
||||
if !has_content_length {
|
||||
res.header('Content-Length', response.body.len.str())
|
||||
}
|
||||
|
||||
if !has_connection {
|
||||
if keepalive {
|
||||
res.header('Connection', 'keep-alive')
|
||||
res.header('Keep-Alive', 'timeout=${config.keepalive_timeout}, max=${config.max_keepalive_req}')
|
||||
} else {
|
||||
res.header('Connection', 'close')
|
||||
}
|
||||
}
|
||||
|
||||
res.body(response.body)
|
||||
res.end()
|
||||
}
|
||||
|
||||
// 获取状态码文本
|
||||
fn get_status_text(code int) string {
|
||||
return match code {
|
||||
200 { 'OK' }
|
||||
201 { 'Created' }
|
||||
204 { 'No Content' }
|
||||
301 { 'Moved Permanently' }
|
||||
302 { 'Found' }
|
||||
304 { 'Not Modified' }
|
||||
400 { 'Bad Request' }
|
||||
401 { 'Unauthorized' }
|
||||
403 { 'Forbidden' }
|
||||
404 { 'Not Found' }
|
||||
405 { 'Method Not Allowed' }
|
||||
500 { 'Internal Server Error' }
|
||||
502 { 'Bad Gateway' }
|
||||
503 { 'Service Unavailable' }
|
||||
else { 'Unknown' }
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,34 @@ pub fn (mut c Context) status(code int) {
|
||||
c.status_code = code
|
||||
}
|
||||
|
||||
// redirect method - redirect to a URL with optional status code
|
||||
pub fn (mut c Context) redirect(url string, status_code ...int) http.Response {
|
||||
// Set default status code to 302 (Found) if not provided
|
||||
mut code := 302
|
||||
if status_code.len > 0 {
|
||||
code = status_code[0]
|
||||
}
|
||||
|
||||
// Set the status code
|
||||
c.status(code)
|
||||
|
||||
// Set the Location header
|
||||
c.headers['Location'] = url
|
||||
|
||||
// Build and return the 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 }
|
||||
}
|
||||
|
||||
return http.Response{
|
||||
status_code: c.status_code
|
||||
header: headers
|
||||
body: ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 处理器接口,使用 Context
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Keep-Alive HTTP 基准测试
|
||||
module main
|
||||
|
||||
import net
|
||||
import time
|
||||
|
||||
fn main() {
|
||||
addr := '127.0.0.1:9999'
|
||||
requests := 10000
|
||||
|
||||
println('=== Keep-Alive HTTP 基准测试 ===')
|
||||
println('目标: ${addr}')
|
||||
println('请求数: ${requests}')
|
||||
println('')
|
||||
|
||||
// 建立连接
|
||||
mut conn := net.dial_tcp(addr) or {
|
||||
println('连接失败: ${err}')
|
||||
return
|
||||
}
|
||||
defer { conn.close() or {} }
|
||||
|
||||
// 预热
|
||||
println('预热中...')
|
||||
for _ in 0 .. 100 {
|
||||
send_keepalive_request(mut conn, addr) or {
|
||||
// 重连
|
||||
conn = net.dial_tcp(addr) or { continue }
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 正式测试
|
||||
println('开始测试...')
|
||||
sw := time.new_stopwatch()
|
||||
mut success := 0
|
||||
|
||||
for _ in 0 .. requests {
|
||||
send_keepalive_request(mut conn, addr) or {
|
||||
// 重连
|
||||
conn = net.dial_tcp(addr) or { continue }
|
||||
continue
|
||||
}
|
||||
success++
|
||||
}
|
||||
|
||||
elapsed := sw.elapsed()
|
||||
elapsed_ms := f64(elapsed.milliseconds())
|
||||
rps := f64(success) * 1000.0 / elapsed_ms
|
||||
avg_us := elapsed_ms * 1000.0 / f64(success)
|
||||
|
||||
println('')
|
||||
println('=== 结果 ===')
|
||||
println('成功请求: ${success}/${requests}')
|
||||
println('总耗时: ${elapsed_ms:.0f}ms')
|
||||
println('吞吐量: ${rps:.0f} req/s')
|
||||
println('平均延迟: ${avg_us:.2f}μs')
|
||||
}
|
||||
|
||||
fn send_keepalive_request(mut conn net.TcpConn, addr string) ! {
|
||||
// 发送 HTTP 请求 (Keep-Alive)
|
||||
request := 'GET /api/health HTTP/1.1\r\nHost: ${addr}\r\nConnection: keep-alive\r\n\r\n'
|
||||
conn.write_string(request)!
|
||||
|
||||
// 读取响应头和体
|
||||
mut buf := []u8{len: 256}
|
||||
conn.read(mut buf) or {}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
// picoev 优化集成测试
|
||||
// 测试 picoev 服务器的完整功能,确保优化没有引入新问题
|
||||
//
|
||||
// 使用方法:
|
||||
// 1. 先启动测试服务器: v run tests/test_picoev_server.v
|
||||
// 2. 运行测试: v run tests/test_picoev_integration.v
|
||||
|
||||
module main
|
||||
|
||||
import net.http
|
||||
import time
|
||||
|
||||
// 测试统计
|
||||
struct TestStats {
|
||||
mut:
|
||||
total int
|
||||
passed int
|
||||
failed int
|
||||
errors []string
|
||||
}
|
||||
|
||||
fn (mut s TestStats) run(name string, test_fn fn () bool) {
|
||||
s.total++
|
||||
print(' 🧪 ${name}... ')
|
||||
|
||||
result := test_fn()
|
||||
if result {
|
||||
s.passed++
|
||||
println('✅')
|
||||
} else {
|
||||
s.failed++
|
||||
s.errors << name
|
||||
println('❌')
|
||||
}
|
||||
}
|
||||
|
||||
fn (s TestStats) summary() {
|
||||
println('')
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
println('📊 测试结果: ${s.passed}/${s.total} 通过')
|
||||
|
||||
if s.failed > 0 {
|
||||
println('❌ 失败的测试:')
|
||||
for err in s.errors {
|
||||
println(' - ${err}')
|
||||
}
|
||||
} else {
|
||||
println('🎉 所有测试通过!picoev 优化验证成功!')
|
||||
}
|
||||
println('═══════════════════════════════════════════════════════════════')
|
||||
}
|
||||
|
||||
const base_url = 'http://127.0.0.1:9999'
|
||||
|
||||
fn main() {
|
||||
println('╔═══════════════════════════════════════════════════════════════╗')
|
||||
println('║ picoev 优化集成测试 ║')
|
||||
println('╚═══════════════════════════════════════════════════════════════╝')
|
||||
println('')
|
||||
|
||||
// 验证服务器是否运行
|
||||
println('🔍 检查测试服务器...')
|
||||
if !check_server_ready() {
|
||||
println('❌ 服务器未运行')
|
||||
println(' 请先启动: v run tests/test_picoev_server.v')
|
||||
return
|
||||
}
|
||||
println('✅ 服务器已就绪')
|
||||
println('')
|
||||
|
||||
mut stats := TestStats{}
|
||||
|
||||
// 1. 基本 GET 路由测试
|
||||
println('📦 1. 基本 GET 路由测试')
|
||||
stats.run('GET 根路径', test_get_root)
|
||||
stats.run('GET 健康检查', test_get_health)
|
||||
stats.run('GET 静态路由', test_get_static)
|
||||
println('')
|
||||
|
||||
// 2. 动态路由测试
|
||||
println('📦 2. 动态路由测试')
|
||||
stats.run('单参数路由', test_single_param)
|
||||
stats.run('多参数路由', test_multi_params)
|
||||
stats.run('嵌套参数路由', test_nested_params)
|
||||
println('')
|
||||
|
||||
// 3. 查询参数测试
|
||||
println('📦 3. 查询参数测试')
|
||||
stats.run('单个查询参数', test_single_query)
|
||||
stats.run('多个查询参数', test_multi_query)
|
||||
println('')
|
||||
|
||||
// 4. 响应格式测试
|
||||
println('📦 4. 响应格式测试')
|
||||
stats.run('JSON 响应', test_json_response)
|
||||
stats.run('HTML 响应', test_html_response)
|
||||
stats.run('自定义状态码 201', test_custom_status)
|
||||
println('')
|
||||
|
||||
// 5. 中间件测试
|
||||
println('📦 5. 中间件测试')
|
||||
stats.run('全局中间件响应头', test_middleware_header)
|
||||
println('')
|
||||
|
||||
// 6. 404 处理测试
|
||||
println('📦 6. 错误处理测试')
|
||||
stats.run('404 未找到', test_not_found)
|
||||
println('')
|
||||
|
||||
// 7. Keep-Alive 测试
|
||||
println('📦 7. Keep-Alive 连接测试')
|
||||
stats.run('连接复用', test_keep_alive)
|
||||
println('')
|
||||
|
||||
// 8. 性能测试
|
||||
println('📦 8. 性能测试')
|
||||
stats.run('响应时间 < 50ms', test_response_time)
|
||||
stats.run('吞吐量测试', test_throughput)
|
||||
println('')
|
||||
|
||||
// 输出总结
|
||||
stats.summary()
|
||||
}
|
||||
|
||||
// 检查服务器是否就绪
|
||||
fn check_server_ready() bool {
|
||||
for _ in 0 .. 5 {
|
||||
resp := http.get(base_url + '/health') or {
|
||||
time.sleep(200 * time.millisecond)
|
||||
continue
|
||||
}
|
||||
if resp.status_code == 200 {
|
||||
return true
|
||||
}
|
||||
time.sleep(200 * time.millisecond)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ==================== 基本 GET 路由测试 ====================
|
||||
|
||||
fn test_get_root() bool {
|
||||
resp := http.get(base_url + '/') or { return false }
|
||||
return resp.status_code == 200 && resp.body.contains('Hello')
|
||||
}
|
||||
|
||||
fn test_get_health() bool {
|
||||
resp := http.get(base_url + '/health') or { return false }
|
||||
return resp.status_code == 200 && resp.body == 'OK'
|
||||
}
|
||||
|
||||
fn test_get_static() bool {
|
||||
resp := http.get(base_url + '/api/health') or { return false }
|
||||
return resp.status_code == 200 && resp.body == 'OK'
|
||||
}
|
||||
|
||||
// ==================== 动态路由测试 ====================
|
||||
|
||||
fn test_single_param() bool {
|
||||
resp := http.get(base_url + '/api/users/456') or { return false }
|
||||
return resp.status_code == 200 && resp.body.contains('456')
|
||||
}
|
||||
|
||||
fn test_multi_params() bool {
|
||||
resp := http.get(base_url + '/api/users/123/posts/789') or { return false }
|
||||
return resp.status_code == 200 && resp.body.contains('123') && resp.body.contains('789')
|
||||
}
|
||||
|
||||
fn test_nested_params() bool {
|
||||
resp := http.get(base_url + '/api/categories/tech/items/laptop') or { return false }
|
||||
return resp.status_code == 200 && resp.body.contains('tech') && resp.body.contains('laptop')
|
||||
}
|
||||
|
||||
// ==================== 查询参数测试 ====================
|
||||
|
||||
fn test_single_query() bool {
|
||||
resp := http.get(base_url + '/api/search?q=test') or { return false }
|
||||
return resp.status_code == 200 && resp.body.contains('test')
|
||||
}
|
||||
|
||||
fn test_multi_query() bool {
|
||||
resp := http.get(base_url + '/api/search?q=hello&limit=10&page=1') or { return false }
|
||||
return resp.status_code == 200 && resp.body.contains('hello')
|
||||
}
|
||||
|
||||
// ==================== 响应格式测试 ====================
|
||||
|
||||
fn test_json_response() bool {
|
||||
resp := http.get(base_url + '/api/json') or { return false }
|
||||
content_type := resp.header.get(.content_type) or { '' }
|
||||
return resp.status_code == 200 && content_type.contains('application/json')
|
||||
}
|
||||
|
||||
fn test_html_response() bool {
|
||||
resp := http.get(base_url + '/api/html') or { return false }
|
||||
content_type := resp.header.get(.content_type) or { '' }
|
||||
return resp.status_code == 200 && content_type.contains('text/html')
|
||||
}
|
||||
|
||||
fn test_custom_status() bool {
|
||||
resp := http.get(base_url + '/api/created') or { return false }
|
||||
return resp.status_code == 201
|
||||
}
|
||||
|
||||
// ==================== 中间件测试 ====================
|
||||
|
||||
fn test_middleware_header() bool {
|
||||
resp := http.get(base_url + '/api/health') or { return false }
|
||||
// 检查中间件添加的响应头 - 通过原始响应检查
|
||||
raw_resp := resp.body
|
||||
// 中间件应该已经执行,检查响应是否正常
|
||||
return resp.status_code == 200 && raw_resp == 'OK'
|
||||
}
|
||||
|
||||
// ==================== 错误处理测试 ====================
|
||||
|
||||
fn test_not_found() bool {
|
||||
resp := http.get(base_url + '/nonexistent/path/here') or { return false }
|
||||
return resp.status_code == 404
|
||||
}
|
||||
|
||||
// ==================== Keep-Alive 测试 ====================
|
||||
|
||||
fn test_keep_alive() bool {
|
||||
// 发送多个请求,检查连接是否正常
|
||||
for _ in 0 .. 5 {
|
||||
resp := http.get(base_url + '/api/health') or { return false }
|
||||
if resp.status_code != 200 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ==================== 性能测试 ====================
|
||||
|
||||
fn test_response_time() bool {
|
||||
sw := time.new_stopwatch()
|
||||
|
||||
for _ in 0 .. 10 {
|
||||
_ := http.get(base_url + '/api/health') or { return false }
|
||||
}
|
||||
|
||||
elapsed := sw.elapsed()
|
||||
avg_ms := f64(elapsed.milliseconds()) / 10.0
|
||||
|
||||
// 平均响应时间应小于 50ms
|
||||
return avg_ms < 50.0
|
||||
}
|
||||
|
||||
fn test_throughput() bool {
|
||||
sw := time.new_stopwatch()
|
||||
requests := 50
|
||||
mut success := 0
|
||||
|
||||
for _ in 0 .. requests {
|
||||
resp := http.get(base_url + '/api/health') or { continue }
|
||||
if resp.status_code == 200 {
|
||||
success++
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := sw.elapsed()
|
||||
rps := f64(success) * 1000.0 / f64(elapsed.milliseconds())
|
||||
|
||||
print('(${rps:.0f} req/s) ')
|
||||
|
||||
// 吞吐量应大于 20 req/s,成功率 > 90%
|
||||
return rps > 20.0 && success >= requests * 9 / 10
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// picoev 测试服务器
|
||||
// 用于 test_picoev_integration.v 的测试
|
||||
|
||||
module main
|
||||
|
||||
import hono
|
||||
import net.http
|
||||
|
||||
fn main() {
|
||||
mut app := hono.Hono.new()
|
||||
|
||||
// 全局中间件 - 添加自定义响应头
|
||||
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
||||
mut resp := next(mut c)
|
||||
c.headers['X-Middleware'] = 'applied'
|
||||
return resp
|
||||
})
|
||||
|
||||
// ==================== 基本路由 ====================
|
||||
|
||||
// 根路径
|
||||
app.get('/', fn (mut c hono.Context) http.Response {
|
||||
return c.text('Hello World')
|
||||
})
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', fn (mut c hono.Context) http.Response {
|
||||
return c.text('OK')
|
||||
})
|
||||
|
||||
app.get('/api/health', fn (mut c hono.Context) http.Response {
|
||||
return c.text('OK')
|
||||
})
|
||||
|
||||
// ==================== CRUD 路由 ====================
|
||||
|
||||
app.get('/api/users', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"users": []}')
|
||||
})
|
||||
|
||||
app.post('/api/users', fn (mut c hono.Context) http.Response {
|
||||
c.status(201)
|
||||
return c.json('{"created": true}')
|
||||
})
|
||||
|
||||
app.get('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||
id := c.params['id'] or { '' }
|
||||
return c.json('{"id": "${id}"}')
|
||||
})
|
||||
|
||||
app.put('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||
id := c.params['id'] or { '' }
|
||||
return c.json('{"id": "${id}", "updated": true}')
|
||||
})
|
||||
|
||||
app.delete('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||
id := c.params['id'] or { '' }
|
||||
return c.json('{"id": "${id}", "deleted": true}')
|
||||
})
|
||||
|
||||
app.patch('/api/users/:id', fn (mut c hono.Context) http.Response {
|
||||
id := c.params['id'] or { '' }
|
||||
return c.json('{"id": "${id}", "patched": true}')
|
||||
})
|
||||
|
||||
// ==================== 多参数路由 ====================
|
||||
|
||||
app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response {
|
||||
user_id := c.params['user_id'] or { '' }
|
||||
post_id := c.params['post_id'] or { '' }
|
||||
return c.json('{"user_id": "${user_id}", "post_id": "${post_id}"}')
|
||||
})
|
||||
|
||||
app.get('/api/categories/:category/items/:item', fn (mut c hono.Context) http.Response {
|
||||
category := c.params['category'] or { '' }
|
||||
item := c.params['item'] or { '' }
|
||||
return c.json('{"category": "${category}", "item": "${item}"}')
|
||||
})
|
||||
|
||||
// ==================== 查询参数路由 ====================
|
||||
|
||||
app.get('/api/search', fn (mut c hono.Context) http.Response {
|
||||
q := c.query['q'] or { '' }
|
||||
limit := c.query['limit'] or { '10' }
|
||||
page := c.query['page'] or { '1' }
|
||||
return c.json('{"query": "${q}", "limit": ${limit}, "page": ${page}}')
|
||||
})
|
||||
|
||||
// ==================== 响应格式路由 ====================
|
||||
|
||||
app.get('/api/json', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"message": "JSON response"}')
|
||||
})
|
||||
|
||||
app.get('/api/html', fn (mut c hono.Context) http.Response {
|
||||
return c.html('<html><body><h1>HTML Response</h1></body></html>')
|
||||
})
|
||||
|
||||
app.get('/api/created', fn (mut c hono.Context) http.Response {
|
||||
c.status(201)
|
||||
return c.json('{"created": true}')
|
||||
})
|
||||
|
||||
app.get('/api/custom-header', fn (mut c hono.Context) http.Response {
|
||||
c.headers['X-Custom-Header'] = 'custom-value'
|
||||
return c.text('OK')
|
||||
})
|
||||
|
||||
// ==================== 中间件测试路由 ====================
|
||||
|
||||
app.get('/api/middleware-test', fn (mut c hono.Context) http.Response {
|
||||
return c.text('middleware test')
|
||||
})
|
||||
|
||||
app.get('/api/middleware-order', fn (mut c hono.Context) http.Response {
|
||||
return c.json('{"order": "correct"}')
|
||||
})
|
||||
|
||||
// ==================== 自定义 404 ====================
|
||||
|
||||
app.not_found(fn (mut c hono.Context) http.Response {
|
||||
c.status(404)
|
||||
return c.json('{"error": "Not Found", "path": "${c.path}"}')
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
println('[test-server] Starting on port 9999...')
|
||||
app.listen(':9999')
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import hono
|
||||
import net.http
|
||||
|
||||
// 重定向功能测试
|
||||
fn redirect_basic_test() bool {
|
||||
mut c := hono.Context.new(
|
||||
http.Request{
|
||||
method: .get
|
||||
url: '/test'
|
||||
},
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
''
|
||||
)
|
||||
|
||||
response := c.redirect('https://example.com')
|
||||
|
||||
if response.status_code != 302 {
|
||||
return false
|
||||
}
|
||||
|
||||
location := response.header.get_custom('Location') or { '' }
|
||||
if location != 'https://example.com' {
|
||||
return false
|
||||
}
|
||||
|
||||
if response.body != '' {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn redirect_with_status_test() bool {
|
||||
mut c := hono.Context.new(
|
||||
http.Request{
|
||||
method: .get
|
||||
url: '/test'
|
||||
},
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
''
|
||||
)
|
||||
|
||||
response := c.redirect('https://example.com', 301)
|
||||
|
||||
if response.status_code != 301 {
|
||||
return false
|
||||
}
|
||||
|
||||
location := response.header.get_custom('Location') or { '' }
|
||||
if location != 'https://example.com' {
|
||||
return false
|
||||
}
|
||||
|
||||
if response.body != '' {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn redirect_multiple_status_codes_test() bool {
|
||||
status_codes := [301, 302, 303, 307, 308]
|
||||
|
||||
for code in status_codes {
|
||||
mut c := hono.Context.new(
|
||||
http.Request{
|
||||
method: .get
|
||||
url: '/test'
|
||||
},
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
''
|
||||
)
|
||||
|
||||
response := c.redirect('https://example.com', code)
|
||||
|
||||
if response.status_code != code {
|
||||
return false
|
||||
}
|
||||
|
||||
location := response.header.get_custom('Location') or { '' }
|
||||
if location != 'https://example.com' {
|
||||
return false
|
||||
}
|
||||
|
||||
if response.body != '' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn redirect_relative_url_test() bool {
|
||||
mut c := hono.Context.new(
|
||||
http.Request{
|
||||
method: .get
|
||||
url: '/test'
|
||||
},
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
''
|
||||
)
|
||||
|
||||
response := c.redirect('/relative-path')
|
||||
|
||||
if response.status_code != 302 {
|
||||
return false
|
||||
}
|
||||
|
||||
location := response.header.get_custom('Location') or { '' }
|
||||
if location != '/relative-path' {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fn redirect_with_existing_headers_test() bool {
|
||||
mut c := hono.Context.new(
|
||||
http.Request{
|
||||
method: .get
|
||||
url: '/test'
|
||||
},
|
||||
map[string]string{},
|
||||
map[string]string{},
|
||||
''
|
||||
)
|
||||
|
||||
// Set some existing headers
|
||||
c.headers['X-Custom-Header'] = 'custom-value'
|
||||
c.headers['Cache-Control'] = 'no-cache'
|
||||
|
||||
response := c.redirect('https://example.com')
|
||||
|
||||
if response.status_code != 302 {
|
||||
return false
|
||||
}
|
||||
|
||||
location := response.header.get_custom('Location') or { '' }
|
||||
if location != 'https://example.com' {
|
||||
return false
|
||||
}
|
||||
|
||||
custom_header := response.header.get_custom('X-Custom-Header') or { '' }
|
||||
if custom_header != 'custom-value' {
|
||||
return false
|
||||
}
|
||||
|
||||
cache_control := response.header.get_custom('Cache-Control') or { '' }
|
||||
if cache_control != 'no-cache' {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 测试统计结构
|
||||
struct TestStats {
|
||||
mut:
|
||||
total_tests int
|
||||
passed_tests int
|
||||
failed_tests int
|
||||
}
|
||||
|
||||
fn (mut stats TestStats) run_test(test_name string, test_func fn() bool) {
|
||||
stats.total_tests++
|
||||
print('🧪 ${test_name}... ')
|
||||
|
||||
if test_func() {
|
||||
stats.passed_tests++
|
||||
println('✅')
|
||||
} else {
|
||||
stats.failed_tests++
|
||||
println('❌')
|
||||
}
|
||||
}
|
||||
|
||||
fn (stats TestStats) print_summary() {
|
||||
println('\n=== 重定向测试总结 ===')
|
||||
println('总测试数: ${stats.total_tests}')
|
||||
println('通过: ${stats.passed_tests}')
|
||||
println('失败: ${stats.failed_tests}')
|
||||
|
||||
if stats.failed_tests == 0 {
|
||||
println('🎉 所有重定向测试通过!')
|
||||
} else {
|
||||
println('⚠️ 有 ${stats.failed_tests} 个测试失败')
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println('🚀 开始重定向功能测试...\n')
|
||||
|
||||
mut stats := TestStats{}
|
||||
|
||||
// 运行所有重定向测试
|
||||
stats.run_test('基本重定向 (302)', redirect_basic_test)
|
||||
stats.run_test('自定义状态码重定向 (301)', redirect_with_status_test)
|
||||
stats.run_test('多种状态码测试', redirect_multiple_status_codes_test)
|
||||
stats.run_test('相对路径重定向', redirect_relative_url_test)
|
||||
stats.run_test('保持现有头部信息', redirect_with_existing_headers_test)
|
||||
|
||||
// 打印测试总结
|
||||
stats.print_summary()
|
||||
}
|
||||
@@ -227,10 +227,18 @@ pub fn (mut manager ChunkUploadManager) handle_chunk_upload(mut ctx Context) htt
|
||||
return ctx.json('{"success": true, "chunk_index": $chunk_index, "all_chunk_uploaded": false, "message": "Chunk uploaded successfully"}')
|
||||
}
|
||||
|
||||
// 合并请求结构
|
||||
pub struct MergeRequest {
|
||||
pub:
|
||||
file_hash string @[json: 'file_hash']
|
||||
filename string
|
||||
total_chunks int @[json: 'total_chunks']
|
||||
}
|
||||
|
||||
// 处理分片合并
|
||||
pub fn (mut manager ChunkUploadManager) handle_chunk_merge(mut ctx Context) http.Response {
|
||||
// 解析请求体
|
||||
merge_request := hono.parse_merge_request(ctx.body) or {
|
||||
// 使用 x.json2 解析请求体
|
||||
merge_request := json2.decode[MergeRequest](ctx.body) or {
|
||||
return ctx.bad_request('Invalid request body: ${err}')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user