Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0298ea577 | ||
|
|
8e1007a1b4 | ||
|
|
6cdba50f36 | ||
|
|
418ead392b | ||
|
|
e305abba2b | ||
|
|
e22f612f24 | ||
|
|
a38aeea614 |
@@ -335,6 +335,24 @@ fn (mut s ServerHanler) exec_context_middlewares(idx int, mut ctx Context, handl
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn (mut app Hono) listen(port string) {
|
pub fn (mut app Hono) listen(port string) {
|
||||||
|
// 解析端口号
|
||||||
|
port_num := port.trim(':').int()
|
||||||
|
if port_num <= 0 {
|
||||||
|
eprintln('[v-hono] Invalid port: ${port}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用优化配置的 picoev 高性能服务器(支持高并发)
|
||||||
|
app.listen_picoev_with_config(PicoevConfig{
|
||||||
|
port: port_num
|
||||||
|
timeout_secs: 120 // 高并发场景需要更长超时
|
||||||
|
keepalive_timeout: 30 // Keep-Alive 超时 30 秒
|
||||||
|
max_keepalive_req: 10000 // 单连接最大请求数
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用传统 http.Server 启动(保留兼容性)
|
||||||
|
pub fn (mut app Hono) listen_http(port string) {
|
||||||
app.server.addr = port
|
app.server.addr = port
|
||||||
app.server.handler = server_hanler_new(app)
|
app.server.handler = server_hanler_new(app)
|
||||||
// 增加超时配置以支持更好的 Keep-Alive
|
// 增加超时配置以支持更好的 Keep-Alive
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Quick concurrent test using runspaces
|
||||||
|
$url = "http://127.0.0.1:8888/"
|
||||||
|
$concurrency = 300
|
||||||
|
$requestsEach = 20
|
||||||
|
|
||||||
|
Write-Host "Testing $url with $concurrency concurrent workers, $requestsEach requests each"
|
||||||
|
Write-Host "Total: $($concurrency * $requestsEach) requests"
|
||||||
|
|
||||||
|
$runspacePool = [runspacefactory]::CreateRunspacePool(1, $concurrency)
|
||||||
|
$runspacePool.Open()
|
||||||
|
|
||||||
|
$scriptBlock = {
|
||||||
|
param($url, $count)
|
||||||
|
$s = 0; $f = 0
|
||||||
|
for ($i = 0; $i -lt $count; $i++) {
|
||||||
|
try {
|
||||||
|
$req = [System.Net.WebRequest]::Create($url)
|
||||||
|
$req.Timeout = 30000
|
||||||
|
$resp = $req.GetResponse()
|
||||||
|
if ($resp.StatusCode -eq "OK") { $s++ } else { $f++ }
|
||||||
|
$resp.Close()
|
||||||
|
} catch {
|
||||||
|
$f++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @{Success=$s; Failed=$f}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
$runspaces = @()
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $concurrency; $i++) {
|
||||||
|
$ps = [powershell]::Create()
|
||||||
|
$ps.RunspacePool = $runspacePool
|
||||||
|
$ps.AddScript($scriptBlock).AddArgument($url).AddArgument($requestsEach) | Out-Null
|
||||||
|
$runspaces += @{PS=$ps; Handle=$ps.BeginInvoke()}
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalSuccess = 0
|
||||||
|
$totalFailed = 0
|
||||||
|
|
||||||
|
foreach ($rs in $runspaces) {
|
||||||
|
$result = $rs.PS.EndInvoke($rs.Handle)
|
||||||
|
$totalSuccess += $result.Success
|
||||||
|
$totalFailed += $result.Failed
|
||||||
|
$rs.PS.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
$sw.Stop()
|
||||||
|
$runspacePool.Close()
|
||||||
|
|
||||||
|
$total = $totalSuccess + $totalFailed
|
||||||
|
$rps = [math]::Round($total * 1000 / $sw.ElapsedMilliseconds, 2)
|
||||||
|
$rate = [math]::Round($totalSuccess * 100 / $total, 2)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Results:"
|
||||||
|
Write-Host " Total: $total"
|
||||||
|
Write-Host " Success: $totalSuccess"
|
||||||
|
Write-Host " Failed: $totalFailed"
|
||||||
|
Write-Host " Duration: $($sw.ElapsedMilliseconds) ms"
|
||||||
|
Write-Host " RPS: $rps"
|
||||||
|
Write-Host " Success Rate: $rate%"
|
||||||
|
|
||||||
|
if ($totalFailed -eq 0) {
|
||||||
|
Write-Host "PASSED - No connection errors!" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "FAILED - $totalFailed errors occurred" -ForegroundColor Red
|
||||||
|
}
|
||||||
+45
-23
@@ -1,40 +1,62 @@
|
|||||||
# 已知问题
|
# 已知问题
|
||||||
|
|
||||||
## 1. HTTP Keep-Alive 连接复用不支持
|
## 1. HTTP Keep-Alive 连接复用优化
|
||||||
|
|
||||||
**状态**: 等待 V 语言官方修复
|
**状态**: 已优化 (2025-12-30)
|
||||||
|
|
||||||
**问题描述**:
|
**问题描述**:
|
||||||
每次 HTTP 请求响应时间约 300ms,其中大部分时间消耗在 TCP 连接建立上。即使响应头中包含 `Connection: keep-alive`,连接也无法复用。
|
在超高并发(5000 req, 200 concurrent)的 URL Params 测试中出现稳定性问题(成功率仅 0.24%)。
|
||||||
|
|
||||||
**现象**:
|
|
||||||
- 快速连续发送请求:每次都慢(~300ms)
|
|
||||||
- 慢速发送请求:快慢交替(复用时快,超时后重建连接时慢)
|
|
||||||
|
|
||||||
**根本原因**:
|
**根本原因**:
|
||||||
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
|
```v
|
||||||
// F:\v\vlib\net\http\server.v
|
// 使用高并发优化配置启动
|
||||||
fn (mut w HandlerWorker) handle_conn(mut conn net.TcpConn) {
|
app.listen_high_concurrency(8081)
|
||||||
defer {
|
|
||||||
conn.close() or { eprintln('close() failed: ${err}') } // 每次请求后关闭连接
|
// 或使用自定义配置
|
||||||
}
|
config := hono.HighConcurrencyConfig{
|
||||||
// ... 处理单个请求 ...
|
port: 8081
|
||||||
|
timeout_secs: 5 // 较短超时
|
||||||
|
keepalive_timeout: 3 // Keep-Alive 超时
|
||||||
|
max_keepalive_req: 500 // 单连接最大请求数
|
||||||
}
|
}
|
||||||
|
app.listen_high_concurrency_with_config(config)
|
||||||
```
|
```
|
||||||
|
|
||||||
**影响**:
|
### 4. 服务器配置优化
|
||||||
- 无法实现 HTTP/1.1 Keep-Alive 连接复用
|
- 降低默认超时时间(8秒 -> 5秒)以更快释放连接
|
||||||
- 每次请求都需要重新建立 TCP 连接(三次握手)
|
- 增大写缓冲区(8KB -> 64KB)
|
||||||
- 高并发场景下性能受限
|
- 添加 TCP_NODELAY 配置选项
|
||||||
|
|
||||||
**临时解决方案**:
|
**使用建议**:
|
||||||
- 在服务器前部署支持 Keep-Alive 的反向代理(如 nginx)
|
1. 高并发场景使用 `--high-concurrency` 或 `-hc` 参数启动服务器
|
||||||
- 或等待 V 语言官方修复此问题
|
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
|
- 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` | 路由匹配性能测试(纯路由,不启动服务器) |
|
| `benchmark_veb_vs_hono.v` | 路由匹配性能测试(纯路由,不启动服务器) |
|
||||||
| `server_vweb.v` | vweb HTTP 服务器示例(端口 8080) |
|
| `server_veb.v` | veb HTTP 服务器示例(端口 8080)- V 语言官方新版 web 框架 |
|
||||||
|
| `server_vweb.v` | vweb HTTP 服务器示例(端口 8080)- 旧版兼容 |
|
||||||
| `server_hono.v` | v-hono HTTP 服务器示例(端口 8081) |
|
| `server_hono.v` | v-hono HTTP 服务器示例(端口 8081) |
|
||||||
|
| `http_benchmark.v` | HTTP 压测工具(自动对比两个服务器) |
|
||||||
|
| `run_benchmark.ps1` | Windows PowerShell 一键测试脚本 |
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
|
### Windows 用户(推荐)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd examples
|
||||||
|
.\run_benchmark.ps1
|
||||||
|
```
|
||||||
|
|
||||||
### 1. 路由匹配性能测试
|
### 1. 路由匹配性能测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
v run benchmark_vweb_vs_hono.v
|
cd examples
|
||||||
|
v run benchmark_veb_vs_hono.v
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. HTTP 服务器压测
|
### 2. HTTP 服务器压测
|
||||||
|
|
||||||
启动 vweb 服务器:
|
#### 方式一:使用内置压测工具
|
||||||
|
|
||||||
|
终端 1 - 启动 veb 服务器:
|
||||||
```bash
|
```bash
|
||||||
v run server_vweb.v
|
v run server_veb.v
|
||||||
```
|
```
|
||||||
|
|
||||||
启动 v-hono 服务器(另一个终端):
|
终端 2 - 启动 v-hono 服务器:
|
||||||
```bash
|
```bash
|
||||||
v run server_hono.v
|
v run server_hono.v
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 wrk 或 ab 进行压测:
|
终端 3 - 运行压测:
|
||||||
```bash
|
```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/
|
||||||
wrk -t4 -c100 -d10s http://localhost:8080/api/users/123
|
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/
|
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/:id/posts` | 动态 | 获取用户的帖子 |
|
||||||
| `GET /api/users/:user_id/posts/:post_id` | 动态 | 获取特定帖子 |
|
| `GET /api/users/:user_id/posts/:post_id` | 动态 | 获取特定帖子 |
|
||||||
| `GET /api/categories/:cat/items/:item` | 动态 | 获取分类商品 |
|
| `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,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('🚀 用户角色菜单管理系统已启动')
|
||||||
println('📱 访问地址: http://localhost:3000')
|
println('📱 访问地址: http://127.0.0.1:3000')
|
||||||
println('')
|
println('')
|
||||||
println('📋 示例用户:')
|
println('📋 示例用户:')
|
||||||
println(' 用户名: admin, 密码: admin123, 角色: admin')
|
println(' 用户名: admin, 密码: admin123, 角色: admin')
|
||||||
|
|||||||
@@ -42,6 +42,6 @@ fn main() {
|
|||||||
return c.json('{"received": "${body}"}')
|
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')
|
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"}')
|
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')
|
app.listen(':3000')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ fn main() {
|
|||||||
return c.html('<h1>Desktop Version</h1>')
|
return c.html('<h1>Desktop Version</h1>')
|
||||||
})
|
})
|
||||||
|
|
||||||
println('Server running on http://localhost:8080')
|
println('Server running on http://127.0.0.1:8080')
|
||||||
println('Try these endpoints:')
|
println('Try these endpoints:')
|
||||||
println(' GET /redirect-basic - Basic redirect (302)')
|
println(' GET /redirect-basic - Basic redirect (302)')
|
||||||
println(' GET /redirect-permanent - Permanent redirect (301)')
|
println(' GET /redirect-permanent - Permanent redirect (301)')
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ fn main() {
|
|||||||
return c.html('<h1>Welcome to v-hono!</h1><p>API available at /api</p>')
|
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('Routes:')
|
||||||
println(' GET / - Welcome page')
|
println(' GET / - Welcome page')
|
||||||
println(' GET /api/users - List users')
|
println(' GET /api/users - List users')
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ app.route("/api/books", mut books)</pre>
|
|||||||
// 打印路由信息
|
// 打印路由信息
|
||||||
fn print_routes_info() {
|
fn print_routes_info() {
|
||||||
println('')
|
println('')
|
||||||
println('📍 服务器地址: http://localhost:8080')
|
println('📍 服务器地址: http://127.0.0.1:8080')
|
||||||
println('')
|
println('')
|
||||||
println('📚 Books API (/api/books):')
|
println('📚 Books API (/api/books):')
|
||||||
println(' GET /api/books - 获取所有书籍')
|
println(' GET /api/books - 获取所有书籍')
|
||||||
|
|||||||
@@ -233,16 +233,16 @@ app.route(\'/api/books\', mut books)</pre>
|
|||||||
<div class="group">
|
<div class="group">
|
||||||
<h2>🧪 测试命令</h2>
|
<h2>🧪 测试命令</h2>
|
||||||
<pre># 测试 all() 方法
|
<pre># 测试 all() 方法
|
||||||
curl http://localhost:8080/echo
|
curl http://127.0.0.1:8080/echo
|
||||||
curl -X POST http://localhost:8080/echo
|
curl -X POST http://127.0.0.1:8080/echo
|
||||||
curl -X PUT http://localhost:8080/echo
|
curl -X PUT http://127.0.0.1:8080/echo
|
||||||
|
|
||||||
# 测试自定义 404
|
# 测试自定义 404
|
||||||
curl http://localhost:8080/not-exist
|
curl http://127.0.0.1:8080/not-exist
|
||||||
|
|
||||||
# 测试 Admin 认证中间件
|
# 测试 Admin 认证中间件
|
||||||
curl http://localhost:8080/admin # 返回 401
|
curl http://127.0.0.1:8080/admin # 返回 401
|
||||||
curl -H "Authorization: Bearer admin-token" http://localhost:8080/admin # 成功</pre>
|
curl -H "Authorization: Bearer admin-token" http://127.0.0.1:8080/admin # 成功</pre>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>'
|
</html>'
|
||||||
@@ -251,7 +251,7 @@ curl -H "Authorization: Bearer admin-token" http://localhost:8080/admin # 成
|
|||||||
// 打印路由信息
|
// 打印路由信息
|
||||||
fn print_routes_info() {
|
fn print_routes_info() {
|
||||||
println('')
|
println('')
|
||||||
println('📍 服务器地址: http://localhost:8080')
|
println('📍 服务器地址: http://127.0.0.1:8080')
|
||||||
println('')
|
println('')
|
||||||
println('🆕 新功能:')
|
println('🆕 新功能:')
|
||||||
println(' - notFound(): 自定义 404 处理器')
|
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-hono 服务器示例 - 用于性能对比测试
|
||||||
// 运行: v run server_hono.v
|
// 运行: v run server_hono.v
|
||||||
// 测试: curl http://localhost:8081/
|
// 测试: curl http://127.0.0.1:8081/
|
||||||
|
|
||||||
module main
|
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 服务器示例 - 用于性能对比测试
|
// vweb 服务器示例 - 用于性能对比测试
|
||||||
// 运行: v run server_vweb.v
|
// 运行: v run server_vweb.v
|
||||||
// 测试: curl http://localhost:8080/
|
// 测试: curl http://127.0.0.1:8080/
|
||||||
|
|
||||||
module main
|
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)
|
time.sleep(2 * time.second)
|
||||||
println('服务器已启动在 http://localhost:8081')
|
println('服务器已启动在 http://127.0.0.1:8081')
|
||||||
|
|
||||||
// 执行测试
|
// 执行测试
|
||||||
run_tests()
|
run_tests()
|
||||||
@@ -124,7 +124,7 @@ fn test_small_file_performance() {
|
|||||||
// 传统方式
|
// 传统方式
|
||||||
start := time.now()
|
start := time.now()
|
||||||
for i in 0 .. 100 {
|
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
|
traditional_time := time.now() - start
|
||||||
println('传统方式 100次请求: $traditional_time')
|
println('传统方式 100次请求: $traditional_time')
|
||||||
@@ -132,7 +132,7 @@ fn test_small_file_performance() {
|
|||||||
// 流式方式
|
// 流式方式
|
||||||
start2 := time.now()
|
start2 := time.now()
|
||||||
for i in 0 .. 100 {
|
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
|
stream_time := time.now() - start2
|
||||||
println('流式方式 100次请求: $stream_time')
|
println('流式方式 100次请求: $stream_time')
|
||||||
@@ -140,7 +140,7 @@ fn test_small_file_performance() {
|
|||||||
// 智能方式
|
// 智能方式
|
||||||
start3 := time.now()
|
start3 := time.now()
|
||||||
for i in 0 .. 100 {
|
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
|
smart_time := time.now() - start3
|
||||||
println('智能方式 100次请求: $smart_time')
|
println('智能方式 100次请求: $smart_time')
|
||||||
@@ -152,7 +152,7 @@ fn test_medium_file_performance() {
|
|||||||
// 传统方式
|
// 传统方式
|
||||||
start := time.now()
|
start := time.now()
|
||||||
for i in 0 .. 10 {
|
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
|
traditional_time := time.now() - start
|
||||||
println('传统方式 10次请求: $traditional_time')
|
println('传统方式 10次请求: $traditional_time')
|
||||||
@@ -160,7 +160,7 @@ fn test_medium_file_performance() {
|
|||||||
// 流式方式
|
// 流式方式
|
||||||
start2 := time.now()
|
start2 := time.now()
|
||||||
for i in 0 .. 10 {
|
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
|
stream_time := time.now() - start2
|
||||||
println('流式方式 10次请求: $stream_time')
|
println('流式方式 10次请求: $stream_time')
|
||||||
@@ -172,7 +172,7 @@ fn test_large_file_performance() {
|
|||||||
// 只测试流式方式,避免内存问题
|
// 只测试流式方式,避免内存问题
|
||||||
start := time.now()
|
start := time.now()
|
||||||
for i in 0 .. 5 {
|
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 {
|
if result.exit_code != 0 {
|
||||||
println(' 请求 $i 失败')
|
println(' 请求 $i 失败')
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ fn test_large_file_performance() {
|
|||||||
// 智能方式(应该自动选择流式传输)
|
// 智能方式(应该自动选择流式传输)
|
||||||
start2 := time.now()
|
start2 := time.now()
|
||||||
for i in 0 .. 5 {
|
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 {
|
if result.exit_code != 0 {
|
||||||
println(' 智能方式请求 $i 失败')
|
println(' 智能方式请求 $i 失败')
|
||||||
}
|
}
|
||||||
@@ -196,14 +196,14 @@ fn test_range_requests() {
|
|||||||
println('\n--- Range请求测试 ---')
|
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 {
|
if result1.exit_code == 0 {
|
||||||
println('✅ Range请求 0-99: 成功')
|
println('✅ Range请求 0-99: 成功')
|
||||||
} else {
|
} else {
|
||||||
println('❌ Range请求 0-99: 失败')
|
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 {
|
if result2.exit_code == 0 {
|
||||||
println('✅ Range请求 100-199: 成功')
|
println('✅ Range请求 100-199: 成功')
|
||||||
} else {
|
} else {
|
||||||
@@ -215,7 +215,7 @@ fn test_smart_selection() {
|
|||||||
println('\n--- 智能选择测试 ---')
|
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 {
|
if result1.exit_code == 0 {
|
||||||
println('✅ 智能选择小文件: 成功')
|
println('✅ 智能选择小文件: 成功')
|
||||||
} else {
|
} 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 result2.exit_code == 0 {
|
||||||
// 检查下载的文件大小
|
// 检查下载的文件大小
|
||||||
if os.exists('large_output.txt') {
|
if os.exists('large_output.txt') {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 测试服务器 - 用于高并发测试
|
||||||
|
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('OK')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/delay', fn (mut c hono.Context) http.Response {
|
||||||
|
// 模拟一些处理延迟
|
||||||
|
return c.text('OK with delay')
|
||||||
|
})
|
||||||
|
|
||||||
|
println('测试服务器启动中...')
|
||||||
|
println('配置: timeout_secs=120, keepalive_timeout=30, max_keepalive_req=10000')
|
||||||
|
|
||||||
|
app.listen(':8888')
|
||||||
|
}
|
||||||
+287
@@ -0,0 +1,287 @@
|
|||||||
|
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 = 120 // 高并发场景需要更长超时(原 8 秒太短)
|
||||||
|
max_headers int = 100
|
||||||
|
max_read int = 8192
|
||||||
|
max_write int = 65536
|
||||||
|
keepalive_timeout int = 30 // Keep-Alive 超时延长到 30 秒
|
||||||
|
max_keepalive_req int = 10000 // 单连接最大请求数提升到 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
for key, value in ctx.headers {
|
||||||
|
// 跳过 Content-Length,picohttpparser 会在 body() 时自动添加
|
||||||
|
if key.to_lower() == 'content-length' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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 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_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' }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
Module {
|
Module {
|
||||||
name: 'hono'
|
name: 'hono'
|
||||||
description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.'
|
description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.'
|
||||||
version: '0.3.1'
|
version: '0.3.4'
|
||||||
license: 'MIT'
|
license: 'MIT'
|
||||||
dependencies: []
|
dependencies: []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user