Compare commits

...
7 Commits
31 changed files with 2555 additions and 216 deletions
+7 -1
View File
@@ -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
+13
View File
@@ -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
View File
@@ -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
+83 -9
View File
@@ -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 的程序
+151
View File
@@ -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)
```
+171
View File
@@ -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。
+1 -1
View File
@@ -100,7 +100,7 @@ fn main() {
})
println('🚀 用户角色菜单管理系统已启动')
println('📱 访问地址: http://localhost:3000')
println('📱 访问地址: http://127.0.0.1:3000')
println('')
println('📋 示例用户:')
println(' 用户名: admin, 密码: admin123, 角色: admin')
+1 -1
View File
@@ -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')
}
+344
View File
@@ -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('╚═══════════════════════════════════════════════════════════════╝')
}
+237
View File
@@ -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/')
}
+1 -1
View File
@@ -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')
}
+75
View File
@@ -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')
}
+1 -1
View File
@@ -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 处理器')
+82
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
// v-hono 服务器示例 - 用于性能对比测试
// 运行: v run server_hono.v
// 测试: curl http://localhost:8081/
// 测试: curl http://127.0.0.1:8081/
module main
+61
View File
@@ -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)
}
+164
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
// vweb 服务器示例 - 用于性能对比测试
// 运行: v run server_vweb.v
// 测试: curl http://localhost:8080/
// 测试: curl http://127.0.0.1:8080/
module main
+93
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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' }
}
}
+28
View File
@@ -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
+68
View File
@@ -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 {}
}
+270
View File
@@ -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
}
+129
View File
@@ -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')
}
+208
View File
@@ -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()
}
+10 -2
View File
@@ -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}')
}
+1 -1
View File
@@ -1,7 +1,7 @@
Module {
name: 'hono'
description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.'
version: '0.3.0'
version: '0.3.3'
license: 'MIT'
dependencies: []
}