Compare commits

..
32 Commits
Author SHA1 Message Date
meiseayoung 22e8dd4548 chore: update v.mod version 2026-01-06 14:11:40 +08:00
meiseayoung a702746df7 build(usockets): add pre-compiled library for MacOS-arm64 platform 2026-01-06 14:10:02 +08:00
meiseayoung a57ce2e77a chore: update v.mod version 2026-01-06 12:01:45 +08:00
meiseayoung 7a08c3142a build(usockets): add pre-compiled library for MacOS-x86 platform 2026-01-06 12:01:20 +08:00
meiseayoung 362015c1cf docs: update build instructions for uSockets backend
- Clarify that Windows requires gcc (tcc does not support .a static library format)
- Add separate build commands for macOS/Linux and Windows
- Document pre-compiled library locations by platform
- Add w64devkit as recommended MinGW download option
2026-01-05 12:25:06 +08:00
meiseayoung 0969901481 chore: update v.mod version 2026-01-05 11:50:46 +08:00
meiseayoung 6bddfc3731 build(usockets): add pre-compiled library for Linux platform 2026-01-05 11:49:47 +08:00
meiseayoung 26ac804b1d chore: update v.mod version 2026-01-05 11:07:41 +08:00
meiseayoung 56299b5485 refactor(usockets): reorganize lib directory structure for multi-platform support
- Organize lib directory by platform: windows/, macos-arm64/, macos-x64/, linux/
- Update build.sh to support unified build process for Windows/macOS/Linux
- Compile libuv + uSockets and merge into complete static library
- Update usockets.v conditional compilation to point to new directory structure
- Optimize benchmark tests with connection pooling for high concurrency testing
- Clean up unused legacy library files

Performance test results (Windows):
- 1000 concurrent, 1M requests: 35,223 req/s
- 10000 concurrent, 1M requests: 26,884 req/s
2026-01-05 11:06:56 +08:00
meiseayoung d31492fd59 chore: update v.mod version 2026-01-04 18:12:21 +08:00
meiseayoung e19f799e46 chore: upload usokets lib 2026-01-04 18:11:19 +08:00
meiseayoung f7e753969d fix: change the usockets/lib path 2026-01-04 15:44:54 +08:00
meiseayoung e9d14a35f6 fix(streaming): use direct socket fd writes for real-time SSE streaming
Previously, PicoevStreamWriter used picohttpparser.Response.raw() to write
streaming data, which only buffered the data until the entire request handler
completed. This caused SSE responses to be delayed until all sleep() calls
finished (e.g., 2.89s delay for /stream-text endpoint).

Changes:
- Modified PicoevStreamWriter to use socket fd directly from picohttpparser.Response
- Implemented write_to_fd() using C.send() for immediate data delivery
- Updated handle_picoev_streaming_response() to write HTTP headers directly to socket
- Added C.send() function declaration for direct socket writes

This fix enables true real-time streaming where each write() or write_sse()
call immediately sends data to the client, rather than buffering until
the callback completes.

All 74 test files pass with no regressions.
2026-01-04 15:43:01 +08:00
meiseayoung 9ae9170bcb feat: add SSE streaming helper support
Add Server-Sent Events (SSE) and streaming response support with three
streaming functions:

- stream(): Basic binary streaming with chunked transfer encoding
- stream_text(): Text streaming with proper Content-Type headers
- stream_sse(): SSE event streaming with full event field support

Features:
- SSEEvent struct with data, event, id, retry fields
- StreamContext for write, writeln, write_sse, sleep, pipe operations
- Multi-line data handling in SSE events
- Custom error handler support
- Client disconnect detection via on_abort callback
- Integrated with both Picoev and uSockets backends

New files:
- streaming.v: Core streaming implementation
- examples/sse_demo.v: Comprehensive SSE demo with test page
- tests/test_stream_*.v: 62 property-based tests

Updated:
- picoev_server.v: PicoevStreamWriter integration
- usockets_server.v: UsocketsStreamWriter integration
- README.md: SSE streaming documentation
2026-01-04 15:13:28 +08:00
meiseayoung 6ab6d3b99e feat(websocket): add WebSocket Helper with RFC 6455 compliance
Add comprehensive WebSocket support for v-hono framework, inspired by Hono.js WebSocket Helper.

Features:
- upgrade_websocket() function for WebSocket route handlers
- Event-based callbacks: on_open, on_message, on_close, on_error
- WSContext with send(), send_bytes(), send_json(), close() methods
- Connection state tracking (connecting, open, closing, closed)
- Configurable options: ping_interval, max_message_size, timeout, protocols
- Subprotocol negotiation support
- Ping/pong keep-alive handling
- Message size validation with 1009 close code
- Integration with picoev and uSockets backends

Protocol Compliance:
- RFC 6455 compliant handshake with Sec-WebSocket-Accept computation
- Frame encoding/decoding with proper masking
- Close handshake with status codes and reasons
- Text and binary message frame support

Testing:
- 13 property-based test files with 82 tests total
- Validates handshake, frame encoding, masking, JSON round-trip
- Tests context preservation, route patterns, connection state
- All existing tests pass (no regressions)

Files added:
- websocket.v: Core WebSocket implementation
- examples/websocket_demo.v: Chat room example
- examples/websocket_test.html: Browser test client
- tests/test_websocket_*.v: Property-based tests

Files modified:
- app.v: Add ws() method for WebSocket routes
- middleware.v: Export WebSocket types and functions
- picoev_server.v: WebSocket upgrade detection
- usockets_server.v: WebSocket upgrade detection
- README.md: WebSocket documentation
2026-01-04 13:38:12 +08:00
meiseayoung 4e2c6a90c3 feat: add 7 built-in middleware components inspired by Hono.js
New Middleware:
- CORS: Cross-Origin Resource Sharing with full configuration support
- Cookie Helper: Cookie management with signed cookie support (HMAC-SHA256)
- JWT: JSON Web Token authentication (HS256/384/512)
- Bearer Auth: Simple Bearer token authentication
- Compression: Gzip and deflate response compression
- Rate Limiting: Request rate limiting with customizable stores
- Validator: Schema-based request validation

Features:
- Context store for middleware data sharing (get/set/get_client_ip)
- Middleware export module with utility functions
- Comprehensive property-based tests for all middleware
- Example file demonstrating all middleware usage

Files Added:
- cors.v, cookie.v, jwt.v, bearer_auth.v, compress.v, rate_limit.v, validator.v
- middleware.v (unified exports)
- examples/middleware_demo.v
- tests/test_*.v (9 new test files)

Documentation:
- Updated README.md with detailed middleware usage guide
- Added Schema Builder API documentation
2026-01-04 11:11:15 +08:00
meiseayoung 44ba82c194 chore: update v.mod version 2026-01-03 20:50:24 +08:00
meiseayoung 9eb360926b feat(usockets): optimize high concurrency support up to 10,000+ connections
- Increase uSockets listen() backlog from 512 to 16384 in bsd.c
- Add high concurrency test suite (5000-10000 concurrent connections)
- Add HIGH_CONCURRENCY.md documentation with optimization guide
- Add usockets/build.sh script for rebuilding uSockets library
- Update README with latest benchmark results

Performance (macOS M3 Max):
- 5,000 concurrent: 81,524 RPS, 100% success
- 10,000 concurrent: 24,198 RPS, 100% success

System requirements:
- kern.ipc.somaxconn >= 8192
- ulimit -n 65535
2026-01-03 20:45:30 +08:00
meiseayoung 3b8d21dd0d fix: update import path to VPM package name (meiseayoung.hono)
- Change all `import hono` to `import meiseayoung.hono` in code and docs
- Ensure compatibility with VPM installation via `v install meiseayoung.hono`
2026-01-02 22:05:12 +08:00
meiseayoung 43aae4f10d chore: update v.mod version 2026-01-02 21:51:01 +08:00
meiseayoung 05bb2d6c95 feat: cross-platform uSockets support and route cache optimization
1. Cross-platform uSockets configuration
   - Use @VMODROOT for portable library paths
   - Support macOS (arm64/x86_64), Windows, and Linux
   - Auto-detect Homebrew paths for libuv on macOS

2. Route cache optimization (3x performance boost)
   - Replace LRU cache with FastRouteCache for dynamic routes
   - Zero-overhead cache lookup (no TTL check, no LRU reordering)
   - Dynamic route performance: 30k -> 97k RPS

3. New test files
   - Go-based integration tests for picoev and uSockets
   - FastCache optimization validation tests
   - Benchmark comparison tools
2026-01-02 21:44:07 +08:00
meiseayoung 9aefe5714d chore: update v.mod version 2025-12-31 18:35:29 +08:00
meiseayoung 1fc22044c3 perf: optimize HTTP parsing with zero-allocation techniques
- Refactor parse_path_and_query() to use single-pass traversal instead of split()
- Add eq_ignore_case() and contains_ignore_case() to avoid to_lower() allocations
- Optimize HTTP method parsing with first-char branching (parse_http_method_fast)
- Optimize Keep-Alive header check to avoid string allocations
- Apply optimizations to all server backends: picoev, usockets, and app.v

Performance improvements:
- Query string parsing: 20-44% faster
- Keep-Alive check: 82-90% faster
- HTTP method parsing: eliminates string comparison overhead
- Response header processing: removes to_lower() allocations

All 20+ test suites pass with no regressions.
2025-12-31 18:34:46 +08:00
meiseayoung b478269d8e chore: update v.mod version 2025-12-31 17:38:29 +08:00
meiseayoung 69d1385ced perf: 0 middleware ignore copy 2025-12-31 17:37:51 +08:00
meiseayoung 1482b9c4d9 doc: add Framework Comparison 2025-12-31 16:04:44 +08:00
meiseayoung 0937dd268c chore: update v.mod version 2025-12-31 15:46:10 +08:00
meiseayoung 07382ab622 feat: integration usockets backend 2025-12-31 15:44:51 +08:00
meiseayoung 5375b7394f chore: update v.mod version 2025-12-30 21:40:39 +08:00
meiseayoung d0298ea577 fix: In high concurrency (200+), V-Hono experiences an error where the connection is reset problem 2025-12-30 19:23:09 +08:00
meiseayoung 8e1007a1b4 chore: update v.mod version 2025-12-30 18:42:02 +08:00
meiseayoung 6cdba50f36 fix: Remove duplicate response headers 2025-12-30 18:41:34 +08:00
153 changed files with 32116 additions and 431 deletions
+3 -1
View File
@@ -2,7 +2,6 @@
*.exe
*.out
*.o
*.a
*.so
*.dll
@@ -45,3 +44,6 @@ temp/
# Chunk upload temp files
chunks/
.kiro
*.def
+974 -6
View File
File diff suppressed because it is too large Load Diff
+130 -17
View File
@@ -40,6 +40,10 @@ pub mut:
use_fast_router bool = true // 新增:是否使用快速路由器
context_middlewares []ContextMiddleware
route_middlewares map[string][]ContextMiddleware // 路由前缀对应的中间件
// 优化:预排序的中间件前缀列表(启动时计算一次)
sorted_middleware_prefixes []string
// 优化:标记是否有中间件(用于零中间件快速路径)
has_middlewares bool
}
// Context 中间件类型
@@ -171,6 +175,35 @@ pub fn (mut app Hono) options(path string, handler fn (mut Context) http.Respons
app.context_trie_router.add_route('OPTIONS', path, h)
}
// ws() - Register a WebSocket route
// This method registers a WebSocket upgrade handler at the specified path.
// The factory function receives the HTTP Context and returns WSEvents configuration.
//
// Parameters:
// path: The route path (supports path parameters like /ws/:room/:id)
// factory: A function that receives the HTTP Context and returns WSEvents
// options: Optional WebSocket configuration
//
// Example:
// app.ws('/chat/:room', fn (c Context) WSEvents {
// room := c.params['room'] or { 'default' }
// return WSEvents{
// on_open: fn (mut ws WSContext) {
// ws.send('Welcome to room!') or {}
// }
// on_message: fn (event WSMessageEvent, mut ws WSContext) {
// ws.send('Echo: ${event.data}') or {}
// }
// }
// })
pub fn (mut app Hono) ws(path string, factory WSHandlerFactory, options ...WebSocketOptions) {
// Create the WebSocket upgrade handler
ws_handler := upgrade_websocket(factory, ...options)
// Register as a GET route (WebSocket upgrades use GET method)
app.get(path, ws_handler)
}
// all() 方法 - 为所有 HTTP 方法注册同一个处理器
pub fn (mut app Hono) all(path string, handler fn (mut Context) http.Response) {
app.get(path, handler)
@@ -185,6 +218,14 @@ pub fn (mut app Hono) all(path string, handler fn (mut Context) http.Response) {
// Context 中间件
pub fn (mut app Hono) use(mw ContextMiddleware) {
app.context_middlewares << mw
app.has_middlewares = true
}
// 预计算中间件前缀排序(在服务器启动前调用)
pub fn (mut app Hono) precompute_middleware_prefixes() {
app.sorted_middleware_prefixes = app.route_middlewares.keys()
app.sorted_middleware_prefixes.sort(a.len < b.len)
app.has_middlewares = app.context_middlewares.len > 0 || app.route_middlewares.len > 0
}
// notFound() - 自定义 404 处理器
@@ -215,25 +256,31 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response {
}
}
// 解析 query
raw_query := url.query().to_map()
// 解析 query - 优化:直接使用 url.raw_query 避免额外解析
mut query_map := map[string]string{}
for key, values in raw_query {
if values.len > 0 {
query_map[key] = values[0]
}
if url.raw_query.len > 0 {
query_map = parse_query_string_app(url.raw_query)
}
// 缓存 method 字符串,避免多次调用 .str()
method_str := req.method.str()
// 尝试 Context 路由
// 优先使用快速路由器
if s.app.use_fast_router {
if route_match := s.app.fast_router.match_route(req.method.str(), url.path) {
if route_match := s.app.fast_router.match_route(method_str, url.path) {
// param 由路由匹配结果提供
param_map := route_match.params.clone()
// body
body := req.data
// 构造 Context
mut ctx := Context.new(req, param_map, query_map, body)
// 优化:零中间件快速路径
if !s.app.has_middlewares {
return route_match.handler.handle(mut ctx)
}
// 获取该路径对应的中间件
middlewares := s.get_middlewares_for_path(url.path)
// 洋葱模型递归执行中间件
@@ -243,13 +290,19 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response {
}
// 如果快速路由器没有匹配,回退到混合路由器
if route_match := s.app.context_hybrid_router.match_route(req.method.str(), url.path) {
if route_match := s.app.context_hybrid_router.match_route(method_str, url.path) {
// param 由路由匹配结果提供
param_map := route_match.params.clone()
// body
body := req.data
// 构造 Context
mut ctx := Context.new(req, param_map, query_map, body)
// 优化:零中间件快速路径
if !s.app.has_middlewares {
return route_match.handler.handle(mut ctx)
}
// 获取该路径对应的中间件
middlewares := s.get_middlewares_for_path(url.path)
// 洋葱模型递归执行中间件
@@ -258,13 +311,19 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response {
})
}
} else {
if route_match := s.app.context_hybrid_router.match_route(req.method.str(), url.path) {
if route_match := s.app.context_hybrid_router.match_route(method_str, url.path) {
// param 由路由匹配结果提供
param_map := route_match.params.clone()
// body
body := req.data
// 构造 Context
mut ctx := Context.new(req, param_map, query_map, body)
// 优化:零中间件快速路径
if !s.app.has_middlewares {
return route_match.handler.handle(mut ctx)
}
// 获取该路径对应的中间件
middlewares := s.get_middlewares_for_path(url.path)
// 洋葱模型递归执行中间件
@@ -291,15 +350,17 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response {
})
}
// 获取路径对应的所有中间件(全局 + 路由前缀匹配的)
// 获取路径对应的所有中间件(全局 + 路由前缀匹配的)- 优化版
fn (s ServerHanler) get_middlewares_for_path(path string) []ContextMiddleware {
// 优化:只有全局中间件时,直接返回引用(避免克隆)
if s.app.route_middlewares.len == 0 {
return s.app.context_middlewares
}
mut middlewares := s.app.context_middlewares.clone()
// 按前缀长度排序,确保更具体的前缀后执行
mut prefixes := s.app.route_middlewares.keys()
prefixes.sort(a.len < b.len)
for prefix in prefixes {
// 优化:使用预排序的前缀列表(启动时已排序,不需要每次请求都排序)
for prefix in s.app.sorted_middleware_prefixes {
if path.starts_with(prefix) || prefix == '/' {
if mws := s.app.route_middlewares[prefix] {
middlewares << mws
@@ -342,8 +403,16 @@ pub fn (mut app Hono) listen(port string) {
return
}
// 默认使用 picoev 高性能服务器
app.listen_picoev(port_num)
// 优化:预计算中间件前缀排序
app.precompute_middleware_prefixes()
// 使用优化配置的 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 启动(保留兼容性)
@@ -367,6 +436,7 @@ pub fn (mut app Hono) route(prefix string, mut subapp Hono) {
} else {
app.route_middlewares[prefix] = subapp.context_middlewares.clone()
}
app.has_middlewares = true
}
// 继承子应用的 notFound 和 onError 处理器(如果主应用没有设置)
@@ -503,5 +573,48 @@ pub fn Hono.new() Hono {
fast_router: FastRouter.new()
use_fast_router: true
route_middlewares: map[string][]ContextMiddleware{}
sorted_middleware_prefixes: []string{}
has_middlewares: false
}
}
// 快速解析 query string(单次遍历,避免 split)- app.v 版本
@[inline]
fn parse_query_string_app(query_str string) map[string]string {
mut query_map := map[string]string{}
len := query_str.len
if len == 0 {
return query_map
}
mut key_start := 0
mut key_end := -1
mut value_start := -1
for i := 0; i <= len; i++ {
ch := if i < len { query_str[i] } else { `&` } // 末尾视为分隔符
if ch == `=` && key_end == -1 {
key_end = i
value_start = i + 1
} else if ch == `&` {
// 完成一个键值对
if key_end > key_start && value_start > 0 {
key := query_str[key_start..key_end]
value := if value_start < i { query_str[value_start..i] } else { '' }
query_map[key] = value
} else if key_end == -1 && i > key_start {
// 只有 key 没有 value
key := query_str[key_start..i]
query_map[key] = ''
}
// 重置状态
key_start = i + 1
key_end = -1
value_start = -1
}
}
return query_map
}
+137
View File
@@ -0,0 +1,137 @@
module hono
import net.http
// BearerToken 类型 - 支持单 token 或多 token 配置
pub type BearerToken = string | []string
// BearerAuthOptions 结构体 - Bearer Auth 配置选项
pub struct BearerAuthOptions {
pub:
token BearerToken // Token 配置(必需)
realm string // WWW-Authenticate realm
prefix string = 'Bearer' // 认证前缀,默认 "Bearer"
header_name string = 'Authorization' // 请求头名称,默认 "Authorization"
hash_function ?fn (string) string // 哈希函数(用于安全比较)
verify_token ?fn (string, Context) bool // 自定义验证回调
}
// bearer_auth - Bearer Auth 中间件工厂函数
// 返回一个 ContextMiddleware,用于验证 Bearer Token
pub fn bearer_auth(options BearerAuthOptions) ContextMiddleware {
return fn [options] (mut c Context, next fn (mut Context) http.Response) http.Response {
// 获取 Authorization 头
auth_header := c.req.header.get_custom(options.header_name) or {
return unauthorized_response(mut c, options.realm, 'Missing authorization header')
}
if auth_header.len == 0 {
return unauthorized_response(mut c, options.realm, 'Missing authorization header')
}
// 解析 token
token := extract_bearer_token(auth_header, options.prefix) or {
return unauthorized_response(mut c, options.realm, 'Invalid token format')
}
// 验证 token
if !validate_bearer_token(token, options, c) {
return unauthorized_response(mut c, options.realm, 'Invalid token')
}
// 将 token 存储到 Context(便于后续使用)
c.set('bearer_token', token)
// 继续处理请求
return next(mut c)
}
}
// extract_bearer_token - 从 Authorization 头提取 token
fn extract_bearer_token(auth_header string, prefix string) !string {
expected_prefix := '${prefix} '
if !auth_header.starts_with(expected_prefix) {
return error('Invalid authorization format')
}
token := auth_header[expected_prefix.len..].trim_space()
if token.len == 0 {
return error('Empty token')
}
return token
}
// validate_bearer_token - 验证 Bearer Token
fn validate_bearer_token(token string, options BearerAuthOptions, c Context) bool {
// 优先使用自定义验证回调
if verify_fn := options.verify_token {
return verify_fn(token, c)
}
// 使用配置的 token 进行验证
match options.token {
string {
// 单 token 验证
return secure_token_compare(token, options.token, options.hash_function)
}
[]string {
// 多 token 验证
for valid_token in options.token {
if secure_token_compare(token, valid_token, options.hash_function) {
return true
}
}
return false
}
}
}
// secure_token_compare - 安全的 token 比较(防止时序攻击)
fn secure_token_compare(provided string, expected string, hash_fn ?fn (string) string) bool {
// 如果提供了哈希函数,先对 token 进行哈希
if hash_function := hash_fn {
hashed_provided := hash_function(provided)
hashed_expected := hash_function(expected)
return constant_time_compare_bearer(hashed_provided, hashed_expected)
}
// 直接使用常量时间比较
return constant_time_compare_bearer(provided, expected)
}
// constant_time_compare_bearer - 常量时间字符串比较,防止时序攻击
fn constant_time_compare_bearer(a string, b string) bool {
if a.len != b.len {
return false
}
mut result := u8(0)
for i := 0; i < a.len; i++ {
result |= a[i] ^ b[i]
}
return result == 0
}
// unauthorized_response - 返回 401 未授权响应
fn unauthorized_response(mut c Context, realm string, message string) http.Response {
c.status(401)
// 设置 WWW-Authenticate 头
mut www_auth := 'Bearer'
if realm.len > 0 {
www_auth = 'Bearer realm="${realm}"'
}
c.headers['WWW-Authenticate'] = www_auth
return c.json('{"error":"Unauthorized","message":"${message}"}')
}
// get_bearer_token - 从 Context 获取已验证的 Bearer Token
// 这是一个便捷方法,用于在 handler 中获取已验证的 token
pub fn get_bearer_token(c Context) ?string {
return c.get('bearer_token')
}
+77
View File
@@ -0,0 +1,77 @@
# V-Hono Benchmark
Performance benchmarks for V-Hono web framework.
## Framework Comparison (500 connections, 100K requests)
| Framework | RPS | Errors | Avg Latency | P50 | P95 | P99 |
|-----------|-----|--------|-------------|-----|-----|-----|
| **v-hono (uSockets)** | **20,231** | **0** | **24.62ms** | **23.65ms** | **33.82ms** | **57.29ms** |
| v-hono (picoev) | 16,177 | 0 | 30.75ms | 26.25ms | 35.37ms | 111.38ms |
| veb (V官方) | 7,278 | 772 | 68.06ms | 55.79ms | 71.62ms | 98.90ms |
### Performance Summary
- **v-hono (uSockets) vs veb**: **2.78x faster**, zero errors
- **v-hono (uSockets) vs picoev**: **25% faster**, better P99 latency
- **v-hono (picoev) vs veb**: **2.22x faster**, zero errors
## Running Benchmarks
### 1. Start Test Server
**uSockets backend:**
```bash
v -enable-globals -cc gcc -ldflags "-ldbghelp" benchmark/usockets_server.v -o server.exe
./server.exe
```
**Picoev backend:**
```bash
v -enable-globals examples/picoev_example.v -o server.exe
./server.exe
```
### 2. Run Integration Tests
```bash
go run benchmark/usockets_verify.go
```
### 3. Run Performance Tests
```bash
# 200 connections, 100K requests
go run tests/main.go -url "http://127.0.0.1:9998" -c 200 -n 100000
# 500 connections, 100K requests
go run tests/main.go -url "http://127.0.0.1:9998" -c 500 -n 100000
```
### 4. Quick Concurrent Test (PowerShell)
```powershell
powershell -File benchmark/quick_test.ps1
```
## Test Files
| File | Description |
|------|-------------|
| `usockets_server.v` | uSockets test server with full API |
| `usockets_verify.go` | Go integration test (17 test cases) |
| `quick_test.ps1` | PowerShell concurrent test script |
| `../tests/main.go` | Go performance benchmark tool |
## Test Coverage
The integration test (`usockets_verify.go`) covers:
- ✅ Basic GET routes (root, health, static)
- ✅ Dynamic routes (single/multi/nested params)
- ✅ Query parameters
- ✅ Response formats (JSON, HTML, custom status)
- ✅ HTTP methods (POST, PUT, DELETE)
- ✅ Error handling (404)
- ✅ Keep-Alive connections
- ✅ Throughput performance
+69
View File
@@ -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
}
+121
View File
@@ -0,0 +1,121 @@
// uSockets 测试服务器
// 用于集成测试验证 uSockets 后端功能
//
// 编译运行:
// v -enable-globals -cc gcc -ldflags "-ldbghelp" benchmark/usockets_test_server.v -o usockets_test.exe
// .\usockets_test.exe
module main
import meiseayoung.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')
})
// ==================== 自定义 404 ====================
app.not_found(fn (mut c hono.Context) http.Response {
c.status(404)
return c.json('{"error": "Not Found", "path": "${c.path}"}')
})
// 启动 uSockets 服务器
println('[usockets-test-server] Starting on port 9998...')
app.listen_usockets(9998)
}
+496
View File
@@ -0,0 +1,496 @@
// uSockets 功能验证测试
// go run benchmark/usockets_verify.go
package main
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
const baseURL = "http://127.0.0.1:9998"
func main() {
fmt.Println("╔═══════════════════════════════════════════════════════════════╗")
fmt.Println("║ uSockets 集成测试 ║")
fmt.Println("╚═══════════════════════════════════════════════════════════════╝")
fmt.Println()
client := &http.Client{Timeout: 5 * time.Second}
// 检查服务器
fmt.Print("🔍 检查测试服务器... ")
if !checkServer(client) {
fmt.Println("❌ 服务器未运行")
return
}
fmt.Println("✅")
fmt.Println()
total, passed, failed := 0, 0, 0
var errors []string
// 1. 基本 GET 路由测试
fmt.Println("📦 1. 基本 GET 路由测试")
total++
fmt.Print(" 🧪 GET 根路径... ")
if checkGetRoot(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "GET 根路径")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 GET 健康检查... ")
if checkGetHealth(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "GET 健康检查")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 GET 静态路由... ")
if checkGetStatic(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "GET 静态路由")
fmt.Println("❌")
}
fmt.Println()
// 2. 动态路由测试
fmt.Println("📦 2. 动态路由测试")
total++
fmt.Print(" 🧪 单参数路由... ")
if checkSingleParam(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "单参数路由")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 多参数路由... ")
if checkMultiParams(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "多参数路由")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 嵌套参数路由... ")
if checkNestedParams(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "嵌套参数路由")
fmt.Println("❌")
}
fmt.Println()
// 3. 查询参数测试
fmt.Println("📦 3. 查询参数测试")
total++
fmt.Print(" 🧪 单个查询参数... ")
if checkSingleQuery(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "单个查询参数")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 多个查询参数... ")
if checkMultiQuery(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "多个查询参数")
fmt.Println("❌")
}
fmt.Println()
// 4. 响应格式测试
fmt.Println("📦 4. 响应格式测试")
total++
fmt.Print(" 🧪 JSON 响应... ")
if checkJSONResponse(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "JSON 响应")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 HTML 响应... ")
if checkHTMLResponse(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "HTML 响应")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 自定义状态码 201... ")
if checkCustomStatus(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "自定义状态码 201")
fmt.Println("❌")
}
fmt.Println()
// 5. HTTP 方法测试
fmt.Println("📦 5. HTTP 方法测试")
total++
fmt.Print(" 🧪 POST 请求... ")
if checkPOST(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "POST 请求")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 PUT 请求... ")
if checkPUT(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "PUT 请求")
fmt.Println("❌")
}
total++
fmt.Print(" 🧪 DELETE 请求... ")
if checkDELETE(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "DELETE 请求")
fmt.Println("❌")
}
fmt.Println()
// 6. 错误处理测试
fmt.Println("📦 6. 错误处理测试")
total++
fmt.Print(" 🧪 404 未找到... ")
if checkNotFound(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "404 未找到")
fmt.Println("❌")
}
fmt.Println()
// 7. Keep-Alive 测试
fmt.Println("📦 7. Keep-Alive 连接测试")
total++
fmt.Print(" 🧪 连接复用... ")
if checkKeepAlive(client) {
passed++
fmt.Println("✅")
} else {
failed++
errors = append(errors, "连接复用")
fmt.Println("❌")
}
fmt.Println()
// 8. 性能测试
fmt.Println("📦 8. 性能测试")
total++
fmt.Print(" 🧪 吞吐量测试... ")
rps := checkThroughput(client)
if rps > 100 {
passed++
fmt.Printf("✅ (%.0f req/s)\n", rps)
} else {
failed++
errors = append(errors, "吞吐量")
fmt.Printf("❌ (%.0f req/s)\n", rps)
}
fmt.Println()
// 输出总结
fmt.Println("═══════════════════════════════════════════════════════════════")
fmt.Printf("📊 测试结果: %d/%d 通过\n", passed, total)
if failed > 0 {
fmt.Println("❌ 失败的测试:")
for _, err := range errors {
fmt.Printf(" - %s\n", err)
}
} else {
fmt.Println("🎉 所有测试通过!uSockets 后端验证成功!")
}
fmt.Println("═══════════════════════════════════════════════════════════════")
}
func checkServer(client *http.Client) bool {
resp, err := client.Get(baseURL + "/health")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
func getBody(resp *http.Response) string {
body, _ := io.ReadAll(resp.Body)
return string(body)
}
func checkGetRoot(client *http.Client) bool {
resp, err := client.Get(baseURL + "/")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "Hello")
}
func checkGetHealth(client *http.Client) bool {
resp, err := client.Get(baseURL + "/health")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && body == "OK"
}
func checkGetStatic(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/health")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && body == "OK"
}
func checkSingleParam(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/users/456")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "456")
}
func checkMultiParams(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/users/123/posts/789")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "123") && strings.Contains(body, "789")
}
func checkNestedParams(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/categories/tech/items/laptop")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "tech") && strings.Contains(body, "laptop")
}
func checkSingleQuery(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/search?q=test")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "test")
}
func checkMultiQuery(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/search?q=hello&limit=10&page=1")
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "hello")
}
func checkJSONResponse(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/json")
if err != nil {
return false
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
return resp.StatusCode == 200 && strings.Contains(ct, "application/json")
}
func checkHTMLResponse(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/html")
if err != nil {
return false
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
return resp.StatusCode == 200 && strings.Contains(ct, "text/html")
}
func checkCustomStatus(client *http.Client) bool {
resp, err := client.Get(baseURL + "/api/created")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 201
}
func checkPOST(client *http.Client) bool {
resp, err := client.Post(baseURL+"/api/users", "application/json", nil)
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 201 && strings.Contains(body, "created")
}
func checkPUT(client *http.Client) bool {
req, _ := http.NewRequest("PUT", baseURL+"/api/users/123", nil)
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "updated")
}
func checkDELETE(client *http.Client) bool {
req, _ := http.NewRequest("DELETE", baseURL+"/api/users/123", nil)
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
body := getBody(resp)
return resp.StatusCode == 200 && strings.Contains(body, "deleted")
}
func checkNotFound(client *http.Client) bool {
resp, err := client.Get(baseURL + "/nonexistent/path")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 404
}
func checkKeepAlive(client *http.Client) bool {
for i := 0; i < 10; i++ {
resp, err := client.Get(baseURL + "/health")
if err != nil {
return false
}
resp.Body.Close()
if resp.StatusCode != 200 {
return false
}
}
return true
}
func checkThroughput(client *http.Client) float64 {
start := time.Now()
concurrency := 1000
totalRequests := 1000000
requestsPerWorker := totalRequests / concurrency
// 创建共享的 Transport,启用连接池复用
transport := &http.Transport{
MaxIdleConns: concurrency,
MaxIdleConnsPerHost: concurrency,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: false,
}
results := make(chan int, concurrency)
for i := 0; i < concurrency; i++ {
go func() {
localSuccess := 0
// 每个 goroutine 使用独立 Client,但共享 Transport(连接池)
localClient := &http.Client{
Timeout: 10 * time.Second,
Transport: transport,
}
for j := 0; j < requestsPerWorker; j++ {
resp, err := localClient.Get(baseURL + "/health")
if err != nil {
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode == 200 {
localSuccess++
}
}
results <- localSuccess
}()
}
success := 0
for i := 0; i < concurrency; i++ {
success += <-results
}
elapsed := time.Since(start).Seconds()
fmt.Printf("\n [并发: %d, 总请求: %d, 成功: %d, 耗时: %.2fs]\n ", concurrency, totalRequests, success, elapsed)
return float64(success) / elapsed
}
+68
View File
@@ -336,3 +336,71 @@ pub fn (mut cache ContextLRUCache) clear() {
cache.size = 0
cache.last_cleanup = time.now().unix()
}
// ============================================================================
// 高性能路由缓存 - 专为路由匹配优化
// ============================================================================
// 特点:
// 1. 无 TTL 检查(路由不会过期)
// 2. 无 LRU 移动(简单 map 查找)
// 3. 固定大小,满了就清空重建
// 4. 零开销的 get 操作
pub struct FastRouteCache {
mut:
cache map[string]ContextRouteMatch
capacity int
enabled bool = true
}
pub fn FastRouteCache.new(capacity int) FastRouteCache {
return FastRouteCache{
cache: map[string]ContextRouteMatch{}
capacity: capacity
enabled: true
}
}
// 快速获取 - 零开销
@[inline]
pub fn (cache &FastRouteCache) get(key string) ?ContextRouteMatch {
if !cache.enabled {
return none
}
if result := cache.cache[key] {
return result
}
return none
}
// 快速设置
@[inline]
pub fn (mut cache FastRouteCache) put(key string, value ContextRouteMatch) {
if !cache.enabled {
return
}
// 如果满了,清空重建(比 LRU 淘汰更快)
if cache.cache.len >= cache.capacity {
cache.cache.clear()
}
cache.cache[key] = value
}
// 获取统计
pub fn (cache &FastRouteCache) get_stats() (int, int) {
return cache.cache.len, cache.capacity
}
// 清空缓存
pub fn (mut cache FastRouteCache) clear() {
cache.cache.clear()
}
// 启用/禁用
pub fn (mut cache FastRouteCache) set_enabled(enabled bool) {
cache.enabled = enabled
if !enabled {
cache.clear()
}
}
+249
View File
@@ -0,0 +1,249 @@
module hono
import net.http
import compress.gzip
import compress.zlib
// CompressEncoding 枚举 - 支持的压缩编码类型
pub enum CompressEncoding {
gzip
deflate
}
// CompressOptions 结构体 - 压缩配置选项
pub struct CompressOptions {
pub:
encoding ?CompressEncoding // 指定编码,none 表示自动选择
threshold int = 1024 // 最小压缩大小(字节),默认 1KB
level int = 128 // 压缩级别 (0-4095 for gzip)
}
// compress - 压缩中间件工厂函数
// 返回一个 ContextMiddleware,用于压缩响应体
pub fn compress(options ...CompressOptions) ContextMiddleware {
opts := if options.len > 0 { options[0] } else { CompressOptions{} }
return fn [opts] (mut c Context, next fn (mut Context) http.Response) http.Response {
// 先执行后续处理器获取响应
mut response := next(mut c)
// 检查 Cache-Control: no-transform
if has_no_transform(response) {
return response
}
// 检查响应体大小是否达到阈值
if response.body.len < opts.threshold {
return response
}
// 检查 Content-Type 是否可压缩
content_type := get_response_content_type(response)
if !is_compressible_content_type(content_type) {
return response
}
// 检查响应是否已经被压缩
if is_already_compressed(response) {
return response
}
// 获取客户端支持的编码
accept_encoding := c.req.header.get_custom('Accept-Encoding') or { '' }
if accept_encoding.len == 0 {
return response
}
// 选择压缩编码
encoding := select_encoding(accept_encoding, opts.encoding)
if encoding == none {
return response
}
// 执行压缩
selected_encoding := encoding or { return response }
compressed_body := compress_body(response.body, selected_encoding, opts.level) or {
// 压缩失败,返回原始响应
return response
}
// 如果压缩后更大,返回原始响应
if compressed_body.len >= response.body.len {
return response
}
// 构建新的响应头
mut new_header := http.new_header()
// 复制原有头部
for key in response.header.keys() {
values := response.header.custom_values(key)
for value in values {
// 跳过 Content-Length,稍后重新设置
if key.to_lower() != 'content-length' {
new_header.add_custom(key, value) or { continue }
}
}
}
// 设置 Content-Encoding
encoding_str := if selected_encoding == .gzip { 'gzip' } else { 'deflate' }
new_header.add_custom('Content-Encoding', encoding_str) or {}
// 设置新的 Content-Length
new_header.add_custom('Content-Length', compressed_body.len.str()) or {}
// 添加 Vary 头,表示响应根据 Accept-Encoding 变化
new_header.add_custom('Vary', 'Accept-Encoding') or {}
return http.Response{
status_code: response.status_code
header: new_header
body: compressed_body.bytestr()
}
}
}
// has_no_transform - 检查响应是否包含 Cache-Control: no-transform
fn has_no_transform(response http.Response) bool {
cache_control := response.header.get_custom('Cache-Control') or { '' }
return cache_control.contains('no-transform')
}
// get_response_content_type - 获取响应的 Content-Type
fn get_response_content_type(response http.Response) string {
return response.header.get_custom('Content-Type') or { '' }
}
// is_compressible_content_type - 检查 Content-Type 是否可压缩
fn is_compressible_content_type(content_type string) bool {
if content_type.len == 0 {
return true // 默认可压缩
}
ct_lower := content_type.to_lower()
// 可压缩的 MIME 类型
compressible_types := [
'text/',
'application/json',
'application/javascript',
'application/xml',
'application/xhtml+xml',
'application/rss+xml',
'application/atom+xml',
'application/x-javascript',
'application/x-font-ttf',
'font/opentype',
'font/ttf',
'font/eot',
'image/svg+xml',
'image/x-icon',
'image/vnd.microsoft.icon',
]
for ct in compressible_types {
if ct_lower.starts_with(ct) || ct_lower.contains(ct) {
return true
}
}
// 不可压缩的类型(已压缩的格式)
non_compressible_types := [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'video/',
'audio/',
'application/zip',
'application/gzip',
'application/x-gzip',
'application/x-compress',
'application/x-compressed',
'application/x-bzip',
'application/x-bzip2',
'application/x-rar-compressed',
'application/x-7z-compressed',
]
for ct in non_compressible_types {
if ct_lower.starts_with(ct) || ct_lower.contains(ct) {
return false
}
}
return true
}
// is_already_compressed - 检查响应是否已经被压缩
fn is_already_compressed(response http.Response) bool {
content_encoding := response.header.get_custom('Content-Encoding') or { '' }
return content_encoding.len > 0
}
// select_encoding - 选择最佳压缩编码
fn select_encoding(accept_encoding string, preferred ?CompressEncoding) ?CompressEncoding {
// 如果指定了首选编码,检查客户端是否支持
if pref := preferred {
match pref {
.gzip {
if accepts_gzip(accept_encoding) {
return .gzip
}
}
.deflate {
if accepts_deflate(accept_encoding) {
return .deflate
}
}
}
return none
}
// 自动选择:优先 gzip,其次 deflate
if accepts_gzip(accept_encoding) {
return .gzip
}
if accepts_deflate(accept_encoding) {
return .deflate
}
return none
}
// accepts_gzip - 检查是否接受 gzip 编码
fn accepts_gzip(accept_encoding string) bool {
return accept_encoding.contains('gzip') || accept_encoding.contains('*')
}
// accepts_deflate - 检查是否接受 deflate 编码
fn accepts_deflate(accept_encoding string) bool {
return accept_encoding.contains('deflate') || accept_encoding.contains('*')
}
// compress_body - 压缩响应体
fn compress_body(body string, encoding CompressEncoding, level int) ![]u8 {
data := body.bytes()
match encoding {
.gzip {
return gzip.compress(data, gzip.CompressParams{
compression_level: level
})
}
.deflate {
return zlib.compress(data)
}
}
}
// decompress_gzip - 解压 gzip 数据(用于测试)
pub fn decompress_gzip(data []u8) ![]u8 {
return gzip.decompress(data)
}
// decompress_deflate - 解压 deflate 数据(用于测试)
pub fn decompress_deflate(data []u8) ![]u8 {
return zlib.decompress(data)
}
+326
View File
@@ -0,0 +1,326 @@
module hono
import time
import crypto.sha256
import encoding.base64
// SameSite 枚举 - Cookie 的 SameSite 属性
pub enum SameSite {
strict
lax
none_
}
// CookieOptions 结构体 - Cookie 配置选项
pub struct CookieOptions {
pub:
path string = '/'
domain string
max_age int // 秒
expires ?time.Time
http_only bool
secure bool
same_site SameSite = .lax
}
// get_cookie - 从请求中获取指定名称的 Cookie 值
// 返回 Cookie 值,如果不存在则返回 none
pub fn get_cookie(c Context, name string) ?string {
cookie_header := c.req.header.get_custom('Cookie') or { return none }
if cookie_header.len == 0 {
return none
}
// 解析 Cookie 头
cookies := parse_cookie_header(cookie_header)
if name in cookies {
return cookies[name]
}
return none
}
// get_all_cookies - 获取请求中的所有 Cookie
// 返回一个 map,包含所有 Cookie 的名称和值
pub fn get_all_cookies(c Context) map[string]string {
cookie_header := c.req.header.get_custom('Cookie') or { return map[string]string{} }
if cookie_header.len == 0 {
return map[string]string{}
}
return parse_cookie_header(cookie_header)
}
// set_cookie - 设置 Cookie
// 将 Cookie 添加到响应的 Set-Cookie 头中
pub fn set_cookie(mut c Context, name string, value string, options ...CookieOptions) {
opts := if options.len > 0 { options[0] } else { CookieOptions{} }
cookie_str := build_cookie_string(name, value, opts)
// 添加到响应头
// 如果已经有 Set-Cookie 头,需要追加
if existing := c.headers['Set-Cookie'] {
c.headers['Set-Cookie'] = '${existing}, ${cookie_str}'
} else {
c.headers['Set-Cookie'] = cookie_str
}
}
// delete_cookie - 删除 Cookie
// 通过设置过期时间为过去的时间来删除 Cookie
pub fn delete_cookie(mut c Context, name string, options ...CookieOptions) {
mut opts := if options.len > 0 { options[0] } else { CookieOptions{} }
// 设置 max_age 为 0 或负数,并设置过期时间为过去
expired_time := time.unix(0)
// 创建新的选项,保留 path 和 domain,但设置过期
delete_opts := CookieOptions{
path: opts.path
domain: opts.domain
max_age: 0
expires: expired_time
http_only: opts.http_only
secure: opts.secure
same_site: opts.same_site
}
cookie_str := build_cookie_string(name, '', delete_opts)
if existing := c.headers['Set-Cookie'] {
c.headers['Set-Cookie'] = '${existing}, ${cookie_str}'
} else {
c.headers['Set-Cookie'] = cookie_str
}
}
// parse_cookie_header - 解析 Cookie 请求头
// 将 "name1=value1; name2=value2" 格式解析为 map
fn parse_cookie_header(header string) map[string]string {
mut cookies := map[string]string{}
// 按分号分割
pairs := header.split(';')
for pair in pairs {
trimmed := pair.trim_space()
if trimmed.len == 0 {
continue
}
// 找到第一个等号的位置
eq_pos := trimmed.index('=') or { continue }
if eq_pos == 0 {
continue
}
name := trimmed[..eq_pos].trim_space()
value := if eq_pos + 1 < trimmed.len {
trimmed[eq_pos + 1..].trim_space()
} else {
''
}
// 移除值两端的引号(如果有)
cleaned_value := if value.len >= 2 && value[0] == `"` && value[value.len - 1] == `"` {
value[1..value.len - 1]
} else {
value
}
cookies[name] = cleaned_value
}
return cookies
}
// build_cookie_string - 构建 Set-Cookie 头的值
fn build_cookie_string(name string, value string, opts CookieOptions) string {
mut parts := []string{}
// 基本的 name=value
parts << '${name}=${value}'
// Path
if opts.path.len > 0 {
parts << 'Path=${opts.path}'
}
// Domain
if opts.domain.len > 0 {
parts << 'Domain=${opts.domain}'
}
// Max-Age
if opts.max_age != 0 {
parts << 'Max-Age=${opts.max_age}'
}
// Expires
if expires := opts.expires {
// 格式化为 HTTP 日期格式: Wed, 09 Jun 2021 10:18:14 GMT
expires_str := format_cookie_date(expires)
parts << 'Expires=${expires_str}'
}
// HttpOnly
if opts.http_only {
parts << 'HttpOnly'
}
// Secure
if opts.secure {
parts << 'Secure'
}
// SameSite
match opts.same_site {
.strict { parts << 'SameSite=Strict' }
.lax { parts << 'SameSite=Lax' }
.none_ { parts << 'SameSite=None' }
}
return parts.join('; ')
}
// format_cookie_date - 格式化时间为 Cookie 日期格式
// 格式: Wed, 09 Jun 2021 10:18:14 GMT
fn format_cookie_date(t time.Time) string {
days := ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
months := ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
day_name := days[t.day_of_week()]
month_name := months[t.month - 1]
return '${day_name}, ${t.day:02} ${month_name} ${t.year} ${t.hour:02}:${t.minute:02}:${t.second:02} GMT'
}
// set_signed_cookie - 设置签名 Cookie
// 使用 HMAC-SHA256 对 Cookie 值进行签名
pub fn set_signed_cookie(mut c Context, name string, value string, secret string, options ...CookieOptions) ! {
if secret.len == 0 {
return error('Secret is required for signed cookies')
}
// 生成签名
signature := generate_hmac_signature(value, secret)
// 将值和签名组合: value.signature
signed_value := '${value}.${signature}'
opts := if options.len > 0 { options[0] } else { CookieOptions{} }
set_cookie(mut c, name, signed_value, opts)
}
// get_signed_cookie - 获取并验证签名 Cookie
// 验证签名,如果有效则返回原始值,否则返回错误
pub fn get_signed_cookie(c Context, name string, secret string) !string {
if secret.len == 0 {
return error('Secret is required for signed cookies')
}
// 获取 Cookie 值
signed_value := get_cookie(c, name) or {
return error('Cookie not found: ${name}')
}
// 分离值和签名
dot_pos := signed_value.last_index('.') or {
return error('Invalid signed cookie format')
}
if dot_pos == 0 || dot_pos >= signed_value.len - 1 {
return error('Invalid signed cookie format')
}
value := signed_value[..dot_pos]
signature := signed_value[dot_pos + 1..]
// 验证签名
expected_signature := generate_hmac_signature(value, secret)
if !constant_time_compare(signature, expected_signature) {
return error('Invalid signature')
}
return value
}
// generate_hmac_signature - 使用 HMAC-SHA256 生成签名
fn generate_hmac_signature(value string, secret string) string {
// 手动实现 HMAC-SHA256
// HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
// 其中 K' 是处理后的密钥,ipad = 0x36, opad = 0x5c
block_size := 64 // SHA256 块大小
// 处理密钥
mut key := secret.bytes()
if key.len > block_size {
// 如果密钥太长,先哈希
hash_result := sha256.sum(key)
key = []u8{len: 32}
for i := 0; i < 32; i++ {
key[i] = hash_result[i]
}
}
// 填充到块大小
for key.len < block_size {
key << u8(0)
}
// 计算 K' ⊕ ipad
mut i_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
i_key_pad[i] = key[i] ^ u8(0x36)
}
// 计算 K' ⊕ opad
mut o_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
o_key_pad[i] = key[i] ^ u8(0x5c)
}
// 计算内部哈希: H((K' ⊕ ipad) || m)
mut inner_data := i_key_pad.clone()
inner_data << value.bytes()
inner_hash_result := sha256.sum(inner_data)
mut inner_hash := []u8{len: 32}
for i := 0; i < 32; i++ {
inner_hash[i] = inner_hash_result[i]
}
// 计算外部哈希: H((K' ⊕ opad) || inner_hash)
mut outer_data := o_key_pad.clone()
outer_data << inner_hash
outer_hash_result := sha256.sum(outer_data)
mut outer_hash := []u8{len: 32}
for i := 0; i < 32; i++ {
outer_hash[i] = outer_hash_result[i]
}
// Base64URL 编码(不带填充)
return base64.url_encode(outer_hash).replace('=', '')
}
// constant_time_compare - 常量时间比较,防止时序攻击
fn constant_time_compare(a string, b string) bool {
if a.len != b.len {
return false
}
mut result := u8(0)
for i := 0; i < a.len; i++ {
result |= a[i] ^ b[i]
}
return result == 0
}
+135
View File
@@ -0,0 +1,135 @@
module hono
import net.http
// CorsOrigin 类型 - 支持多种来源配置方式
// 可以是字符串(单域名或 "*")、字符串数组(多域名)或回调函数
pub type CorsOrigin = string | []string | fn (string, Context) string
// CorsOptions 结构体 - CORS 配置选项
pub struct CorsOptions {
pub:
origin CorsOrigin = '*' // 允许的来源,默认允许所有
allow_methods []string = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
allow_headers []string = [] // 允许的请求头
expose_headers []string = [] // 暴露的响应头
max_age int // 预检请求缓存时间(秒),默认 0
credentials bool // 是否允许凭证,默认 false
}
// cors - CORS 中间件工厂函数
// 返回一个 ContextMiddleware,用于处理跨域请求
pub fn cors(options ...CorsOptions) ContextMiddleware {
opts := if options.len > 0 { options[0] } else { CorsOptions{} }
return fn [opts] (mut c Context, next fn (mut Context) http.Response) http.Response {
// 获取请求的 Origin 头
origin := c.req.header.get_custom('Origin') or { '' }
// 如果没有 Origin 头,直接继续处理
if origin.len == 0 {
return next(mut c)
}
// 计算允许的 Origin
allowed_origin := get_allowed_origin(origin, opts.origin, c)
// 如果 Origin 不被允许,直接继续处理(不设置 CORS 头)
if allowed_origin.len == 0 {
return next(mut c)
}
// 设置 Access-Control-Allow-Origin
c.headers['Access-Control-Allow-Origin'] = allowed_origin
// 设置 Access-Control-Allow-Credentials
if opts.credentials {
c.headers['Access-Control-Allow-Credentials'] = 'true'
}
// 设置 Access-Control-Expose-Headers
if opts.expose_headers.len > 0 {
c.headers['Access-Control-Expose-Headers'] = opts.expose_headers.join(', ')
}
// 检查是否为预检请求 (OPTIONS)
if c.req.method == http.Method.options {
return handle_preflight(mut c, opts)
}
// 非预检请求,继续处理
return next(mut c)
}
}
// get_allowed_origin - 根据配置计算允许的 Origin
fn get_allowed_origin(request_origin string, origin_config CorsOrigin, c Context) string {
match origin_config {
string {
// 单个字符串配置
if origin_config == '*' {
// 通配符,允许所有来源
return '*'
}
// 特定域名,检查是否匹配
if origin_config == request_origin {
return request_origin
}
return ''
}
[]string {
// 多域名配置,检查请求来源是否在列表中
for allowed in origin_config {
if allowed == '*' {
return '*'
}
if allowed == request_origin {
return request_origin
}
}
return ''
}
fn (string, Context) string {
// 回调函数配置
return origin_config(request_origin, c)
}
}
}
// handle_preflight - 处理 OPTIONS 预检请求
fn handle_preflight(mut c Context, opts CorsOptions) http.Response {
// 设置 Access-Control-Allow-Methods
if opts.allow_methods.len > 0 {
c.headers['Access-Control-Allow-Methods'] = opts.allow_methods.join(', ')
}
// 设置 Access-Control-Allow-Headers
if opts.allow_headers.len > 0 {
c.headers['Access-Control-Allow-Headers'] = opts.allow_headers.join(', ')
} else {
// 如果没有配置,尝试使用请求中的 Access-Control-Request-Headers
if request_headers := c.req.header.get_custom('Access-Control-Request-Headers') {
c.headers['Access-Control-Allow-Headers'] = request_headers
}
}
// 设置 Access-Control-Max-Age
if opts.max_age > 0 {
c.headers['Access-Control-Max-Age'] = opts.max_age.str()
}
// 返回 204 No Content
c.status(204)
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: ''
}
}
+115
View File
@@ -0,0 +1,115 @@
# v-hono 高并发优化指南
## 概述
v-hono 使用 uSockets 作为高性能网络后端,经过优化后可以稳定支持 **10,000+ 并发连接**。
## 性能测试结果
| 并发数 | RPS | 平均延迟 | P99延迟 | 成功率 |
|--------|-----|----------|---------|--------|
| 5,000 | 81,524 | 60.77ms | 295.17ms | 100% |
| 6,000 | 56,290 | 98.46ms | 557.47ms | 100% |
| 7,000 | 42,895 | 130.04ms | 824.80ms | 100% |
| 8,000 | 30,537 | 165.27ms | 1058.05ms | 100% |
| 9,000 | 28,693 | 215.86ms | 1422.67ms | 100% |
| 10,000 | 24,198 | 240.90ms | 1602.10ms | 100% |
测试环境:macOS (Apple Silicon M3 Max)
**重要提示**:每次测试后需等待 60 秒让 TIME_WAIT 连接释放,否则会影响下一次测试结果。
## 关键优化
### 1. uSockets backlog 参数
原始 uSockets 库的 `listen()` backlog 参数硬编码为 512,限制了高并发能力。
**修复位置**:`v-hono/usockets/src/bsd.c` (第 532 行和第 578 行)
```c
// 修改前
listen(listenFd, 512)
// 修改后
listen(listenFd, 16384)
```
修改后的源码保存在 `v-hono/usockets/src/bsd.c`,编译后的库文件在 `v-hono/lib/libusockets_full.a`。
**重新编译**:运行 `v-hono/usockets/build.sh` 脚本。
### 2. 系统参数配置
macOS 需要调整以下系统参数:
```bash
# 查看当前值
sysctl kern.ipc.somaxconn
# 临时修改(重启后失效)
sudo sysctl -w kern.ipc.somaxconn=8192
# 永久修改(添加到 /etc/sysctl.conf)
kern.ipc.somaxconn=8192
```
**注意**:实际 backlog 值会被 `kern.ipc.somaxconn` 截断,所以系统参数也需要相应调整。
## 重新编译 uSockets(可选)
如果需要自定义 backlog 值,可以重新编译 uSockets:
```bash
# 1. 克隆 uSockets
git clone https://github.com/uNetworking/uSockets.git
# 2. 修改 src/bsd.c 中的 listen 调用
# 将 listen(listenFd, 512) 改为 listen(listenFd, 16384)
# 3. 编译(macOS with libuv)
WITH_LIBUV=1 CFLAGS="-I/opt/homebrew/include" make
# 4. 复制到 v-hono
cp uSockets.a /path/to/v-hono/lib/libusockets_full.a
# 5. 重新编译 v-hono 应用
v -enable-globals -prod -o your_app your_app.v
```
## 使用建议
1. **生产环境**:建议使用 uSockets 后端(`app.listen_usockets(port)`)
2. **系统调优**:确保 `kern.ipc.somaxconn` >= 8192
3. **文件描述符**:服务器和客户端都需要设置 `ulimit -n 65535`
4. **启动命令**:`ulimit -n 65535 && ./your_server`
## 高并发测试
运行高并发测试:
```bash
# 启动服务器
ulimit -n 65535 && ./bench_server_usockets
# 运行测试 (5000-10000 并发)
ulimit -n 65535 && go run v-hono/tests/test_high_concurrency.go
# 单独测试某个级别
ulimit -n 65535 && go run v-hono/tests/test_high_concurrency.go 8000
```
## 问题排查
如果高并发测试失败,检查:
1. 服务器是否正常响应:`curl http://localhost:8080/health`
2. 系统参数:`sysctl kern.ipc.somaxconn`
3. 文件描述符限制:`ulimit -n`
4. 测试客户端是否有端口耗尽问题
## 版本信息
- uSockets backlog: 16384
- 测试日期: 2026-01-03
- 测试通过最大并发: 10,000(不使用渐进式启动)
+1 -1
View File
@@ -11,7 +11,7 @@
#### 使用方式
```v
import hono
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
+1 -1
View File
@@ -1,6 +1,6 @@
module main
import hono
import meiseayoung.hono
import net.http
import x.json2
+1 -1
View File
@@ -4,7 +4,7 @@
module main
import net.http
import hono
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
+1 -1
View File
@@ -9,7 +9,7 @@
module main
import time
import hono
import meiseayoung.hono
import net.http
// ============================================
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
fn main() {
+1 -1
View File
@@ -3,7 +3,7 @@ module main
import net.http
import time
import hono
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
+321
View File
@@ -0,0 +1,321 @@
// middleware_demo.v - 中间件使用示例
// 本示例展示了 v-hono 框架所有内置中间件的基本用法
module main
import hono
import net.http
import time
fn main() {
mut app := hono.Hono.new()
// ============================================================================
// 1. CORS 中间件示例
// ============================================================================
// 基本用法:允许所有来源
app.use(hono.cors())
// 高级配置示例(注释掉以避免重复)
// app.use(hono.cors(hono.CorsOptions{
// origin: 'https://example.com' // 只允许特定域名
// credentials: true // 允许携带凭证
// max_age: 600 // 预检请求缓存 10 分钟
// allow_methods: ['GET', 'POST', 'PUT', 'DELETE']
// allow_headers: ['Content-Type', 'Authorization']
// }))
// ============================================================================
// 2. 压缩中间件示例
// ============================================================================
// 使用 gzip 压缩(默认)
app.use(hono.gzip())
// 或者使用 deflate 压缩
// app.use(hono.deflate_compress())
// 自定义压缩配置
// app.use(hono.compress(hono.CompressOptions{
// encoding: .gzip
// threshold: 2048 // 只压缩大于 2KB 的响应
// level: 9 // 最高压缩级别
// }))
// ============================================================================
// 3. 安全响应头中间件示例
// ============================================================================
app.use(hono.secure_headers())
// ============================================================================
// 4. 请求 ID 中间件示例
// ============================================================================
app.use(hono.request_id())
// ============================================================================
// 5. 请求计时中间件示例
// ============================================================================
app.use(hono.timing())
// ============================================================================
// 6. 限流中间件示例
// ============================================================================
// 创建内存存储
store := hono.MemoryStore.new()
// 应用限流中间件:每分钟最多 100 个请求
app.use(hono.rate_limiter(hono.RateLimitOptions{
store: store
window_ms: 60000 // 1 分钟
limit: 100 // 最多 100 个请求
headers: true // 添加限流响应头
}))
// ============================================================================
// 路由示例
// ============================================================================
// 基本路由
app.get('/', fn (mut c hono.Context) http.Response {
return c.json('{"message": "Welcome to v-hono middleware demo!"}')
})
// 获取请求信息
app.get('/info', fn (mut c hono.Context) http.Response {
// 获取请求 ID
request_id := c.get('request_id') or { 'unknown' }
// 获取客户端 IP
client_ip := c.get_client_ip()
return c.json('{"request_id": "${request_id}", "client_ip": "${client_ip}"}')
})
// ============================================================================
// 7. Cookie Helper 示例
// ============================================================================
// 设置 Cookie
app.get('/cookie/set', fn (mut c hono.Context) http.Response {
// 设置普通 Cookie
hono.set_cookie(mut c, 'session_id', 'abc123', hono.CookieOptions{
http_only: true
secure: false // 开发环境设为 false
max_age: 3600 // 1 小时
path: '/'
})
return c.json('{"message": "Cookie set successfully"}')
})
// 获取 Cookie
app.get('/cookie/get', fn (mut c hono.Context) http.Response {
if session_id := hono.get_cookie(c, 'session_id') {
return c.json('{"session_id": "${session_id}"}')
}
return c.json('{"error": "Cookie not found"}')
})
// 获取所有 Cookie
app.get('/cookie/all', fn (mut c hono.Context) http.Response {
cookies := hono.get_all_cookies(c)
mut parts := []string{}
for name, value in cookies {
parts << '"${name}": "${value}"'
}
return c.json('{${parts.join(", ")}}')
})
// 删除 Cookie
app.get('/cookie/delete', fn (mut c hono.Context) http.Response {
hono.delete_cookie(mut c, 'session_id')
return c.json('{"message": "Cookie deleted"}')
})
// 签名 Cookie 示例
app.get('/cookie/signed/set', fn (mut c hono.Context) http.Response {
secret := 'my-secret-key-for-signing'
hono.set_signed_cookie(mut c, 'user_data', 'user123', secret) or {
c.status(500)
return c.json('{"error": "Failed to set signed cookie"}')
}
return c.json('{"message": "Signed cookie set successfully"}')
})
app.get('/cookie/signed/get', fn (mut c hono.Context) http.Response {
secret := 'my-secret-key-for-signing'
user_data := hono.get_signed_cookie(c, 'user_data', secret) or {
c.status(400)
return c.json('{"error": "Invalid or missing signed cookie"}')
}
return c.json('{"user_data": "${user_data}"}')
})
// ============================================================================
// 8. JWT 中间件示例
// ============================================================================
// 生成 JWT Token
app.post('/auth/login', fn (mut c hono.Context) http.Response {
// 在实际应用中,这里应该验证用户凭证
secret := 'my-jwt-secret-key'
// 创建 JWT payload
payload := hono.JwtPayload{
sub: 'user123'
iss: 'v-hono-demo'
exp: time.now().unix() + 3600 // 1 小时后过期
iat: time.now().unix()
claims: {
'role': 'admin'
'name': 'John Doe'
}
}
// 签名 JWT
token := hono.sign_jwt(payload, secret, .hs256) or {
c.status(500)
return c.json('{"error": "Failed to generate token"}')
}
return c.json('{"token": "${token}"}')
})
// 验证 JWT Token(手动验证示例)
app.get('/auth/verify', fn (mut c hono.Context) http.Response {
secret := 'my-jwt-secret-key'
// 从 Authorization 头获取 token
auth_header := c.req.header.get_custom('Authorization') or {
c.status(401)
return c.json('{"error": "Missing Authorization header"}')
}
if !auth_header.starts_with('Bearer ') {
c.status(401)
return c.json('{"error": "Invalid Authorization format"}')
}
token := auth_header[7..]
// 验证 token
payload := hono.verify_jwt(token, secret, .hs256) or {
c.status(401)
return c.json('{"error": "Invalid token: ${err}"}')
}
return c.json('{"sub": "${payload.sub}", "iss": "${payload.iss}"}')
})
// ============================================================================
// 9. Bearer Auth 中间件示例(保护特定路由)
// ============================================================================
// 创建受保护的子应用
mut protected_app := hono.Hono.new()
// 应用 Bearer Auth 中间件
protected_app.use(hono.bearer(hono.BearerAuthOptions{
token: 'my-api-token' // 简单的静态 token
realm: 'Protected API'
}))
protected_app.get('/data', fn (mut c hono.Context) http.Response {
// 获取已验证的 token
token := hono.get_bearer_token(c) or { 'unknown' }
return c.json('{"message": "Protected data", "token": "${token}"}')
})
// 挂载受保护的路由
app.route('/api', mut protected_app)
// ============================================================================
// 10. 请求验证示例
// ============================================================================
// JSON body 验证
app.post('/users',
hono.validate_json(hono.v_object({
'name': hono.v_string().required().min(2).max(50)
'email': hono.v_string().required().pattern(r'^[\w\.-]+@[\w\.-]+\.\w+$')
'age': hono.v_int().min(0).max(150)
})),
fn (mut c hono.Context) http.Response {
// 获取验证后的数据
data := hono.get_validated_data(c)
name := data['name'] or { '' }
email := data['email'] or { '' }
return c.json('{"message": "User created", "name": "${name}", "email": "${email}"}')
}
)
// Query 参数验证
app.get('/search',
hono.validate_query(hono.v_object({
'q': hono.v_string().required().min(1)
'page': hono.v_int().min(1)
'size': hono.v_int().min(1).max(100)
})),
fn (mut c hono.Context) http.Response {
q := hono.get_validated_field(c, 'q') or { '' }
page := hono.get_validated_field(c, 'page') or { '1' }
return c.json('{"query": "${q}", "page": ${page}}')
}
)
// ============================================================================
// 11. 中间件组合示例
// ============================================================================
// 组合多个中间件
combined := hono.combine_middlewares([
hono.cors_middleware(),
hono.secure_headers(),
hono.timing(),
])
// 创建使用组合中间件的子应用
mut combined_app := hono.Hono.new()
combined_app.use(combined)
combined_app.get('/test', fn (mut c hono.Context) http.Response {
duration := c.get('request_duration_ms') or { '0' }
return c.json('{"message": "Combined middleware test", "duration_ms": "${duration}"}')
})
app.route('/combined', mut combined_app)
// ============================================================================
// 启动服务器
// ============================================================================
println('=== v-hono Middleware Demo ===')
println('')
println('Available endpoints:')
println(' GET / - Welcome message')
println(' GET /info - Request info (ID, IP)')
println('')
println('Cookie endpoints:')
println(' GET /cookie/set - Set a cookie')
println(' GET /cookie/get - Get a cookie')
println(' GET /cookie/all - Get all cookies')
println(' GET /cookie/delete - Delete a cookie')
println(' GET /cookie/signed/set - Set a signed cookie')
println(' GET /cookie/signed/get - Get a signed cookie')
println('')
println('Auth endpoints:')
println(' POST /auth/login - Get JWT token')
println(' GET /auth/verify - Verify JWT token')
println(' GET /api/data - Protected endpoint (Bearer token: my-api-token)')
println('')
println('Validation endpoints:')
println(' POST /users - Create user (JSON body validation)')
println(' GET /search?q=xxx - Search (query validation)')
println('')
println('Combined middleware:')
println(' GET /combined/test - Test combined middlewares')
println('')
println('Starting server on http://localhost:3000')
println('')
app.listen(':3000')
}
+24
View File
@@ -0,0 +1,24 @@
module main
import net.http
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
app.get('/', fn (mut c hono.Context) http.Response {
return c.text('Hello from V-Hono + picoev!')
})
app.get('/json', fn (mut c hono.Context) http.Response {
return c.json('{"message": "Hello, JSON!"}')
})
app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id'] or { 'unknown' }
return c.json('{"user_id": "${user_id}"}')
})
// 使用默认 picoev 后端
app.listen(':3009')
}
+1 -1
View File
@@ -1,6 +1,6 @@
module main
import hono
import meiseayoung.hono
import net.http
fn main() {
+1 -1
View File
@@ -2,7 +2,7 @@
module main
import net.http
import hono
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
@@ -12,7 +12,7 @@
// ```
import net.http
import hono
import meiseayoung.hono
fn main() {
println('🚀 V-Hono 路由分组示例启动中...')
@@ -2,7 +2,7 @@
// 演示新功能:子应用中间件继承、all() 方法、notFound/onError 处理器
import net.http
import hono
import meiseayoung.hono
fn main() {
println('🚀 V-Hono 路由分组增强示例启动中...')
+1 -1
View File
@@ -4,7 +4,7 @@
module main
import hono
import meiseayoung.hono
import net.http
fn main() {
+1 -1
View File
@@ -4,7 +4,7 @@
module main
import hono
import meiseayoung.hono
import net.http
fn main() {
+554
View File
@@ -0,0 +1,554 @@
// SSE Streaming Demo - Server-Sent Events Example for v-hono
// This example demonstrates all SSE streaming helper functions:
// - stream() - Basic binary streaming
// - stream_text() - Text streaming
// - stream_sse() - Server-Sent Events streaming
//
// Run with: v -enable-globals run examples/sse_demo.v
// Then open a browser and navigate to http://127.0.0.1:3000
// Or use curl to test the endpoints:
// curl http://127.0.0.1:3000/stream
// curl http://127.0.0.1:3000/stream-text
// curl http://127.0.0.1:3000/sse
//
// Features demonstrated:
// - Basic binary streaming with stream()
// - Text streaming with stream_text()
// - SSE event streaming with stream_sse()
// - Error handling with custom error handlers
// - SSE event fields (data, event, id, retry)
// - Multi-line data in SSE events
// - Sleep/delay between stream writes
// - onAbort callback for client disconnection
module main
import net.http
import os
import hono
fn main() {
mut app := hono.Hono.new()
// =========================================================================
// Static HTML page for testing SSE
// =========================================================================
app.get('/', fn (mut c hono.Context) http.Response {
return c.html(get_sse_test_page())
})
// =========================================================================
// Basic Binary Streaming Example
// =========================================================================
// Basic stream - demonstrates binary data streaming
// Sets Transfer-Encoding: chunked header
app.get('/stream', fn (mut c hono.Context) http.Response {
return hono.c_stream(mut c, fn (mut stream hono.StreamContext) ! {
// Register abort callback for client disconnection
stream.on_abort(fn () {
println('[Stream] Client disconnected')
})
// Stream binary data in chunks
for i in 0 .. 5 {
data := 'Chunk ${i + 1}: Hello from binary stream!\n'
stream.write(data.bytes())!
stream.sleep(500) // 500ms delay between chunks
}
stream.write('Stream complete!\n'.bytes())!
})
})
// =========================================================================
// Text Streaming Example
// =========================================================================
// Text stream - demonstrates text data streaming
// Sets Content-Type: text/plain; charset=utf-8
// Sets Transfer-Encoding: chunked
// Sets X-Content-Type-Options: nosniff
app.get('/stream-text', fn (mut c hono.Context) http.Response {
return hono.c_stream_text(mut c, fn (mut stream hono.StreamContext) ! {
// Register abort callback
stream.on_abort(fn () {
println('[StreamText] Client disconnected')
})
// Stream text data with writeln (adds newline automatically)
stream.writeln('=== Text Streaming Demo ===')!
stream.sleep(300)
for i in 0 .. 5 {
stream.write_string('Processing item ${i + 1}... ')!
stream.sleep(200)
stream.writeln('Done!')!
stream.sleep(300)
}
stream.writeln('')!
stream.writeln('All items processed successfully!')!
})
})
// =========================================================================
// SSE (Server-Sent Events) Streaming Example
// =========================================================================
// SSE stream - demonstrates Server-Sent Events
// Sets Content-Type: text/event-stream
// Sets Cache-Control: no-cache
// Sets Connection: keep-alive
app.get('/sse', fn (mut c hono.Context) http.Response {
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
// Register abort callback
stream.on_abort(fn () {
println('[SSE] Client disconnected')
})
// Send initial connection event
stream.write_sse(hono.SSEEvent{
data: 'Connected to SSE stream'
event: 'connect'
id: '0'
})!
// Send periodic updates
for i in 1 .. 6 {
stream.sleep(1000) // 1 second delay
stream.write_sse(hono.SSEEvent{
data: 'Update ${i}: Current time is ${get_timestamp()}'
event: 'update'
id: '${i}'
})!
}
// Send completion event
stream.write_sse(hono.SSEEvent{
data: 'Stream completed'
event: 'complete'
id: '999'
})!
})
})
// =========================================================================
// SSE with Multi-line Data Example
// =========================================================================
// SSE with multi-line data - demonstrates multi-line data handling
app.get('/sse-multiline', fn (mut c hono.Context) http.Response {
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
// Send event with multi-line data
stream.write_sse(hono.SSEEvent{
data: 'Line 1: First line of data\nLine 2: Second line of data\nLine 3: Third line of data'
event: 'multiline'
id: '1'
})!
stream.sleep(1000)
// Send JSON data (multi-line formatted)
json_data := '{\n "name": "v-hono",\n "version": "1.0.0",\n "features": ["SSE", "WebSocket", "Streaming"]\n}'
stream.write_sse(hono.SSEEvent{
data: json_data
event: 'json'
id: '2'
})!
})
})
// =========================================================================
// SSE with Retry Field Example
// =========================================================================
// SSE with retry - demonstrates retry field for reconnection
app.get('/sse-retry', fn (mut c hono.Context) http.Response {
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
// Send event with retry field (tells client to reconnect after 3 seconds)
stream.write_sse(hono.SSEEvent{
data: 'This event includes a retry field'
event: 'message'
id: '1'
retry: 3000 // 3 seconds
})!
stream.sleep(1000)
// Send another event without retry
stream.write_sse(hono.SSEEvent{
data: 'This event does not include retry'
event: 'message'
id: '2'
})!
})
})
// =========================================================================
// SSE with Error Handling Example
// =========================================================================
// SSE with error handling - demonstrates custom error handler
app.get('/sse-error', fn (mut c hono.Context) http.Response {
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
// Send initial event
stream.write_sse(hono.SSEEvent{
data: 'Starting stream with potential error...'
event: 'start'
id: '1'
})!
stream.sleep(1000)
// Simulate an error condition
// In real applications, this could be a database error, API failure, etc.
should_error := true
if should_error {
return error('Simulated error for demonstration')
}
// This won't be reached due to the error above
stream.write_sse(hono.SSEEvent{
data: 'This message will not be sent'
event: 'message'
id: '2'
})!
}, fn (err IError, mut stream hono.StreamContext) {
// Custom error handler - send error event to client
println('[SSE Error Handler] Error occurred: ${err.msg()}')
// Try to send error event to client before closing
stream.write_sse(hono.SSEEvent{
data: 'Error: ${err.msg()}'
event: 'error'
id: 'error'
}) or {
println('[SSE Error Handler] Failed to send error event: ${err}')
}
})
})
// =========================================================================
// Real-time Counter Example
// =========================================================================
// Real-time counter - demonstrates continuous SSE updates
app.get('/sse-counter', fn (mut c hono.Context) http.Response {
return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
mut counter := 0
// Send counter updates every 500ms for 20 iterations
for _ in 0 .. 20 {
if !stream.is_open() {
println('[Counter] Stream closed, stopping')
break
}
counter++
stream.write_sse(hono.SSEEvent{
data: '${counter}'
event: 'counter'
id: '${counter}'
})!
stream.sleep(500)
}
// Send completion event
stream.write_sse(hono.SSEEvent{
data: 'Counter finished at ${counter}'
event: 'complete'
id: 'done'
})!
})
})
// =========================================================================
// Pipe Example - Stream file content
// =========================================================================
// Pipe example - demonstrates piping data to stream
app.get('/stream-pipe', fn (mut c hono.Context) http.Response {
return hono.c_stream(mut c, fn (mut stream hono.StreamContext) ! {
// Create some sample data to pipe
sample_data := 'This is sample data that will be piped to the stream.\n'.repeat(5)
// Pipe the data in chunks
chunk_size := 50
mut offset := 0
for offset < sample_data.len {
end := if offset + chunk_size > sample_data.len { sample_data.len } else { offset + chunk_size }
chunk := sample_data[offset..end]
stream.pipe(chunk.bytes())!
offset = end
stream.sleep(100)
}
})
})
// =========================================================================
// REST API endpoints for demonstration
// =========================================================================
app.get('/api/status', fn (mut c hono.Context) http.Response {
return c.json('{"status":"running","streaming_endpoints":["/stream","/stream-text","/sse","/sse-multiline","/sse-retry","/sse-error","/sse-counter","/stream-pipe"]}')
})
// =========================================================================
// Start the server
// =========================================================================
println('============================================================')
println('SSE Streaming Demo Server')
println('============================================================')
println('')
println('Server starting on http://127.0.0.1:3000')
println('')
println('Streaming Endpoints:')
println(' http://127.0.0.1:3000/stream - Basic binary streaming')
println(' http://127.0.0.1:3000/stream-text - Text streaming')
println(' http://127.0.0.1:3000/sse - SSE event streaming')
println(' http://127.0.0.1:3000/sse-multiline - SSE with multi-line data')
println(' http://127.0.0.1:3000/sse-retry - SSE with retry field')
println(' http://127.0.0.1:3000/sse-error - SSE with error handling')
println(' http://127.0.0.1:3000/sse-counter - Real-time counter')
println(' http://127.0.0.1:3000/stream-pipe - Pipe data to stream')
println('')
println('HTTP Endpoints:')
println(' http://127.0.0.1:3000/ - Test page')
println(' http://127.0.0.1:3000/api/status - Server status')
println('')
println('Test with curl:')
println(' curl http://127.0.0.1:3000/stream')
println(' curl http://127.0.0.1:3000/stream-text')
println(' curl http://127.0.0.1:3000/sse')
println('')
println('============================================================')
app.listen(':3000')
}
// get_timestamp - Get current timestamp string
fn get_timestamp() string {
return '${os.execute("echo %TIME%").output.trim_space()}'
}
// get_sse_test_page - Returns HTML test page for SSE streaming
fn get_sse_test_page() string {
sq := "'"
return '<!DOCTYPE html>
<html>
<head>
<title>v-hono SSE Streaming Demo</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 1000px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
h1 { color: #667eea; margin-bottom: 10px; }
h2 { color: #333; font-size: 1.2em; margin: 20px 0 10px; }
.subtitle { color: #666; margin-bottom: 30px; }
.card { background: white; padding: 20px; margin: 15px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.endpoint { display: flex; align-items: center; gap: 10px; margin: 10px 0; flex-wrap: wrap; }
.endpoint-url { font-family: monospace; background: #f0f0f0; padding: 8px 12px; border-radius: 4px; flex: 1; min-width: 200px; }
button { padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; font-size: 14px; transition: background 0.2s; }
.btn-connect { background: #667eea; color: white; }
.btn-connect:hover { background: #5a6fd6; }
.btn-disconnect { background: #e74c3c; color: white; }
.btn-disconnect:hover { background: #c0392b; }
.btn-clear { background: #95a5a6; color: white; }
.btn-clear:hover { background: #7f8c8d; }
.status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.status-connected { background: #27ae60; color: white; }
.status-disconnected { background: #e74c3c; color: white; }
.status-connecting { background: #f39c12; color: white; }
.output { height: 200px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 13px; line-height: 1.5; }
.output:empty::before { content: "No messages yet..."; color: #666; }
.msg { margin: 2px 0; padding: 2px 0; }
.msg-event { color: #569cd6; }
.msg-data { color: #ce9178; }
.msg-id { color: #b5cea8; }
.msg-retry { color: #dcdcaa; }
.msg-system { color: #6a9955; font-style: italic; }
.msg-error { color: #f44747; }
.msg-text { color: #d4d4d4; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 20px; }
@media (max-width: 600px) { .grid { grid-template-columns: 1fr; } .endpoint { flex-direction: column; align-items: stretch; } }
</style>
</head>
<body>
<h1>v-hono SSE Streaming Demo</h1>
<p class="subtitle">Test Server-Sent Events and streaming responses</p>
<div class="grid">
<div class="card">
<h2>SSE Event Stream</h2>
<div class="endpoint">
<span class="endpoint-url">/sse</span>
<button class="btn-connect" onclick="connectSSE(${sq}/sse${sq}, ${sq}sse-output${sq}, ${sq}sse-status${sq})">Connect</button>
<button class="btn-disconnect" onclick="disconnectSSE(${sq}sse${sq})">Disconnect</button>
<span id="sse-status" class="status status-disconnected">Disconnected</span>
</div>
<div id="sse-output" class="output"></div>
<button class="btn-clear" onclick="clearOutput(${sq}sse-output${sq})">Clear</button>
</div>
<div class="card">
<h2>Multi-line SSE</h2>
<div class="endpoint">
<span class="endpoint-url">/sse-multiline</span>
<button class="btn-connect" onclick="connectSSE(${sq}/sse-multiline${sq}, ${sq}multiline-output${sq}, ${sq}multiline-status${sq})">Connect</button>
<button class="btn-disconnect" onclick="disconnectSSE(${sq}multiline${sq})">Disconnect</button>
<span id="multiline-status" class="status status-disconnected">Disconnected</span>
</div>
<div id="multiline-output" class="output"></div>
<button class="btn-clear" onclick="clearOutput(${sq}multiline-output${sq})">Clear</button>
</div>
<div class="card">
<h2>Real-time Counter</h2>
<div class="endpoint">
<span class="endpoint-url">/sse-counter</span>
<button class="btn-connect" onclick="connectSSE(${sq}/sse-counter${sq}, ${sq}counter-output${sq}, ${sq}counter-status${sq})">Connect</button>
<button class="btn-disconnect" onclick="disconnectSSE(${sq}counter${sq})">Disconnect</button>
<span id="counter-status" class="status status-disconnected">Disconnected</span>
</div>
<div id="counter-output" class="output"></div>
<button class="btn-clear" onclick="clearOutput(${sq}counter-output${sq})">Clear</button>
</div>
<div class="card">
<h2>Error Handling</h2>
<div class="endpoint">
<span class="endpoint-url">/sse-error</span>
<button class="btn-connect" onclick="connectSSE(${sq}/sse-error${sq}, ${sq}error-output${sq}, ${sq}error-status${sq})">Connect</button>
<button class="btn-disconnect" onclick="disconnectSSE(${sq}error${sq})">Disconnect</button>
<span id="error-status" class="status status-disconnected">Disconnected</span>
</div>
<div id="error-output" class="output"></div>
<button class="btn-clear" onclick="clearOutput(${sq}error-output${sq})">Clear</button>
</div>
<div class="card">
<h2>Text Stream</h2>
<div class="endpoint">
<span class="endpoint-url">/stream-text</span>
<button class="btn-connect" onclick="fetchStream(${sq}/stream-text${sq}, ${sq}text-output${sq}, ${sq}text-status${sq})">Fetch</button>
<span id="text-status" class="status status-disconnected">Ready</span>
</div>
<div id="text-output" class="output"></div>
<button class="btn-clear" onclick="clearOutput(${sq}text-output${sq})">Clear</button>
</div>
<div class="card">
<h2>Binary Stream</h2>
<div class="endpoint">
<span class="endpoint-url">/stream</span>
<button class="btn-connect" onclick="fetchStream(${sq}/stream${sq}, ${sq}binary-output${sq}, ${sq}binary-status${sq})">Fetch</button>
<span id="binary-status" class="status status-disconnected">Ready</span>
</div>
<div id="binary-output" class="output"></div>
<button class="btn-clear" onclick="clearOutput(${sq}binary-output${sq})">Clear</button>
</div>
</div>
<script>
var connections = {};
function log(outputId, msg, type) {
var output = document.getElementById(outputId);
var div = document.createElement(${sq}div${sq});
div.className = ${sq}msg msg-${sq} + (type || ${sq}text${sq});
div.textContent = msg;
output.appendChild(div);
output.scrollTop = output.scrollHeight;
}
function setStatus(statusId, status) {
var el = document.getElementById(statusId);
el.textContent = status;
el.className = ${sq}status status-${sq} + status.toLowerCase();
}
function clearOutput(outputId) {
document.getElementById(outputId).innerHTML = ${sq}${sq};
}
function connectSSE(url, outputId, statusId) {
var key = outputId.replace(${sq}-output${sq}, ${sq}${sq});
if (connections[key]) {
connections[key].close();
}
setStatus(statusId, ${sq}Connecting${sq});
log(outputId, ${sq}Connecting to ${sq} + url + ${sq}...${sq}, ${sq}system${sq});
var es = new EventSource(url);
connections[key] = es;
es.onopen = function() {
setStatus(statusId, ${sq}Connected${sq});
log(outputId, ${sq}Connected!${sq}, ${sq}system${sq});
};
es.onmessage = function(e) {
log(outputId, ${sq}data: ${sq} + e.data, ${sq}data${sq});
};
es.onerror = function() {
setStatus(statusId, ${sq}Disconnected${sq});
log(outputId, ${sq}Connection closed${sq}, ${sq}system${sq});
connections[key] = null;
};
// Listen for custom event types
[${sq}connect${sq}, ${sq}update${sq}, ${sq}complete${sq}, ${sq}multiline${sq}, ${sq}json${sq}, ${sq}message${sq}, ${sq}counter${sq}, ${sq}start${sq}, ${sq}error${sq}].forEach(function(type) {
es.addEventListener(type, function(e) {
log(outputId, ${sq}event: ${sq} + type, ${sq}event${sq});
log(outputId, ${sq}data: ${sq} + e.data, ${sq}data${sq});
if (e.lastEventId) log(outputId, ${sq}id: ${sq} + e.lastEventId, ${sq}id${sq});
});
});
}
function disconnectSSE(key) {
if (connections[key]) {
connections[key].close();
connections[key] = null;
setStatus(key + ${sq}-status${sq}, ${sq}Disconnected${sq});
log(key + ${sq}-output${sq}, ${sq}Disconnected by user${sq}, ${sq}system${sq});
}
}
async function fetchStream(url, outputId, statusId) {
setStatus(statusId, ${sq}Connecting${sq});
log(outputId, ${sq}Fetching ${sq} + url + ${sq}...${sq}, ${sq}system${sq});
try {
var response = await fetch(url);
var reader = response.body.getReader();
var decoder = new TextDecoder();
setStatus(statusId, ${sq}Connected${sq});
while (true) {
var result = await reader.read();
if (result.done) break;
var text = decoder.decode(result.value, {stream: true});
log(outputId, text, ${sq}text${sq});
}
setStatus(statusId, ${sq}Disconnected${sq});
log(outputId, ${sq}Stream complete${sq}, ${sq}system${sq});
} catch (err) {
setStatus(statusId, ${sq}Disconnected${sq});
log(outputId, ${sq}Error: ${sq} + err.message, ${sq}error${sq});
}
}
</script>
</body>
</html>'
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
import os
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
import os
import time
+24
View File
@@ -0,0 +1,24 @@
// 测试服务器 - 用于高并发测试
module main
import meiseayoung.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')
}
+24
View File
@@ -0,0 +1,24 @@
module main
import net.http
import meiseayoung.hono
fn main() {
mut app := hono.Hono.new()
app.get('/', fn (mut c hono.Context) http.Response {
return c.text('Hello from V-Hono + uSockets!')
})
app.get('/json', fn (mut c hono.Context) http.Response {
return c.json('{"message": "Hello, JSON!"}')
})
app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id'] or { 'unknown' }
return c.json('{"user_id": "${user_id}"}')
})
// 使用 uSockets 后端
app.listen_usockets(3008)
}
+336
View File
@@ -0,0 +1,336 @@
// WebSocket Demo - Chat Room Example for v-hono
// This example demonstrates all WebSocket event handlers and configuration options.
//
// Run with: v run examples/websocket_demo.v
// Then open a browser and navigate to http://127.0.0.1:3000
// Or use a WebSocket client to connect to ws://127.0.0.1:3000/ws/chat/general
//
// Features demonstrated:
// - WebSocket upgrade handling
// - Event handlers (on_open, on_message, on_close, on_error)
// - Route parameters (/ws/chat/:room)
// - Query parameters (?username=xxx)
// - Subprotocol negotiation
// - Custom configuration options (ping interval, max message size, timeout)
// - JSON message handling
// - Binary message handling
// - Middleware integration with WebSocket routes
module main
import net.http
import time
import os
import hono
fn main() {
mut app := hono.Hono.new()
// =========================================================================
// Middleware - Runs before WebSocket upgrade
// =========================================================================
// Logger middleware - logs all requests including WebSocket upgrades
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
start := time.now()
response := next(mut c)
duration := time.since(start)
println('[${time.now().format_ss()}] ${c.req.method} ${c.path} - ${response.status_code} (${duration})')
return response
})
// =========================================================================
// Static HTML page for testing WebSocket
// =========================================================================
app.get('/', fn (mut c hono.Context) http.Response {
// Try to load the HTML file from the examples directory
html_content := os.read_file('examples/websocket_test.html') or {
// Fallback to a simple HTML page if file not found
return c.html(get_simple_test_page())
}
return c.html(html_content)
})
// =========================================================================
// Basic WebSocket Echo Example
// =========================================================================
// Simple echo WebSocket - echoes back any message received
app.ws('/ws/echo', fn (c hono.Context) hono.WSEvents {
return hono.WSEvents{
on_open: fn (mut ws hono.WSContext) {
println('[Echo] Client connected')
ws.send('Welcome to the echo server!') or {
println('[Echo] Failed to send welcome: ${err}')
}
}
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
if event.is_binary {
println('[Echo] Received binary message: ${event.data_bytes.len} bytes')
ws.send_bytes(event.data_bytes) or {
println('[Echo] Failed to echo binary: ${err}')
}
} else {
println('[Echo] Received: ${event.data}')
ws.send('Echo: ${event.data}') or {
println('[Echo] Failed to echo: ${err}')
}
}
}
on_close: fn (event hono.WSCloseEvent, mut ws hono.WSContext) {
println('[Echo] Client disconnected - Code: ${event.code}, Reason: ${event.reason}, Clean: ${event.was_clean}')
}
on_error: fn (error string, mut ws hono.WSContext) {
println('[Echo] Error: ${error}')
}
}
})
// =========================================================================
// Chat Room WebSocket with Route Parameters
// =========================================================================
// Chat room WebSocket - demonstrates route parameters and query parameters
// URL format: /ws/chat/:room?username=xxx
app.ws('/ws/chat/:room', fn (c hono.Context) hono.WSEvents {
// Access route parameters from the HTTP context
room := c.params['room'] or { 'general' }
username := c.query['username'] or { 'anonymous' }
println('[Chat] User "${username}" joining room "${room}"')
return hono.WSEvents{
on_open: fn [room, username] (mut ws hono.WSContext) {
// Access preserved context data
actual_room := ws.params['room'] or { room }
actual_user := ws.query['username'] or { username }
println('[Chat/${actual_room}] ${actual_user} connected')
// Send welcome message as JSON
welcome := '{"type":"system","message":"Welcome to room ${actual_room}, ${actual_user}!"}'
ws.send_json(welcome) or {
println('[Chat] Failed to send welcome: ${err}')
}
// Broadcast join notification
join_msg := '{"type":"join","user":"${actual_user}","room":"${actual_room}"}'
ws.send_json(join_msg) or {}
}
on_message: fn [room, username] (event hono.WSMessageEvent, mut ws hono.WSContext) {
actual_room := ws.params['room'] or { room }
actual_user := ws.query['username'] or { username }
println('[Chat/${actual_room}] ${actual_user}: ${event.data}')
// Echo the message back with metadata
response := '{"type":"message","user":"${actual_user}","room":"${actual_room}","content":"${event.data}"}'
ws.send_json(response) or {
println('[Chat] Failed to send response: ${err}')
}
}
on_close: fn [room, username] (event hono.WSCloseEvent, mut ws hono.WSContext) {
actual_room := ws.params['room'] or { room }
actual_user := ws.query['username'] or { username }
println('[Chat/${actual_room}] ${actual_user} disconnected (code: ${event.code})')
}
on_error: fn [room, username] (error string, mut ws hono.WSContext) {
actual_room := ws.params['room'] or { room }
actual_user := ws.query['username'] or { username }
println('[Chat/${actual_room}] Error for ${actual_user}: ${error}')
}
}
})
// =========================================================================
// WebSocket with Custom Configuration Options
// =========================================================================
// Configured WebSocket - demonstrates custom options
app.ws('/ws/configured', fn (c hono.Context) hono.WSEvents {
return hono.WSEvents{
on_open: fn (mut ws hono.WSContext) {
println('[Configured] Client connected with custom settings')
ws.send('Connected with custom configuration!') or {}
}
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
println('[Configured] Message: ${event.data}')
ws.send('Received: ${event.data}') or {}
}
on_close: fn (event hono.WSCloseEvent, mut ws hono.WSContext) {
println('[Configured] Disconnected')
}
on_error: fn (error string, mut ws hono.WSContext) {
println('[Configured] Error: ${error}')
}
}
}, hono.WebSocketOptions{
ping_interval: 15000 // Send ping every 15 seconds
max_message_size: 65536 // Max 64KB messages
timeout: 30000 // 30 second timeout
protocols: ['chat', 'json'] // Supported subprotocols
})
// =========================================================================
// WebSocket with Subprotocol Negotiation
// =========================================================================
// Subprotocol WebSocket - demonstrates protocol negotiation
app.ws('/ws/protocol', fn (c hono.Context) hono.WSEvents {
return hono.WSEvents{
on_open: fn (mut ws hono.WSContext) {
protocol := ws.protocol
println('[Protocol] Client connected with protocol: ${protocol}')
if protocol == 'json' {
ws.send_json('{"status":"connected","protocol":"json"}') or {}
} else {
ws.send('Connected with protocol: ${protocol}') or {}
}
}
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
protocol := ws.protocol
println('[Protocol] Message (${protocol}): ${event.data}')
if protocol == 'json' {
ws.send_json('{"echo":"${event.data}"}') or {}
} else {
ws.send('Echo: ${event.data}') or {}
}
}
on_close: fn (event hono.WSCloseEvent, mut ws hono.WSContext) {
println('[Protocol] Disconnected')
}
on_error: fn (error string, mut ws hono.WSContext) {
println('[Protocol] Error: ${error}')
}
}
}, hono.WebSocketOptions{
protocols: ['json', 'text', 'binary']
})
// =========================================================================
// Binary Data WebSocket
// =========================================================================
// Binary WebSocket - demonstrates binary message handling
app.ws('/ws/binary', fn (c hono.Context) hono.WSEvents {
return hono.WSEvents{
on_open: fn (mut ws hono.WSContext) {
println('[Binary] Client connected')
ws.send('Binary WebSocket ready. Send binary data!') or {}
}
on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
if event.is_binary {
println('[Binary] Received ${event.data_bytes.len} bytes')
// Echo back the binary data
ws.send_bytes(event.data_bytes) or {
println('[Binary] Failed to echo: ${err}')
}
// Also send a text confirmation
ws.send('Received ${event.data_bytes.len} bytes of binary data') or {}
} else {
println('[Binary] Received text: ${event.data}')
ws.send('Please send binary data, not text!') or {}
}
}
on_close: fn (event hono.WSCloseEvent, mut ws hono.WSContext) {
println('[Binary] Disconnected')
}
on_error: fn (error string, mut ws hono.WSContext) {
println('[Binary] Error: ${error}')
}
}
})
// =========================================================================
// REST API endpoints for demonstration
// =========================================================================
app.get('/api/rooms', fn (mut c hono.Context) http.Response {
return c.json('{"rooms":["general","tech","random"]}')
})
app.get('/api/status', fn (mut c hono.Context) http.Response {
return c.json('{"status":"running","websocket_endpoints":["/ws/echo","/ws/chat/:room","/ws/configured","/ws/protocol","/ws/binary"]}')
})
// =========================================================================
// Start the server
// =========================================================================
println('============================================================')
println('WebSocket Demo Server')
println('============================================================')
println('')
println('Server starting on http://127.0.0.1:3000')
println('')
println('WebSocket Endpoints:')
println(' ws://127.0.0.1:3000/ws/echo - Simple echo server')
println(' ws://127.0.0.1:3000/ws/chat/:room - Chat room with route params')
println(' ws://127.0.0.1:3000/ws/configured - Custom configuration demo')
println(' ws://127.0.0.1:3000/ws/protocol - Subprotocol negotiation')
println(' ws://127.0.0.1:3000/ws/binary - Binary data handling')
println('')
println('HTTP Endpoints:')
println(' http://127.0.0.1:3000/ - Test page')
println(' http://127.0.0.1:3000/api/rooms - List rooms')
println(' http://127.0.0.1:3000/api/status - Server status')
println('')
println('============================================================')
app.listen(':3000')
}
// Simple fallback test page when HTML file is not found
fn get_simple_test_page() string {
return '<!DOCTYPE html>
<html>
<head>
<title>v-hono WebSocket Demo</title>
<style>
body{font-family:sans-serif;max-width:800px;margin:50px auto;padding:20px}
h1{color:#667eea}
.card{background:#f5f5f5;padding:20px;margin:20px 0;border-radius:8px}
input,button{padding:10px;margin:5px;border-radius:4px;border:1px solid #ccc}
button{background:#667eea;color:white;border:none;cursor:pointer}
button:hover{background:#5a6fd6}
#messages{height:200px;overflow-y:auto;background:white;padding:10px;border:1px solid #ccc;border-radius:4px}
.msg{padding:5px;margin:2px 0;border-radius:4px}
.sent{background:#e3f2fd}
.received{background:#e8f5e9}
.system{background:#fff3e0}
</style>
</head>
<body>
<h1>v-hono WebSocket Demo</h1>
<div class="card">
<h3>Connection</h3>
<input type="text" id="url" value="ws://127.0.0.1:3000/ws/echo" style="width:300px">
<button onclick="connect()">Connect</button>
<button onclick="disconnect()">Disconnect</button>
<span id="status">Disconnected</span>
</div>
<div class="card">
<h3>Send Message</h3>
<input type="text" id="msg" placeholder="Type message..." style="width:300px">
<button onclick="send()">Send</button>
</div>
<div class="card">
<h3>Messages</h3>
<div id="messages"></div>
</div>
<script>
var ws=null;
function log(t,c){var m=document.getElementById("messages");m.innerHTML+="<div class=msg "+c+">"+t+"</div>";m.scrollTop=m.scrollHeight}
function connect(){if(ws)return;ws=new WebSocket(document.getElementById("url").value);ws.onopen=function(){document.getElementById("status").textContent="Connected";log("Connected","system")};ws.onmessage=function(e){log("Received: "+e.data,"received")};ws.onclose=function(){document.getElementById("status").textContent="Disconnected";log("Disconnected","system");ws=null};ws.onerror=function(){log("Error","system")}}
function disconnect(){if(ws)ws.close()}
function send(){if(!ws)return;var m=document.getElementById("msg").value;ws.send(m);log("Sent: "+m,"sent");document.getElementById("msg").value=""}
</script>
</body>
</html>'
}
+294
View File
@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>v-hono WebSocket Demo</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 {
color: white;
text-align: center;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.card {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.card h2 {
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #667eea;
}
.form-group { margin-bottom: 15px; }
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #555;
}
input, select {
width: 100%;
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.btn-group { display: flex; gap: 10px; flex-wrap: wrap; }
button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
button:active { transform: translateY(0); }
.btn-connect { background: #667eea; color: white; }
.btn-disconnect { background: #e74c3c; color: white; }
.btn-send { background: #27ae60; color: white; }
.btn-clear { background: #95a5a6; color: white; }
.status {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.status-disconnected { background: #fee2e2; color: #dc2626; }
.status-connecting { background: #fef3c7; color: #d97706; }
.status-connected { background: #d1fae5; color: #059669; }
#messages {
height: 300px;
overflow-y: auto;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
background: #f8f9fa;
font-family: "Monaco", "Menlo", monospace;
font-size: 13px;
}
.message {
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-sent { background: #e3f2fd; border-left: 4px solid #2196f3; }
.message-received { background: #e8f5e9; border-left: 4px solid #4caf50; }
.message-system { background: #fff3e0; border-left: 4px solid #ff9800; }
.message-error { background: #ffebee; border-left: 4px solid #f44336; }
.message-time { color: #999; font-size: 11px; }
.endpoints {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 15px;
}
.endpoint {
padding: 10px;
background: #f0f4ff;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.endpoint:hover { background: #e0e7ff; }
.endpoint code {
display: block;
font-size: 12px;
color: #667eea;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔌 v-hono WebSocket Demo</h1>
<div class="card">
<h2>Connection <span id="status" class="status status-disconnected">Disconnected</span></h2>
<div class="form-group">
<label>WebSocket URL</label>
<input type="text" id="wsUrl" value="ws://127.0.0.1:3000/ws/echo" placeholder="ws://...">
</div>
<div class="form-group">
<label>Quick Connect</label>
<div class="endpoints">
<div class="endpoint" onclick="setUrl('ws://127.0.0.1:3000/ws/echo')">
<strong>Echo Server</strong>
<code>/ws/echo</code>
</div>
<div class="endpoint" onclick="setUrl('ws://127.0.0.1:3000/ws/chat/general?username=guest')">
<strong>Chat Room</strong>
<code>/ws/chat/:room</code>
</div>
<div class="endpoint" onclick="setUrl('ws://127.0.0.1:3000/ws/configured')">
<strong>Configured</strong>
<code>/ws/configured</code>
</div>
<div class="endpoint" onclick="setUrl('ws://127.0.0.1:3000/ws/protocol')">
<strong>Protocol</strong>
<code>/ws/protocol</code>
</div>
</div>
</div>
<div class="btn-group">
<button class="btn-connect" onclick="connect()">Connect</button>
<button class="btn-disconnect" onclick="disconnect()">Disconnect</button>
</div>
</div>
<div class="card">
<h2>Send Message</h2>
<div class="form-group">
<label>Message</label>
<input type="text" id="message" placeholder="Type your message..." onkeypress="if(event.key==='Enter')sendMessage()">
</div>
<div class="btn-group">
<button class="btn-send" onclick="sendMessage()">Send Text</button>
<button class="btn-send" onclick="sendJson()">Send JSON</button>
</div>
</div>
<div class="card">
<h2>Messages <button class="btn-clear" onclick="clearMessages()" style="float:right;padding:6px 12px;">Clear</button></h2>
<div id="messages"></div>
</div>
</div>
<script>
let ws = null;
function setUrl(url) {
document.getElementById('wsUrl').value = url;
}
function updateStatus(status) {
const el = document.getElementById('status');
el.textContent = status;
el.className = 'status status-' + status.toLowerCase();
}
function addMessage(type, text) {
const messages = document.getElementById('messages');
const time = new Date().toLocaleTimeString();
const div = document.createElement('div');
div.className = 'message message-' + type;
div.innerHTML = '<span class="message-time">[' + time + ']</span> ' + text;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function connect() {
if (ws && ws.readyState === WebSocket.OPEN) {
addMessage('system', 'Already connected!');
return;
}
const url = document.getElementById('wsUrl').value;
updateStatus('Connecting');
addMessage('system', 'Connecting to ' + url + '...');
try {
ws = new WebSocket(url);
ws.onopen = function() {
updateStatus('Connected');
addMessage('system', 'Connected successfully!');
};
ws.onmessage = function(event) {
addMessage('received', event.data);
};
ws.onclose = function(event) {
updateStatus('Disconnected');
addMessage('system', 'Disconnected (code: ' + event.code + ', reason: ' + (event.reason || 'none') + ')');
ws = null;
};
ws.onerror = function(error) {
addMessage('error', 'WebSocket error occurred');
};
} catch (e) {
addMessage('error', 'Failed to connect: ' + e.message);
updateStatus('Disconnected');
}
}
function disconnect() {
if (ws) {
ws.close(1000, 'User requested disconnect');
addMessage('system', 'Disconnecting...');
} else {
addMessage('system', 'Not connected');
}
}
function sendMessage() {
const input = document.getElementById('message');
const text = input.value.trim();
if (!text) {
addMessage('error', 'Please enter a message');
return;
}
if (!ws || ws.readyState !== WebSocket.OPEN) {
addMessage('error', 'Not connected!');
return;
}
ws.send(text);
addMessage('sent', text);
input.value = '';
}
function sendJson() {
const input = document.getElementById('message');
const text = input.value.trim() || 'Hello';
if (!ws || ws.readyState !== WebSocket.OPEN) {
addMessage('error', 'Not connected!');
return;
}
const json = JSON.stringify({ type: 'message', content: text, timestamp: Date.now() });
ws.send(json);
addMessage('sent', json);
input.value = '';
}
function clearMessages() {
document.getElementById('messages').innerHTML = '';
}
</script>
</body>
</html>
+25 -10
View File
@@ -27,10 +27,12 @@ pub mut:
static_routes map[string]IHandler
static_route_results map[string]ContextRouteMatch // 预分配的静态路由结果
precompiled_routes []PrecompiledRoute
lru_cache ContextLRUCache // 使用 LRU 缓存替代简单 map
fast_cache FastRouteCache // 使用高性能缓存替代 LRU
lru_cache ContextLRUCache // 保留 LRU 缓存用于兼容
regex_cache map[string]FastCompiledRegex // 正则表达式缓存
cache_enabled bool = true
sort_enabled bool = true // 是否启用路由排序
use_fast_cache bool = true // 是否使用高性能缓存
}
// 创建快速路由器
@@ -39,8 +41,10 @@ pub fn FastRouter.new() FastRouter {
static_routes: map[string]IHandler{}
static_route_results: map[string]ContextRouteMatch{}
precompiled_routes: []PrecompiledRoute{}
lru_cache: ContextLRUCache.new(1000) // 默认1000条缓存
fast_cache: FastRouteCache.new(10000) // 高性能缓存,10000条
lru_cache: ContextLRUCache.new(1000) // LRU 缓存保留兼容
regex_cache: map[string]FastCompiledRegex{}
use_fast_cache: true
}
}
@@ -50,8 +54,10 @@ pub fn FastRouter.new_with_cache_size(cache_size int) FastRouter {
static_routes: map[string]IHandler{}
static_route_results: map[string]ContextRouteMatch{}
precompiled_routes: []PrecompiledRoute{}
fast_cache: FastRouteCache.new(cache_size * 10) // 高性能缓存
lru_cache: ContextLRUCache.new(cache_size)
regex_cache: map[string]FastCompiledRegex{}
use_fast_cache: true
}
}
@@ -171,17 +177,23 @@ fn (router FastRouter) precompile_route(method string, handler IHandler) !Precom
}
}
// 快速路由匹配(优化版:静态路由跳过缓存 + 预分配结果)
// 快速路由匹配(优化版:静态路由跳过缓存 + 预分配结果 + 高性能缓存)
pub fn (mut router FastRouter) match_route(method string, path string) ?ContextRouteMatch {
// 优化:只拼接一次 cache key,复用于静态路由和缓存查找
cache_key := '${method}:${path}'
// 1. 静态路由直接返回预分配的结果(零分配)
static_key := '${method}:${path}'
if result := router.static_route_results[static_key] {
if result := router.static_route_results[cache_key] {
return result
}
// 2. 动态路由先检查 LRU 缓存(避免重复正则匹配)
if router.cache_enabled {
cache_key := '${method}:${path}'
// 2. 动态路由先检查高性能缓存(零开销查找)
if router.cache_enabled && router.use_fast_cache {
if cached := router.fast_cache.get(cache_key) {
return cached
}
} else if router.cache_enabled {
// 回退到 LRU 缓存
if cached := router.lru_cache.get(cache_key) {
return cached
}
@@ -208,11 +220,14 @@ pub fn (mut router FastRouter) match_route(method string, path string) ?ContextR
base_path: ''
}
// 只缓存动态路由结果
// 缓存动态路由结果
if router.cache_enabled {
cache_key := '${method}:${path}'
if router.use_fast_cache {
router.fast_cache.put(cache_key, result)
} else {
router.lru_cache.put(cache_key, result)
}
}
return result
}
+600
View File
@@ -0,0 +1,600 @@
module hono
import crypto.sha256
import crypto.sha512
import encoding.base64
import net.http
import time
import x.json2
// JwtAlgorithm 枚举 - 支持的 JWT 签名算法
pub enum JwtAlgorithm {
hs256
hs384
hs512
}
// JwtVerifyOptions 结构体 - JWT 验证选项
pub struct JwtVerifyOptions {
pub:
iss string // 发行者验证
exp bool = true // 验证过期时间
nbf bool = true // 验证生效时间
iat bool = true // 验证签发时间
}
// JwtOptions 结构体 - JWT 中间件配置选项
pub struct JwtOptions {
pub:
secret string // 密钥(必需)
alg JwtAlgorithm = .hs256 // 算法
cookie string // 从 cookie 读取 token
header_name string = 'Authorization' // 请求头名称
verify_options JwtVerifyOptions = JwtVerifyOptions{}
}
// JwtPayload 结构体 - JWT 载荷
pub struct JwtPayload {
pub mut:
sub string // Subject
iss string // Issuer
aud string // Audience
exp i64 // Expiration Time (Unix timestamp)
nbf i64 // Not Before (Unix timestamp)
iat i64 // Issued At (Unix timestamp)
jti string // JWT ID
claims map[string]string // 自定义声明
}
// JwtHeader 结构体 - JWT 头部
struct JwtHeader {
alg string
typ string
}
// JwtToken 结构体 - 完整的 JWT Token
struct JwtToken {
header JwtHeader
payload JwtPayload
signature string
}
// base64url_encode - Base64URL 编码(不带填充)
fn base64url_encode(data []u8) string {
encoded := base64.encode(data)
// 转换为 URL 安全格式并移除填充
return encoded.replace('+', '-').replace('/', '_').trim_right('=')
}
// base64url_decode - Base64URL 解码
fn base64url_decode(data string) ![]u8 {
// 转换回标准 Base64 格式
mut standard := data.replace('-', '+').replace('_', '/')
// 添加填充
padding := (4 - standard.len % 4) % 4
for _ in 0 .. padding {
standard += '='
}
return base64.decode(standard)
}
// hmac_sha256 - HMAC-SHA256 签名
fn hmac_sha256(message []u8, secret []u8) []u8 {
block_size := 64 // SHA256 块大小
// 处理密钥
mut key := secret.clone()
if key.len > block_size {
// 如果密钥太长,先哈希
hash_result := sha256.sum(key)
key = []u8{len: 32}
for i := 0; i < 32; i++ {
key[i] = hash_result[i]
}
}
// 填充到块大小
for key.len < block_size {
key << u8(0)
}
// 计算 K' ⊕ ipad
mut i_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
i_key_pad[i] = key[i] ^ u8(0x36)
}
// 计算 K' ⊕ opad
mut o_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
o_key_pad[i] = key[i] ^ u8(0x5c)
}
// 计算内部哈希: H((K' ⊕ ipad) || m)
mut inner_data := i_key_pad.clone()
inner_data << message
inner_hash_result := sha256.sum(inner_data)
mut inner_hash := []u8{len: 32}
for i := 0; i < 32; i++ {
inner_hash[i] = inner_hash_result[i]
}
// 计算外部哈希: H((K' ⊕ opad) || inner_hash)
mut outer_data := o_key_pad.clone()
outer_data << inner_hash
outer_hash_result := sha256.sum(outer_data)
mut result := []u8{len: 32}
for i := 0; i < 32; i++ {
result[i] = outer_hash_result[i]
}
return result
}
// hmac_sha384 - HMAC-SHA384 签名
fn hmac_sha384(message []u8, secret []u8) []u8 {
block_size := 128 // SHA384 块大小
// 处理密钥
mut key := secret.clone()
if key.len > block_size {
// 如果密钥太长,先哈希
hash_result := sha512.sum384(key)
key = []u8{len: 48}
for i := 0; i < 48; i++ {
key[i] = hash_result[i]
}
}
// 填充到块大小
for key.len < block_size {
key << u8(0)
}
// 计算 K' ⊕ ipad
mut i_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
i_key_pad[i] = key[i] ^ u8(0x36)
}
// 计算 K' ⊕ opad
mut o_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
o_key_pad[i] = key[i] ^ u8(0x5c)
}
// 计算内部哈希
mut inner_data := i_key_pad.clone()
inner_data << message
inner_hash_result := sha512.sum384(inner_data)
mut inner_hash := []u8{len: 48}
for i := 0; i < 48; i++ {
inner_hash[i] = inner_hash_result[i]
}
// 计算外部哈希
mut outer_data := o_key_pad.clone()
outer_data << inner_hash
outer_hash_result := sha512.sum384(outer_data)
mut result := []u8{len: 48}
for i := 0; i < 48; i++ {
result[i] = outer_hash_result[i]
}
return result
}
// hmac_sha512 - HMAC-SHA512 签名
fn hmac_sha512(message []u8, secret []u8) []u8 {
block_size := 128 // SHA512 块大小
// 处理密钥
mut key := secret.clone()
if key.len > block_size {
// 如果密钥太长,先哈希
hash_result := sha512.sum512(key)
key = []u8{len: 64}
for i := 0; i < 64; i++ {
key[i] = hash_result[i]
}
}
// 填充到块大小
for key.len < block_size {
key << u8(0)
}
// 计算 K' ⊕ ipad
mut i_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
i_key_pad[i] = key[i] ^ u8(0x36)
}
// 计算 K' ⊕ opad
mut o_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
o_key_pad[i] = key[i] ^ u8(0x5c)
}
// 计算内部哈希
mut inner_data := i_key_pad.clone()
inner_data << message
inner_hash_result := sha512.sum512(inner_data)
mut inner_hash := []u8{len: 64}
for i := 0; i < 64; i++ {
inner_hash[i] = inner_hash_result[i]
}
// 计算外部哈希
mut outer_data := o_key_pad.clone()
outer_data << inner_hash
outer_hash_result := sha512.sum512(outer_data)
mut result := []u8{len: 64}
for i := 0; i < 64; i++ {
result[i] = outer_hash_result[i]
}
return result
}
// sign_message - 根据算法签名消息
fn sign_message(message []u8, secret []u8, alg JwtAlgorithm) []u8 {
return match alg {
.hs256 { hmac_sha256(message, secret) }
.hs384 { hmac_sha384(message, secret) }
.hs512 { hmac_sha512(message, secret) }
}
}
// alg_to_string - 将算法枚举转换为字符串
fn alg_to_string(alg JwtAlgorithm) string {
return match alg {
.hs256 { 'HS256' }
.hs384 { 'HS384' }
.hs512 { 'HS512' }
}
}
// string_to_alg - 将字符串转换为算法枚举
fn string_to_alg(s string) !JwtAlgorithm {
return match s {
'HS256' { JwtAlgorithm.hs256 }
'HS384' { JwtAlgorithm.hs384 }
'HS512' { JwtAlgorithm.hs512 }
else { error('Unsupported algorithm: ${s}') }
}
}
// sign_jwt - 创建并签名 JWT token
// 返回格式: header.payload.signature
pub fn sign_jwt(payload JwtPayload, secret string, alg JwtAlgorithm) !string {
if secret.len == 0 {
return error('Secret is required')
}
// 构建 header
header := JwtHeader{
alg: alg_to_string(alg)
typ: 'JWT'
}
// 编码 header
header_json := '{"alg":"${header.alg}","typ":"${header.typ}"}'
header_encoded := base64url_encode(header_json.bytes())
// 编码 payload
payload_json := encode_payload(payload)
payload_encoded := base64url_encode(payload_json.bytes())
// 创建签名输入
signing_input := '${header_encoded}.${payload_encoded}'
// 签名
signature := sign_message(signing_input.bytes(), secret.bytes(), alg)
signature_encoded := base64url_encode(signature)
return '${signing_input}.${signature_encoded}'
}
// encode_payload - 将 JwtPayload 编码为 JSON 字符串
fn encode_payload(payload JwtPayload) string {
mut parts := []string{}
if payload.sub.len > 0 {
parts << '"sub":"${escape_json_string(payload.sub)}"'
}
if payload.iss.len > 0 {
parts << '"iss":"${escape_json_string(payload.iss)}"'
}
if payload.aud.len > 0 {
parts << '"aud":"${escape_json_string(payload.aud)}"'
}
if payload.exp != 0 {
parts << '"exp":${payload.exp}'
}
if payload.nbf != 0 {
parts << '"nbf":${payload.nbf}'
}
if payload.iat != 0 {
parts << '"iat":${payload.iat}'
}
if payload.jti.len > 0 {
parts << '"jti":"${escape_json_string(payload.jti)}"'
}
// 添加自定义声明
for key, value in payload.claims {
parts << '"${escape_json_string(key)}":"${escape_json_string(value)}"'
}
return '{${parts.join(",")}}'
}
// escape_json_string - 转义 JSON 字符串中的特殊字符
fn escape_json_string(s string) string {
mut result := []u8{}
for c in s.bytes() {
match c {
`"` { result << `\\`; result << `"` }
`\\` { result << `\\`; result << `\\` }
`\n` { result << `\\`; result << `n` }
`\r` { result << `\\`; result << `r` }
`\t` { result << `\\`; result << `t` }
else { result << c }
}
}
return result.bytestr()
}
// decode_jwt - 解码 JWT token(不验证签名)
// 返回解码后的 payload
pub fn decode_jwt(token string) !JwtPayload {
parts := token.split('.')
if parts.len != 3 {
return error('Invalid token format')
}
// 解码 payload
payload_bytes := base64url_decode(parts[1])!
payload_json := payload_bytes.bytestr()
return parse_payload(payload_json)
}
// parse_payload - 解析 JSON payload 为 JwtPayload 结构体
fn parse_payload(json_str string) !JwtPayload {
raw := json2.decode[json2.Any](json_str) or {
return error('Invalid JSON payload: ${err}')
}
obj := raw.as_map()
mut payload := JwtPayload{
claims: map[string]string{}
}
// 解析标准声明
if 'sub' in obj {
payload.sub = obj['sub'] or { json2.Any('') }.str()
}
if 'iss' in obj {
payload.iss = obj['iss'] or { json2.Any('') }.str()
}
if 'aud' in obj {
payload.aud = obj['aud'] or { json2.Any('') }.str()
}
if 'exp' in obj {
payload.exp = (obj['exp'] or { json2.Any(0) }).i64()
}
if 'nbf' in obj {
payload.nbf = (obj['nbf'] or { json2.Any(0) }).i64()
}
if 'iat' in obj {
payload.iat = (obj['iat'] or { json2.Any(0) }).i64()
}
if 'jti' in obj {
payload.jti = obj['jti'] or { json2.Any('') }.str()
}
// 解析自定义声明
standard_claims := ['sub', 'iss', 'aud', 'exp', 'nbf', 'iat', 'jti']
for key, value in obj {
if key !in standard_claims {
payload.claims[key] = value.str()
}
}
return payload
}
// verify_jwt - 验证 JWT token 并返回 payload
// 验证签名、过期时间、生效时间等
pub fn verify_jwt(token string, secret string, alg JwtAlgorithm) !JwtPayload {
if secret.len == 0 {
return error('Secret is required')
}
parts := token.split('.')
if parts.len != 3 {
return error('Invalid token format')
}
// 解码 header 并验证算法
header_bytes := base64url_decode(parts[0])!
header_json := header_bytes.bytestr()
header_raw := json2.decode[json2.Any](header_json) or {
return error('Invalid header JSON: ${err}')
}
header_obj := header_raw.as_map()
token_alg_str := (header_obj['alg'] or { json2.Any('') }).str()
token_alg := string_to_alg(token_alg_str)!
// 验证算法一致性
if token_alg != alg {
return error('Algorithm mismatch: expected ${alg_to_string(alg)}, got ${token_alg_str}')
}
// 验证签名
signing_input := '${parts[0]}.${parts[1]}'
expected_signature := sign_message(signing_input.bytes(), secret.bytes(), alg)
expected_signature_encoded := base64url_encode(expected_signature)
if !constant_time_compare_jwt(parts[2], expected_signature_encoded) {
return error('Invalid signature')
}
// 解码 payload
payload := decode_jwt(token)!
return payload
}
// verify_jwt_with_options - 带选项的 JWT 验证
// 验证签名、过期时间、生效时间、发行者等
pub fn verify_jwt_with_options(token string, secret string, alg JwtAlgorithm, options JwtVerifyOptions) !JwtPayload {
// 先进行基本验证
payload := verify_jwt(token, secret, alg)!
now := time.now().unix()
// 验证过期时间
if options.exp && payload.exp != 0 {
if now > payload.exp {
return error('Token expired')
}
}
// 验证生效时间
if options.nbf && payload.nbf != 0 {
if now < payload.nbf {
return error('Token not yet valid')
}
}
// 验证发行者
if options.iss.len > 0 {
if payload.iss != options.iss {
return error('Invalid issuer: expected ${options.iss}, got ${payload.iss}')
}
}
return payload
}
// constant_time_compare_jwt - 常量时间比较,防止时序攻击
fn constant_time_compare_jwt(a string, b string) bool {
if a.len != b.len {
return false
}
mut result := u8(0)
for i := 0; i < a.len; i++ {
result |= a[i] ^ b[i]
}
return result == 0
}
// jwt - JWT 中间件工厂函数
// 返回一个 ContextMiddleware,用于验证 JWT token
pub fn jwt_middleware(options JwtOptions) ContextMiddleware {
return fn [options] (mut c Context, next fn (mut Context) http.Response) http.Response {
// 获取 token
token := get_jwt_token(c, options) or {
c.status(401)
return c.json('{"error":"Unauthorized","message":"${err}"}')
}
// 验证 token
payload := verify_jwt_with_options(token, options.secret, options.alg, options.verify_options) or {
c.status(401)
return c.json('{"error":"Unauthorized","message":"${err}"}')
}
// 将 payload 存储到 Context
store_jwt_payload(mut c, payload)
// 继续处理请求
return next(mut c)
}
}
// get_jwt_token - 从请求中获取 JWT token
fn get_jwt_token(c Context, options JwtOptions) !string {
// 优先从 Cookie 读取
if options.cookie.len > 0 {
if cookie_token := get_cookie(c, options.cookie) {
return cookie_token
}
}
// 从 header 读取
header_value := c.req.header.get_custom(options.header_name) or {
return error('Missing authorization header')
}
if header_value.len == 0 {
return error('Missing authorization header')
}
// 解析 Bearer token
if header_value.starts_with('Bearer ') {
token := header_value[7..].trim_space()
if token.len == 0 {
return error('Invalid token format')
}
return token
}
// 如果不是 Bearer 格式,直接返回整个值
return header_value
}
// store_jwt_payload - 将 JWT payload 存储到 Context
fn store_jwt_payload(mut c Context, payload JwtPayload) {
// 存储标准声明
if payload.sub.len > 0 {
c.set('jwt_sub', payload.sub)
}
if payload.iss.len > 0 {
c.set('jwt_iss', payload.iss)
}
if payload.aud.len > 0 {
c.set('jwt_aud', payload.aud)
}
if payload.exp != 0 {
c.set('jwt_exp', payload.exp.str())
}
if payload.nbf != 0 {
c.set('jwt_nbf', payload.nbf.str())
}
if payload.iat != 0 {
c.set('jwt_iat', payload.iat.str())
}
if payload.jti.len > 0 {
c.set('jwt_jti', payload.jti)
}
// 存储自定义声明
for key, value in payload.claims {
c.set('jwt_${key}', value)
}
// 存储完整的 payload JSON(便于获取)
c.set('jwt_payload', encode_payload(payload))
}
// get_jwt_payload - 从 Context 获取 JWT payload
// 这是一个便捷方法,用于在 handler 中获取已验证的 JWT payload
pub fn get_jwt_payload(c Context) ?JwtPayload {
payload_json := c.get('jwt_payload') or { return none }
payload := parse_payload(payload_json) or { return none }
return payload
}
// get_jwt_claim - 从 Context 获取单个 JWT 声明
pub fn get_jwt_claim(c Context, key string) ?string {
return c.get('jwt_${key}')
}
+377
View File
@@ -0,0 +1,377 @@
// middleware.v - 中间件统一导出模块
// 本模块提供所有内置中间件的统一访问接口
// 使用方式: import hono 后可通过 hono.cors(), hono.jwt_middleware() 等方式调用
module hono
import net.http
import time
import rand
// ============================================================================
// 中间件导出说明
// ============================================================================
//
// 本框架提供以下 8 个内置中间件:
//
// 1. CORS 中间件 (cors.v)
// - cors(options ...CorsOptions) ContextMiddleware
// - 用于处理跨域资源共享
//
// 2. Cookie Helper (cookie.v)
// - get_cookie(c Context, name string) ?string
// - get_all_cookies(c Context) map[string]string
// - set_cookie(mut c Context, name string, value string, options ...CookieOptions)
// - delete_cookie(mut c Context, name string, options ...CookieOptions)
// - set_signed_cookie(mut c Context, name string, value string, secret string, options ...CookieOptions) !
// - get_signed_cookie(c Context, name string, secret string) !string
//
// 3. JWT 中间件 (jwt.v)
// - jwt_middleware(options JwtOptions) ContextMiddleware
// - sign_jwt(payload JwtPayload, secret string, alg JwtAlgorithm) !string
// - verify_jwt(token string, secret string, alg JwtAlgorithm) !JwtPayload
// - decode_jwt(token string) !JwtPayload
// - get_jwt_payload(c Context) ?JwtPayload
// - get_jwt_claim(c Context, key string) ?string
//
// 4. Bearer Auth 中间件 (bearer_auth.v)
// - bearer_auth(options BearerAuthOptions) ContextMiddleware
// - get_bearer_token(c Context) ?string
//
// 5. 压缩中间件 (compress.v)
// - compress(options ...CompressOptions) ContextMiddleware
// - decompress_gzip(data []u8) ![]u8
// - decompress_deflate(data []u8) ![]u8
//
// 6. 限流中间件 (rate_limit.v)
// - rate_limit(options RateLimitOptions) ContextMiddleware
// - MemoryStore.new() &MemoryStore
// - get_rate_limit_info(c Context) ?RateLimitInfo
//
// 7. 请求验证系统 (validator.v)
// - validator(target ValidationTarget, schema ObjectSchema, options ...ValidatorOptions) ContextMiddleware
// - v_string() StringSchema
// - v_int() IntSchema
// - v_float() FloatSchema
// - v_bool() BoolSchema
// - v_array(items Schema) ArraySchema
// - v_object(properties map[string]Schema) ObjectSchema
// - get_validated_data(c Context) map[string]string
// - get_validated_json(c Context) ?string
// - get_validated_field(c Context, field string) ?string
//
// 8. WebSocket Helper (websocket.v)
// - upgrade_websocket(factory WSHandlerFactory, options ...WebSocketOptions) fn (mut Context) http.Response
// - 用于处理 WebSocket 连接升级和事件处理
//
// Types:
// - WebSocketOptions: 配置选项 (ping_interval, max_message_size, timeout, protocols)
// - WSReadyState: 连接状态枚举 (connecting, open, closing, closed)
// - WSMessageEvent: 消息事件结构
// - WSCloseEvent: 关闭事件结构
// - WSContext: WebSocket 上下文,提供 send/send_bytes/send_json/close 方法
// - WSEvents: 事件处理器配置 (on_open, on_message, on_close, on_error)
// - WSHandlerFactory: 处理器工厂函数类型
//
// Constants:
// - ws_opcode_text, ws_opcode_binary, ws_opcode_close, ws_opcode_ping, ws_opcode_pong
// - ws_close_normal, ws_close_going_away, ws_close_protocol_error, etc.
//
// Helper Functions:
// - is_websocket_upgrade(c Context) bool: 检查是否为 WebSocket 升级请求
// - compute_accept_key(key string) string: 计算 Sec-WebSocket-Accept
// - encode_ws_frame(opcode u8, payload []u8, masked bool) []u8: 编码 WebSocket 帧
// - decode_ws_frame(data []u8) !WSFrame: 解码 WebSocket 帧
//
// ============================================================================
// ============================================================================
// 中间件快捷别名函数
// 提供更简洁的调用方式,与 Hono.js 风格保持一致
// ============================================================================
// cors_middleware - CORS 中间件的别名函数
// 使用示例:
// app.use(hono.cors_middleware())
// app.use(hono.cors_middleware(CorsOptions{ origin: 'https://example.com' }))
pub fn cors_middleware(options ...CorsOptions) ContextMiddleware {
return cors(...options)
}
// jwt_auth - JWT 认证中间件的别名函数
// 使用示例:
// app.use('/api/*', hono.jwt_auth(JwtOptions{ secret: 'my-secret' }))
pub fn jwt_auth(options JwtOptions) ContextMiddleware {
return jwt_middleware(options)
}
// bearer - Bearer Token 认证中间件的别名函数
// 使用示例:
// app.use('/api/*', hono.bearer(BearerAuthOptions{ token: 'my-token' }))
pub fn bearer(options BearerAuthOptions) ContextMiddleware {
return bearer_auth(options)
}
// gzip - 压缩中间件的别名函数(默认使用 gzip)
// 使用示例:
// app.use(hono.gzip())
pub fn gzip(options ...CompressOptions) ContextMiddleware {
if options.len > 0 {
return compress(options[0])
}
return compress(CompressOptions{
encoding: .gzip
})
}
// deflate_compress - 压缩中间件的别名函数(使用 deflate)
// 使用示例:
// app.use(hono.deflate_compress())
pub fn deflate_compress(options ...CompressOptions) ContextMiddleware {
if options.len > 0 {
return compress(options[0])
}
return compress(CompressOptions{
encoding: .deflate
})
}
// rate_limiter - 限流中间件的别名函数
// 使用示例:
// store := MemoryStore.new()
// app.use(hono.rate_limiter(RateLimitOptions{ store: store, limit: 100 }))
pub fn rate_limiter(options RateLimitOptions) ContextMiddleware {
return rate_limit(options)
}
// validate_json - JSON body 验证中间件的便捷函数
// 使用示例:
// app.post('/users', hono.validate_json(v_object({
// 'name': v_string().required()
// 'email': v_string().required()
// })), handler)
pub fn validate_json(schema ObjectSchema, options ...ValidatorOptions) ContextMiddleware {
return validator(.json, schema, ...options)
}
// validate_query - Query 参数验证中间件的便捷函数
// 使用示例:
// app.get('/search', hono.validate_query(v_object({
// 'q': v_string().required()
// 'page': v_int().min(1)
// })), handler)
pub fn validate_query(schema ObjectSchema, options ...ValidatorOptions) ContextMiddleware {
return validator(.query, schema, ...options)
}
// validate_params - Path 参数验证中间件的便捷函数
// 使用示例:
// app.get('/users/:id', hono.validate_params(v_object({
// 'id': v_int().required().min(1)
// })), handler)
pub fn validate_params(schema ObjectSchema, options ...ValidatorOptions) ContextMiddleware {
return validator(.param, schema, ...options)
}
// validate_headers - Header 验证中间件的便捷函数
// 使用示例:
// app.use(hono.validate_headers(v_object({
// 'X-API-Key': v_string().required()
// })))
pub fn validate_headers(schema ObjectSchema, options ...ValidatorOptions) ContextMiddleware {
return validator(.header, schema, ...options)
}
// validate_form - Form 数据验证中间件的便捷函数
// 使用示例:
// app.post('/login', hono.validate_form(v_object({
// 'username': v_string().required()
// 'password': v_string().required().min(6)
// })), handler)
pub fn validate_form(schema ObjectSchema, options ...ValidatorOptions) ContextMiddleware {
return validator(.form, schema, ...options)
}
// ============================================================================
// 中间件组合工具
// ============================================================================
// combine_middlewares - 组合多个中间件为一个
// 使用示例:
// combined := hono.combine_middlewares([
// hono.cors_middleware(),
// hono.gzip(),
// hono.rate_limiter(options)
// ])
// app.use(combined)
pub fn combine_middlewares(middlewares []ContextMiddleware) ContextMiddleware {
return fn [middlewares] (mut c Context, next fn (mut Context) http.Response) http.Response {
// 递归执行中间件链
return execute_middleware_chain(0, middlewares, mut c, next)
}
}
// execute_middleware_chain - 递归执行中间件链
fn execute_middleware_chain(idx int, middlewares []ContextMiddleware, mut c Context, final_next fn (mut Context) http.Response) http.Response {
if idx >= middlewares.len {
return final_next(mut c)
}
mw := middlewares[idx]
return mw(mut c, fn [idx, middlewares, final_next] (mut ctx Context) http.Response {
return execute_middleware_chain(idx + 1, middlewares, mut ctx, final_next)
})
}
// ============================================================================
// 预配置中间件工厂
// ============================================================================
// secure_headers - 安全响应头中间件
// 添加常用的安全响应头
pub fn secure_headers() ContextMiddleware {
return fn (mut c Context, next fn (mut Context) http.Response) http.Response {
// 设置安全响应头
c.headers['X-Content-Type-Options'] = 'nosniff'
c.headers['X-Frame-Options'] = 'DENY'
c.headers['X-XSS-Protection'] = '1; mode=block'
c.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return next(mut c)
}
}
// request_id - 请求 ID 中间件
// 为每个请求生成唯一 ID
pub fn request_id() ContextMiddleware {
return fn (mut c Context, next fn (mut Context) http.Response) http.Response {
// 检查是否已有请求 ID
existing_id := c.req.header.get_custom('X-Request-ID') or { '' }
request_id := if existing_id.len > 0 {
existing_id
} else {
generate_request_id()
}
// 存储到 Context
c.set('request_id', request_id)
// 添加到响应头
c.headers['X-Request-ID'] = request_id
return next(mut c)
}
}
// generate_request_id - 生成简单的请求 ID
fn generate_request_id() string {
timestamp := time.now().unix_milli()
random_part := rand.u32()
return '${timestamp:x}-${random_part:08x}'
}
// timing - 请求计时中间件
// 记录请求处理时间
pub fn timing() ContextMiddleware {
return fn (mut c Context, next fn (mut Context) http.Response) http.Response {
start := time.now()
// 执行后续处理
response := next(mut c)
// 计算耗时
duration := time.since(start)
duration_ms := duration.milliseconds()
// 存储到 Context
c.set('request_duration_ms', duration_ms.str())
// 添加到响应头
c.headers['X-Response-Time'] = '${duration_ms}ms'
return response
}
}
// ============================================================================
// WebSocket Helper 导出
// ============================================================================
//
// WebSocket Helper 提供服务端 WebSocket 支持,实现 RFC 6455 协议。
// 主要功能通过 websocket.v 文件导出,包括:
//
// 核心函数:
// - upgrade_websocket(factory, options...) - 创建 WebSocket 升级处理器
// - is_websocket_upgrade(c) - 检查是否为 WebSocket 升级请求
//
// 类型定义:
// - WebSocketOptions - 配置选项
// - WSReadyState - 连接状态枚举
// - WSMessageEvent - 消息事件
// - WSCloseEvent - 关闭事件
// - WSContext - WebSocket 上下文
// - WSEvents - 事件处理器配置
// - WSHandlerFactory - 处理器工厂类型
//
// 常量:
// - ws_opcode_* - WebSocket 操作码
// - ws_close_* - WebSocket 关闭状态码
//
// 帧处理函数:
// - encode_ws_frame() - 编码 WebSocket 帧
// - decode_ws_frame() - 解码 WebSocket 帧
// - compute_accept_key() - 计算握手响应密钥
//
// 使用示例:
// app.get('/ws', hono.upgrade_websocket(fn (c hono.Context) hono.WSEvents {
// return hono.WSEvents{
// on_open: fn (mut ws hono.WSContext) {
// ws.send('Welcome!') or {}
// }
// on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
// ws.send('Echo: ${event.data}') or {}
// }
// on_close: fn (event hono.WSCloseEvent, mut ws hono.WSContext) {
// println('Closed: ${event.code}')
// }
// }
// }))
//
// 带配置选项:
// app.get('/ws', hono.upgrade_websocket(factory, hono.WebSocketOptions{
// ping_interval: 30000 // 30秒 ping 间隔
// max_message_size: 1048576 // 1MB 最大消息大小
// timeout: 60000 // 60秒超时
// protocols: ['chat', 'json'] // 支持的子协议
// }))
// ============================================================================
// websocket - WebSocket 升级处理器的别名函数
// 使用示例:
// app.get('/ws', hono.websocket(fn (c hono.Context) hono.WSEvents {
// return hono.WSEvents{
// on_message: fn (event hono.WSMessageEvent, mut ws hono.WSContext) {
// ws.send('Echo: ${event.data}') or {}
// }
// }
// }))
pub fn websocket(factory WSHandlerFactory, options ...WebSocketOptions) fn (mut Context) http.Response {
return upgrade_websocket(factory, ...options)
}
// ws - WebSocket 升级处理器的简短别名
// 使用示例:
// app.get('/ws', hono.ws(handler_factory))
pub fn ws(factory WSHandlerFactory, options ...WebSocketOptions) fn (mut Context) http.Response {
return upgrade_websocket(factory, ...options)
}
// is_ws_upgrade - 检查请求是否为 WebSocket 升级请求的别名
// 使用示例:
// if hono.is_ws_upgrade(c) {
// // Handle WebSocket upgrade
// }
pub fn is_ws_upgrade(c Context) bool {
return is_websocket_upgrade(c)
}
+475 -51
View File
@@ -11,12 +11,12 @@ pub:
port int = 8080
host string
family net.AddrFamily = .ip6
timeout_secs int = 8
timeout_secs int = 120 // 高并发场景需要更长超时(原 8 秒太短)
max_headers int = 100
max_read int = 8192
max_write int = 65536
keepalive_timeout int = 5
max_keepalive_req int = 1000
keepalive_timeout int = 30 // Keep-Alive 超时延长到 30 秒
max_keepalive_req int = 10000 // 单连接最大请求数提升到 10000
}
// picoev 请求上下文
@@ -61,6 +61,135 @@ pub fn (mut app Hono) listen_picoev_with_config(config PicoevConfig) {
pico.serve()
}
// ============================================================================
// WebSocket Upgrade Detection and Handling
// ============================================================================
// Check if a picoev request is a WebSocket upgrade request
fn is_picoev_ws_upgrade(req picohttpparser.Request) bool {
mut has_upgrade := false
mut has_connection := false
mut has_ws_key := false
for i in 0 .. req.num_headers {
h := req.headers[i]
name_lower := h.name.to_lower()
if name_lower == 'upgrade' {
if h.value.to_lower() == 'websocket' {
has_upgrade = true
}
} else if name_lower == 'connection' {
if contains_ignore_case(h.value, 'upgrade') {
has_connection = true
}
} else if name_lower == 'sec-websocket-key' {
if h.value.len > 0 {
has_ws_key = true
}
}
}
return has_upgrade && has_connection && has_ws_key
}
// Get WebSocket key from picoev request headers
fn get_picoev_ws_key(req picohttpparser.Request) string {
for i in 0 .. req.num_headers {
h := req.headers[i]
if eq_ignore_case(h.name, 'sec-websocket-key') {
return h.value
}
}
return ''
}
// Get WebSocket protocol from picoev request headers
fn get_picoev_ws_protocol(req picohttpparser.Request) string {
for i in 0 .. req.num_headers {
h := req.headers[i]
if eq_ignore_case(h.name, 'sec-websocket-protocol') {
return h.value
}
}
return ''
}
// Get WebSocket version from picoev request headers
fn get_picoev_ws_version(req picohttpparser.Request) string {
for i in 0 .. req.num_headers {
h := req.headers[i]
if eq_ignore_case(h.name, 'sec-websocket-version') {
return h.value
}
}
return '13'
}
// Handle WebSocket upgrade for picoev
fn handle_picoev_ws_upgrade(mut ctx PicoevRequestContext, req picohttpparser.Request, mut res picohttpparser.Response, route_match ContextRouteMatch, hono_ctx Context) bool {
// Validate WebSocket version
ws_version := get_picoev_ws_version(req)
if ws_version != '13' {
res.raw('HTTP/1.1 426 Upgrade Required\r\n')
res.header('Sec-WebSocket-Version', '13')
res.header('Content-Type', 'text/plain')
res.body('Unsupported WebSocket version')
res.end()
return false
}
// Get WebSocket key
ws_key := get_picoev_ws_key(req)
if ws_key.len == 0 {
res.raw('HTTP/1.1 400 Bad Request\r\n')
res.header('Content-Type', 'text/plain')
res.body('Missing Sec-WebSocket-Key header')
res.end()
return false
}
// Compute accept key
accept_key := compute_accept_key(ws_key)
// Execute the route handler to get WSEvents
// The handler should return a 101 response with WebSocket context stored
mut mutable_ctx := hono_ctx
response := route_match.handler.handle(mut mutable_ctx)
// Check if this is a WebSocket upgrade response
if response.status_code != 101 {
// Not a WebSocket upgrade, send the response as-is
send_picoev_response(mut res, mutable_ctx, response, false, ctx.config)
return false
}
// Negotiate subprotocol
mut selected_protocol := ''
if '_ws_protocol' in mutable_ctx.store {
selected_protocol = mutable_ctx.store['_ws_protocol']
}
// Send WebSocket handshake response
res.raw('HTTP/1.1 101 Switching Protocols\r\n')
res.header('Upgrade', 'websocket')
res.header('Connection', 'Upgrade')
res.header('Sec-WebSocket-Accept', accept_key)
if selected_protocol.len > 0 {
res.header('Sec-WebSocket-Protocol', selected_protocol)
}
res.body('')
res.end()
return true
}
// Send WebSocket frame via picoev response
fn send_ws_frame_picoev(data []u8) ! {
// This is a placeholder - actual implementation depends on picoev's raw socket access
// In practice, we need to write directly to the socket fd
}
// picoev 回调函数
fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picohttpparser.Response) {
mut ctx := unsafe { &PicoevRequestContext(user_data) }
@@ -75,11 +204,32 @@ fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picoht
keepalive := check_keepalive_request(req)
method_str := req.method
// Check if this is a WebSocket upgrade request
is_ws_upgrade := is_picoev_ws_upgrade(req)
// 优先使用快速路由器
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)
// Handle WebSocket upgrade if detected
if is_ws_upgrade {
if handle_picoev_ws_upgrade(mut ctx, req, mut res, route_match, hono_ctx) {
// WebSocket upgrade successful, connection is now in WebSocket mode
return
}
// If upgrade failed, response was already sent
return
}
// 优化:零中间件快速路径
if !ctx.app.has_middlewares {
response := route_match.handler.handle(mut hono_ctx)
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
return
}
middlewares := get_middlewares_for_path_picoev_optimized(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
@@ -89,7 +239,24 @@ fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picoht
// 回退到混合路由器
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)
// Handle WebSocket upgrade if detected
if is_ws_upgrade {
if handle_picoev_ws_upgrade(mut ctx, req, mut res, route_match, hono_ctx) {
// WebSocket upgrade successful
return
}
return
}
// 优化:零中间件快速路径
if !ctx.app.has_middlewares {
response := route_match.handler.handle(mut hono_ctx)
send_picoev_response(mut res, hono_ctx, response, keepalive, ctx.config)
return
}
middlewares := get_middlewares_for_path_picoev_optimized(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
@@ -118,25 +285,76 @@ fn picoev_callback(user_data voidptr, req picohttpparser.Request, mut res picoht
res.end()
}
// 检查客户端是否请求 Keep-Alive
// 检查客户端是否请求 Keep-Alive - 优化版
// 避免 to_lower() 创建新字符串,使用大小写不敏感比较
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')
// 大小写不敏感比较 "connection"
if h.name.len == 10 && eq_ignore_case(h.name, 'connection') {
// 检查是否包含 "keep-alive"(大小写不敏感)
return contains_ignore_case(h.value, 'keep-alive')
}
}
return true // HTTP/1.1 默认 Keep-Alive
}
// 获取路径对应的所有中间件
fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
// 大小写不敏感字符串比较(避免分配)
@[inline]
fn eq_ignore_case(a string, b string) bool {
if a.len != b.len {
return false
}
for i in 0 .. a.len {
ca := a[i]
cb := b[i]
// 转换为小写比较
la := if ca >= `A` && ca <= `Z` { ca + 32 } else { ca }
lb := if cb >= `A` && cb <= `Z` { cb + 32 } else { cb }
if la != lb {
return false
}
}
return true
}
// 大小写不敏感的 contains 检查(避免分配)
@[inline]
fn contains_ignore_case(haystack string, needle string) bool {
if needle.len > haystack.len {
return false
}
max_start := haystack.len - needle.len
for i := 0; i <= max_start; i++ {
mut found := true
for j in 0 .. needle.len {
ch := haystack[i + j]
cn := needle[j]
lh := if ch >= `A` && ch <= `Z` { ch + 32 } else { ch }
ln := if cn >= `A` && cn <= `Z` { cn + 32 } else { cn }
if lh != ln {
found = false
break
}
}
if found {
return true
}
}
return false
}
// 获取路径对应的所有中间件(优化版:使用预排序的前缀列表)
fn get_middlewares_for_path_picoev_optimized(app &Hono, path string) []ContextMiddleware {
// 优化:只有全局中间件时,直接返回引用(避免克隆)
if app.route_middlewares.len == 0 {
return app.context_middlewares
}
mut middlewares := app.context_middlewares.clone()
mut prefixes := app.route_middlewares.keys()
prefixes.sort(a.len < b.len)
for prefix in prefixes {
// 优化:使用预排序的前缀列表(启动时已排序,不需要每次请求都排序)
for prefix in app.sorted_middleware_prefixes {
if path.starts_with(prefix) || prefix == '/' {
if mws := app.route_middlewares[prefix] {
middlewares << mws
@@ -147,6 +365,11 @@ fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
return middlewares
}
// 获取路径对应的所有中间件(保留旧版本兼容)
fn get_middlewares_for_path_picoev(app &Hono, path string) []ContextMiddleware {
return get_middlewares_for_path_picoev_optimized(app, path)
}
// 执行中间件链
fn exec_middlewares_picoev(idx int, middlewares []ContextMiddleware, mut ctx Context, handler IHandler) http.Response {
if idx < middlewares.len {
@@ -158,40 +381,96 @@ fn exec_middlewares_picoev(idx int, middlewares []ContextMiddleware, mut ctx Con
return handler.handle(mut ctx)
}
// 解析路径和查询参数
// 解析路径和查询参数 - 零分配优化版
// 使用指针遍历避免创建临时数组
fn parse_path_and_query(full_path string) (string, map[string]string) {
mut query_map := map[string]string{}
len := full_path.len
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
}
if len == 0 {
return full_path, query_map
}
// 创建 picoev 上下文
// 快速路径:大多数请求没有查询参数
// 从后向前搜索 '?' 通常更快(查询参数在末尾)
mut query_start := -1
for i := len - 1; i >= 0; i-- {
if full_path[i] == `?` {
query_start = i
break
}
}
// 没有查询参数,直接返回(最常见情况)
if query_start == -1 {
return full_path, query_map
}
path := full_path[..query_start]
// 解析查询参数(单次遍历,避免 split)
mut key_start := query_start + 1
mut key_end := -1
mut value_start := -1
for i := query_start + 1; i <= len; i++ {
ch := if i < len { full_path[i] } else { `&` } // 末尾视为分隔符
if ch == `=` && key_end == -1 {
key_end = i
value_start = i + 1
} else if ch == `&` {
// 完成一个键值对
if key_end > key_start && value_start > 0 {
key := full_path[key_start..key_end]
value := if value_start < i { full_path[value_start..i] } else { '' }
query_map[key] = value
} else if key_end == -1 && i > key_start {
// 只有 key 没有 value(如 ?foo&bar=1)
key := full_path[key_start..i]
query_map[key] = ''
}
// 重置状态
key_start = i + 1
key_end = -1
value_start = -1
}
}
return path, query_map
}
// 创建 picoev 上下文 - 优化版
// 延迟转换 http.Request,只在真正需要时才转换
fn create_picoev_context(req picohttpparser.Request, params map[string]string, query map[string]string) Context {
// 提取纯路径(不含查询参数)
path := extract_path_only(req.path)
return Context{
req: convert_picoev_request(req)
params: params
query: query
body: req.body
url: req.path
path: req.path
path: path
status_code: 200
headers: map[string]string{}
}
}
// 转换 picoev 请求
// 快速提取路径(不含查询参数)
@[inline]
fn extract_path_only(full_path string) string {
for i in 0 .. full_path.len {
if full_path[i] == `?` {
return full_path[..i]
}
}
return full_path
}
// 转换 picoev 请求 - 优化版
// 使用预计算的方法映射避免字符串比较
fn convert_picoev_request(req picohttpparser.Request) http.Request {
mut headers := http.new_header()
@@ -201,24 +480,54 @@ fn convert_picoev_request(req picohttpparser.Request) http.Request {
}
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 }
}
method: parse_http_method_fast(req.method)
url: req.path
data: req.body
header: headers
}
}
// 发送 picoev 响应
// 快速 HTTP 方法解析(基于首字符和长度)
@[inline]
fn parse_http_method_fast(method string) http.Method {
len := method.len
if len == 0 {
return http.Method.get
}
// 基于首字符快速分支
match method[0] {
`G` {
if len == 3 { return http.Method.get }
}
`P` {
if len == 4 && method[1] == `O` { return http.Method.post }
if len == 3 && method[1] == `U` { return http.Method.put }
if len == 5 { return http.Method.patch }
}
`D` {
if len == 6 { return http.Method.delete }
}
`H` {
if len == 4 { return http.Method.head }
}
`O` {
if len == 7 { return http.Method.options }
}
else {}
}
return http.Method.get
}
// 发送 picoev 响应 - 优化版
fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response http.Response, keepalive bool, config PicoevConfig) {
// Check if this is a streaming response
if is_streaming_response(ctx) {
// Handle streaming response
handle_picoev_streaming_response(mut res, ctx, config)
return
}
status_code := if ctx.status_code != 0 { ctx.status_code } else { response.status_code }
if status_code == 200 {
@@ -229,18 +538,23 @@ fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response h
mut has_content_type := false
mut has_connection := false
mut has_content_length := false
for key, value in ctx.headers {
// 使用快速大小写不敏感比较
key_len := key.len
if key_len == 14 && eq_ignore_case(key, 'content-length') {
continue // 跳过 Content-Length
}
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 key_len == 12 && eq_ignore_case(key, 'content-type') {
has_content_type = true
} else if key_len == 10 && eq_ignore_case(key, 'connection') {
has_connection = true
}
}
if content_type := response.header.get(.content_type) {
if !has_content_type {
if content_type := response.header.get(.content_type) {
res.header('Content-Type', content_type)
has_content_type = true
}
@@ -250,10 +564,6 @@ fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response h
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')
@@ -267,9 +577,122 @@ fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response h
res.end()
}
// ============================================================================
// Streaming Response Handling for Picoev
// ============================================================================
// handle_picoev_streaming_response - Handle a streaming response using PicoevStreamWriter
// This function:
// 1. Sends the HTTP headers for streaming directly to socket fd
// 2. Creates a PicoevStreamWriter with the socket fd
// 3. Executes the streaming callback
// 4. Handles errors and cleanup
fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context, config PicoevConfig) {
// Get stream configuration
stream_config := get_stream_config(ctx) or {
// Fallback to error response if stream config not found
res.raw('HTTP/1.1 500 Internal Server Error\r\n')
res.header('Content-Type', 'text/plain')
res.body('Stream configuration not found')
res.end()
return
}
// Determine status code
status_code := if ctx.status_code != 0 { ctx.status_code } else { 200 }
// Build HTTP headers string for direct socket write
mut headers_str := 'HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n'
// Add headers from context
mut has_transfer_encoding := false
mut has_connection := false
for key, value in ctx.headers {
key_len := key.len
// Skip Content-Length for streaming (we use chunked encoding)
if key_len == 14 && eq_ignore_case(key, 'content-length') {
continue
}
headers_str += '${key}: ${value}\r\n'
if key_len == 17 && eq_ignore_case(key, 'transfer-encoding') {
has_transfer_encoding = true
} else if key_len == 10 && eq_ignore_case(key, 'connection') {
has_connection = true
}
}
// Ensure Transfer-Encoding: chunked is set
if !has_transfer_encoding {
headers_str += 'Transfer-Encoding: chunked\r\n'
}
// Ensure Connection header is set for streaming
if !has_connection {
headers_str += 'Connection: keep-alive\r\n'
headers_str += 'Keep-Alive: timeout=${config.timeout_secs}\r\n'
}
// End headers section
headers_str += '\r\n'
// Write headers directly to socket fd for immediate delivery
fd := res.fd
write_to_socket_fd(fd, headers_str.bytes()) or {
eprintln('[Picoev Stream Error] Failed to write headers: ${err.msg()}')
cleanup_stream_config(ctx)
return
}
// Create PicoevStreamWriter for streaming (uses fd directly)
mut writer := PicoevStreamWriter.new(res)
// Create StreamContext with the writer
mut stream_ctx := StreamContext.new(writer)
// Execute the streaming callback
stream_config.callback(mut stream_ctx) or {
// Handle error
if error_handler := stream_config.error_handler {
error_handler(err, mut stream_ctx)
} else {
// Default: log error to console
eprintln('[Picoev Stream Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
stream_ctx.close()
// Cleanup the stored configuration
cleanup_stream_config(ctx)
}
// write_to_socket_fd - Helper function to write bytes directly to socket fd
// Used for sending HTTP headers before streaming begins
fn write_to_socket_fd(fd int, data []u8) ! {
if data.len == 0 {
return
}
mut total_written := 0
for total_written < data.len {
remaining := data.len - total_written
ptr := unsafe { &u8(data.data) + total_written }
written := C.send(fd, ptr, remaining, 0)
if written < 0 {
return error('Failed to write to socket')
}
if written == 0 {
return error('Connection closed by peer')
}
total_written += written
}
}
// 获取状态码文本
fn get_status_text(code int) string {
return match code {
101 { 'Switching Protocols' }
200 { 'OK' }
201 { 'Created' }
204 { 'No Content' }
@@ -281,6 +704,7 @@ fn get_status_text(code int) string {
403 { 'Forbidden' }
404 { 'Not Found' }
405 { 'Method Not Allowed' }
426 { 'Upgrade Required' }
500 { 'Internal Server Error' }
502 { 'Bad Gateway' }
503 { 'Service Unavailable' }
+225
View File
@@ -0,0 +1,225 @@
module hono
import net.http
import time
import sync
// RateLimitEntry 结构体 - 限流条目
pub struct RateLimitEntry {
pub mut:
count int // 当前请求计数
reset_at i64 // 重置时间(Unix 毫秒时间戳)
}
// RateLimitStore 接口 - 限流存储后端
pub interface RateLimitStore {
mut:
// increment - 增加计数并返回当前计数和重置时间
// 参数: key - 客户端标识, window_ms - 时间窗口(毫秒)
// 返回: (当前计数, 重置时间戳)
increment(key string, window_ms i64) (int, i64)
// reset - 重置指定 key 的计数
reset(key string)
}
// MemoryStore 结构体 - 内存存储实现
pub struct MemoryStore {
mut:
data map[string]RateLimitEntry
mtx sync.Mutex // 线程安全锁
}
// MemoryStore.new - 创建新的内存存储
pub fn MemoryStore.new() &MemoryStore {
return &MemoryStore{
data: map[string]RateLimitEntry{}
}
}
// increment - 增加计数并返回当前计数和重置时间
pub fn (mut s MemoryStore) increment(key string, window_ms i64) (int, i64) {
s.mtx.@lock()
defer { s.mtx.unlock() }
now := time.now().unix_milli()
// 检查是否存在条目
if key in s.data {
mut entry := s.data[key]
// 检查是否需要重置(窗口已过期)
if now >= entry.reset_at {
// 窗口已过期,重置计数
entry.count = 1
entry.reset_at = now + window_ms
} else {
// 窗口内,增加计数
entry.count++
}
s.data[key] = entry
return entry.count, entry.reset_at
}
// 新条目
new_entry := RateLimitEntry{
count: 1
reset_at: now + window_ms
}
s.data[key] = new_entry
return new_entry.count, new_entry.reset_at
}
// reset - 重置指定 key 的计数
pub fn (mut s MemoryStore) reset(key string) {
s.mtx.@lock()
defer { s.mtx.unlock() }
s.data.delete(key)
}
// get_entry - 获取指定 key 的条目(用于测试)
pub fn (s MemoryStore) get_entry(key string) ?RateLimitEntry {
if key in s.data {
return s.data[key]
}
return none
}
// cleanup_expired - 清理过期条目(可选的维护方法)
pub fn (mut s MemoryStore) cleanup_expired() {
s.mtx.@lock()
defer { s.mtx.unlock() }
now := time.now().unix_milli()
mut keys_to_delete := []string{}
for key, entry in s.data {
if now >= entry.reset_at {
keys_to_delete << key
}
}
for key in keys_to_delete {
s.data.delete(key)
}
}
// RateLimitOptions 结构体 - 限流配置选项
pub struct RateLimitOptions {
pub:
window_ms i64 = 60000 // 时间窗口(毫秒),默认 1 分钟
limit int = 100 // 窗口内最大请求数,默认 100
key_generator ?fn (Context) string // 客户端标识生成器
skip ?fn (Context) bool // 跳过限流条件
handler ?fn (mut Context, RateLimitInfo) http.Response // 自定义限流响应
store &MemoryStore // 存储后端(必需)
headers bool = true // 是否添加限流头,默认 true
}
// RateLimitInfo 结构体 - 限流信息(传递给自定义 handler)
pub struct RateLimitInfo {
pub:
limit int // 最大请求数
remaining int // 剩余请求数
reset_at i64 // 重置时间戳(毫秒)
}
// rate_limit - 限流中间件工厂函数
// 返回一个 ContextMiddleware,用于限制请求频率
// 注意:必须提供 store 参数
pub fn rate_limit(options RateLimitOptions) ContextMiddleware {
mut store := options.store
return fn [options, mut store] (mut c Context, next fn (mut Context) http.Response) http.Response {
// 检查是否跳过限流
if skip_fn := options.skip {
if skip_fn(c) {
return next(mut c)
}
}
// 生成客户端标识 key
key := generate_rate_limit_key(c, options.key_generator)
// 增加计数
count, reset_at := store.increment(key, options.window_ms)
// 计算剩余请求数
remaining := if count > options.limit { 0 } else { options.limit - count }
// 设置限流响应头
if options.headers {
set_rate_limit_headers(mut c, options.limit, remaining, reset_at)
}
// 检查是否超过限制
if count > options.limit {
// 创建限流信息
info := RateLimitInfo{
limit: options.limit
remaining: 0
reset_at: reset_at
}
// 使用自定义 handler 或默认响应
if custom_handler := options.handler {
return custom_handler(mut c, info)
}
return rate_limit_exceeded_response(mut c, reset_at)
}
// 继续处理请求
return next(mut c)
}
}
// generate_rate_limit_key - 生成客户端标识 key
fn generate_rate_limit_key(c Context, key_generator ?fn (Context) string) string {
// 使用自定义 key 生成器
if gen_fn := key_generator {
return gen_fn(c)
}
// 默认使用客户端 IP
return c.get_client_ip()
}
// set_rate_limit_headers - 设置限流响应头
fn set_rate_limit_headers(mut c Context, limit int, remaining int, reset_at i64) {
c.headers['X-RateLimit-Limit'] = limit.str()
c.headers['X-RateLimit-Remaining'] = remaining.str()
// 将毫秒时间戳转换为秒(HTTP 标准)
c.headers['X-RateLimit-Reset'] = (reset_at / 1000).str()
}
// rate_limit_exceeded_response - 返回 429 限流响应
fn rate_limit_exceeded_response(mut c Context, reset_at i64) http.Response {
c.status(429)
// 计算 Retry-After(秒)
now := time.now().unix_milli()
retry_after := if reset_at > now { (reset_at - now) / 1000 } else { i64(0) }
if retry_after > 0 {
c.headers['Retry-After'] = retry_after.str()
}
return c.json('{"error":"Too Many Requests","message":"Rate limit exceeded. Please try again later."}')
}
// get_rate_limit_info - 从 Context 获取限流信息
// 这是一个便捷方法,用于在 handler 中获取当前请求的限流状态
pub fn get_rate_limit_info(c Context) ?RateLimitInfo {
limit_str := c.headers['X-RateLimit-Limit'] or { return none }
remaining_str := c.headers['X-RateLimit-Remaining'] or { return none }
reset_str := c.headers['X-RateLimit-Reset'] or { return none }
return RateLimitInfo{
limit: limit_str.int()
remaining: remaining_str.int()
reset_at: reset_str.i64() * 1000 // 转换回毫秒
}
}
+44
View File
@@ -17,6 +17,7 @@ pub mut:
status_code int = 200
headers map[string]string
body string
store map[string]string // 中间件数据存储
}
// Context 构造函数
@@ -35,6 +36,7 @@ pub fn Context.new(req http.Request, params map[string]string, query map[string]
url: url.str() // 设置为完整的URL字符串
path: url.path // 设置 path 属性
headers: map[string]string{}
store: map[string]string{} // 初始化中间件数据存储
}
}
@@ -256,6 +258,48 @@ pub fn (mut c Context) status(code int) {
c.status_code = code
}
// get 方法 - 从 store 中获取中间件数据
pub fn (c Context) get(key string) ?string {
if key in c.store {
return c.store[key]
}
return none
}
// set 方法 - 向 store 中存储中间件数据
pub fn (mut c Context) set(key string, value string) {
c.store[key] = value
}
// get_client_ip 方法 - 获取客户端 IP 地址
// 优先从 X-Forwarded-For 和 X-Real-IP 头获取,否则从连接信息获取
pub fn (c Context) get_client_ip() string {
// 尝试从 X-Forwarded-For 头获取(代理场景)
if forwarded_for := c.req.header.get_custom('X-Forwarded-For') {
// X-Forwarded-For 可能包含多个 IP,取第一个
ips := forwarded_for.split(',')
if ips.len > 0 {
ip := ips[0].trim_space()
if ip != '' {
return ip
}
}
}
// 尝试从 X-Real-IP 头获取
if real_ip := c.req.header.get_custom('X-Real-IP') {
trimmed := real_ip.trim_space()
if trimmed != '' {
return trimmed
}
}
// 从请求 URL 或连接信息获取
// 注意:V 语言的 http.Request 可能没有直接的远程地址字段
// 这里返回一个默认值,实际使用时可能需要根据服务器实现调整
return '127.0.0.1'
}
// 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
+26 -18
View File
@@ -208,10 +208,34 @@ pub fn (router ContextHybridRouter) match_static_route(method string, path strin
return none
}
// Context 版本的动态路径匹配
// Context 版本的动态路径匹配(保留旧版本兼容)
fn (mut router ContextHybridRouter) match_dynamic_route(method string, path string) ?ContextRouteMatch {
// 先检查缓存
cache_key := '${method}:${path}'
return router.match_dynamic_route_with_key(cache_key, method, path)
}
// Context 版本的主匹配函数(优化版:复用 cache key)
pub fn (mut router ContextHybridRouter) match_route(method string, path string) ?ContextRouteMatch {
// 优化:只拼接一次 cache key
cache_key := '${method}:${path}'
// 1. 先尝试静态路径匹配(最快)
if cache_key in router.static_routes {
return ContextRouteMatch{
handler: router.static_routes[cache_key]
params: map[string]string{}
path: path
base_path: ''
}
}
// 2. 再尝试动态路径匹配(复用 cache_key)
return router.match_dynamic_route_with_key(cache_key, method, path)
}
// Context 版本的动态路径匹配(优化版:接收预计算的 cache_key)
fn (mut router ContextHybridRouter) match_dynamic_route_with_key(cache_key string, method string, path string) ?ContextRouteMatch {
// 先检查缓存
if cached := router.cache.get(cache_key) {
return cached
}
@@ -240,22 +264,6 @@ fn (mut router ContextHybridRouter) match_dynamic_route(method string, path stri
return none
}
// Context 版本的主匹配函数
pub fn (mut router ContextHybridRouter) match_route(method string, path string) ?ContextRouteMatch {
// 1. 先尝试静态路径匹配(最快)
if static_handler := router.match_static_route(method, path) {
return ContextRouteMatch{
handler: static_handler
params: map[string]string{}
path: path
base_path: ''
}
}
// 2. 再尝试动态路径匹配
return router.match_dynamic_route(method, path)
}
// Context 版本的获取所有路由
pub fn (router ContextHybridRouter) get_all_routes() ([]string, []string) {
mut static_paths := []string{}
+982
View File
@@ -0,0 +1,982 @@
// streaming.v - SSE Streaming Helper for v-hono
// Provides Server-Sent Events (SSE) and streaming response support
// Reference: Hono.js Streaming Helper API
module hono
import net.http
import time
import picohttpparser
import usockets
// C function declaration for direct socket writes
fn C.send(sockfd int, buf voidptr, len int, flags int) int
// ============================================================================
// SSE Event Structure
// ============================================================================
// SSEEvent - Server-Sent Events event structure
// Used with stream_sse() to send formatted SSE events to clients
pub struct SSEEvent {
pub:
data string // Event data (required) - supports multi-line
event string // Event type name (optional)
id string // Event ID for client reconnection (optional)
retry int // Reconnection time in milliseconds (optional, 0 = not set)
}
// ============================================================================
// StreamWriter Interface
// ============================================================================
// StreamWriter - Abstract interface for stream writing
// Allows different server backends (picoev, uSockets) to implement their own writers
pub interface StreamWriter {
// Check if the connection is still active (immutable)
is_connected() bool
mut:
// Write raw bytes to the stream
write(data []u8) !
// Write string to the stream
write_string(data string) !
// Flush the buffer to ensure data is sent
flush() !
// Close the stream connection
close() !
}
// ============================================================================
// StreamContext Structure
// ============================================================================
// StreamContext - Context provided to streaming callbacks
// Provides methods for writing data, SSE events, and managing the stream
@[heap]
pub struct StreamContext {
mut:
writer &StreamWriter
is_closed bool
abort_fn ?fn ()
}
// StreamContext.new - Create a new StreamContext with the given writer
pub fn StreamContext.new(writer &StreamWriter) StreamContext {
return StreamContext{
writer: unsafe { writer }
is_closed: false
abort_fn: none
}
}
// write - Write raw bytes to the stream
pub fn (mut ctx StreamContext) write(data []u8) ! {
if ctx.is_closed {
return error('Stream is closed')
}
if !ctx.writer.is_connected() {
ctx.is_closed = true
return error('Connection lost')
}
ctx.writer.write(data)!
}
// write_string - Write a string to the stream
pub fn (mut ctx StreamContext) write_string(data string) ! {
if ctx.is_closed {
return error('Stream is closed')
}
if !ctx.writer.is_connected() {
ctx.is_closed = true
return error('Connection lost')
}
ctx.writer.write_string(data)!
}
// writeln - Write a string followed by a newline character
pub fn (mut ctx StreamContext) writeln(data string) ! {
ctx.write_string(data)!
ctx.write_string('\n')!
}
// write_sse - Write an SSE event to the stream
// Formats the event according to the SSE specification:
// - event: {type}\n (if event field is set)
// - data: {line}\n (for each line of data)
// - id: {id}\n (if id field is set)
// - retry: {ms}\n (if retry > 0)
// - \n (event separator)
pub fn (mut ctx StreamContext) write_sse(event SSEEvent) ! {
if ctx.is_closed {
return error('Stream is closed')
}
if !ctx.writer.is_connected() {
ctx.is_closed = true
return error('Connection lost')
}
// Write event type if specified
if event.event.len > 0 {
ctx.writer.write_string('event: ${event.event}\n')!
}
// Write data field - handle multi-line data
lines := event.data.split('\n')
for line in lines {
ctx.writer.write_string('data: ${line}\n')!
}
// Write id if specified
if event.id.len > 0 {
ctx.writer.write_string('id: ${event.id}\n')!
}
// Write retry if specified (must be > 0)
if event.retry > 0 {
ctx.writer.write_string('retry: ${event.retry}\n')!
}
// Write event separator (empty line)
ctx.writer.write_string('\n')!
// Flush to ensure event is sent immediately
ctx.writer.flush()!
}
// sleep - Pause execution for the specified milliseconds
pub fn (ctx StreamContext) sleep(ms int) {
time.sleep(ms * time.millisecond)
}
// pipe - Pipe data from a byte array to the stream
pub fn (mut ctx StreamContext) pipe(data []u8) ! {
ctx.write(data)!
}
// on_abort - Register a callback to be called when the client aborts the connection
pub fn (mut ctx StreamContext) on_abort(callback fn ()) {
ctx.abort_fn = callback
}
// close - Close the stream
pub fn (mut ctx StreamContext) close() {
if !ctx.is_closed {
ctx.is_closed = true
ctx.writer.close() or {}
}
}
// is_open - Check if the stream is still open
pub fn (ctx StreamContext) is_open() bool {
return !ctx.is_closed && ctx.writer.is_connected()
}
// trigger_abort - Internal method to trigger the abort callback
// Called by server backends when client disconnects
pub fn (mut ctx StreamContext) trigger_abort() {
if abort_callback := ctx.abort_fn {
abort_callback()
}
ctx.is_closed = true
}
// ============================================================================
// Picoev StreamWriter Implementation
// ============================================================================
// PicoevStreamWriter - StreamWriter implementation for picoev server
// Supports chunked transfer encoding for streaming responses
// Uses direct socket fd writes for immediate data delivery (true streaming)
@[heap]
pub struct PicoevStreamWriter {
mut:
fd int // Socket file descriptor for direct writes
connected bool
}
// PicoevStreamWriter.new - Create a new PicoevStreamWriter with the given response
// Extracts the fd from picohttpparser.Response for direct socket writes
pub fn PicoevStreamWriter.new(res &picohttpparser.Response) &PicoevStreamWriter {
return &PicoevStreamWriter{
fd: res.fd
connected: true
}
}
// write - Write raw bytes to the stream using chunked transfer encoding
// Format: {hex_size}\r\n{data}\r\n
// Writes directly to socket fd for immediate delivery
pub fn (mut w PicoevStreamWriter) write(data []u8) ! {
if !w.connected {
return error('Connection closed')
}
if data.len == 0 {
return
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := format_chunk_size(data.len)
// Write directly to socket fd
w.write_to_fd(chunk_header.bytes())!
w.write_to_fd(data)!
w.write_to_fd('\r\n'.bytes())!
}
// write_string - Write a string to the stream using chunked transfer encoding
pub fn (mut w PicoevStreamWriter) write_string(data string) ! {
if !w.connected {
return error('Connection closed')
}
if data.len == 0 {
return
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := format_chunk_size(data.len)
// Write directly to socket fd
w.write_to_fd(chunk_header.bytes())!
w.write_to_fd(data.bytes())!
w.write_to_fd('\r\n'.bytes())!
}
// write_to_fd - Write bytes directly to the socket file descriptor
// This ensures immediate delivery without buffering
@[inline]
fn (mut w PicoevStreamWriter) write_to_fd(data []u8) ! {
if data.len == 0 {
return
}
mut total_written := 0
for total_written < data.len {
remaining := data.len - total_written
ptr := unsafe { &u8(data.data) + total_written }
written := C.send(w.fd, ptr, remaining, 0)
if written < 0 {
w.connected = false
return error('Failed to write to socket')
}
if written == 0 {
w.connected = false
return error('Connection closed by peer')
}
total_written += written
}
}
// flush - Flush the buffer (direct fd writes are already unbuffered)
pub fn (mut w PicoevStreamWriter) flush() ! {
// Direct fd writes are unbuffered, no explicit flush needed
// But we can use TCP_NODELAY to disable Nagle's algorithm if needed
}
// close - Close the stream by sending the final chunk
// Final chunk format: 0\r\n\r\n
pub fn (mut w PicoevStreamWriter) close() ! {
if w.connected {
// Write the final chunk (zero-length chunk) to signal end of stream
w.write_to_fd('0\r\n\r\n'.bytes()) or {}
w.connected = false
// Note: Don't close the fd here, picoev manages the connection lifecycle
}
}
// is_connected - Check if the connection is still active
pub fn (w PicoevStreamWriter) is_connected() bool {
return w.connected
}
// ============================================================================
// uSockets StreamWriter Implementation
// ============================================================================
// UsocketsStreamWriter - StreamWriter implementation for uSockets server
// Supports chunked transfer encoding for streaming responses
@[heap]
pub struct UsocketsStreamWriter {
mut:
socket usockets.Socket
connected bool
}
// UsocketsStreamWriter.new - Create a new UsocketsStreamWriter with the given socket
pub fn UsocketsStreamWriter.new(socket usockets.Socket) &UsocketsStreamWriter {
return &UsocketsStreamWriter{
socket: socket
connected: true
}
}
// write - Write raw bytes to the stream using chunked transfer encoding
// Format: {hex_size}\r\n{data}\r\n
pub fn (mut w UsocketsStreamWriter) write(data []u8) ! {
if !w.connected {
return error('Connection closed')
}
if data.len == 0 {
return
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := format_chunk_size(data.len)
w.socket.write_bytes(chunk_header)
w.socket.write_bytes(data.bytestr())
w.socket.write_bytes('\r\n')
}
// write_string - Write a string to the stream using chunked transfer encoding
pub fn (mut w UsocketsStreamWriter) write_string(data string) ! {
if !w.connected {
return error('Connection closed')
}
if data.len == 0 {
return
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := format_chunk_size(data.len)
w.socket.write_bytes(chunk_header)
w.socket.write_bytes(data)
w.socket.write_bytes('\r\n')
}
// flush - Flush the buffer (uSockets handles this automatically)
pub fn (mut w UsocketsStreamWriter) flush() ! {
// uSockets automatically flushes data
// No explicit flush needed
}
// close - Close the stream by sending the final chunk
// Final chunk format: 0\r\n\r\n
pub fn (mut w UsocketsStreamWriter) close() ! {
if w.connected {
// Write the final chunk (zero-length chunk) to signal end of stream
w.socket.write_bytes('0\r\n\r\n')
w.connected = false
}
}
// is_connected - Check if the connection is still active
pub fn (w UsocketsStreamWriter) is_connected() bool {
return w.connected
}
// mark_disconnected - Mark the connection as disconnected
// Called by server backends when client disconnects
pub fn (mut w UsocketsStreamWriter) mark_disconnected() {
w.connected = false
}
// ============================================================================
// Helper Functions for Chunked Encoding
// ============================================================================
// format_chunk_size - Format the chunk size as hex followed by CRLF
// This is used for HTTP chunked transfer encoding
@[inline]
fn format_chunk_size(size int) string {
return '${size:x}\r\n'
}
// format_chunk - Format a complete chunk with size, data, and trailing CRLF
// Returns the full chunk in chunked transfer encoding format
pub fn format_chunk(data []u8) string {
if data.len == 0 {
return ''
}
return '${data.len:x}\r\n${data.bytestr()}\r\n'
}
// format_chunk_string - Format a string as a complete chunk
pub fn format_chunk_string(data string) string {
if data.len == 0 {
return ''
}
return '${data.len:x}\r\n${data}\r\n'
}
// format_final_chunk - Return the final chunk marker for chunked encoding
pub fn format_final_chunk() string {
return '0\r\n\r\n'
}
// ============================================================================
// Streaming Helper Functions
// ============================================================================
// StreamCallback - Callback function type for streaming
// The callback receives a mutable StreamContext and can write data to the stream
pub type StreamCallback = fn (mut StreamContext) !
// StreamErrorHandler - Error handler function type for streaming
// Called when an error occurs during streaming
pub type StreamErrorHandler = fn (err IError, mut ctx StreamContext)
// StreamResponse - Response type for streaming operations
// Contains the headers that should be sent before streaming begins
pub struct StreamResponse {
pub:
status_code int = 200
headers map[string]string
is_stream bool = true
}
// stream - Basic binary streaming function
// Sets Transfer-Encoding: chunked and executes the callback with a StreamContext
//
// Parameters:
// writer: The StreamWriter implementation (PicoevStreamWriter or UsocketsStreamWriter)
// callback: The callback function that writes data to the stream
// error_handler: Optional error handler called if an error occurs
//
// Returns:
// StreamResponse with the appropriate headers
//
// Example:
// stream(writer, fn (mut stream) ! {
// stream.write('Hello'.bytes())!
// stream.sleep(100)
// stream.write('World'.bytes())!
// })
pub fn stream(mut writer StreamWriter, callback StreamCallback, error_handler ...StreamErrorHandler) StreamResponse {
// Create StreamContext with the writer
mut ctx := StreamContext.new(writer)
// Execute the callback and handle errors
callback(mut ctx) or {
// Handle error
if error_handler.len > 0 {
// Call custom error handler
error_handler[0](err, mut ctx)
} else {
// Default: log error to console
eprintln('[SSE Stream Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
ctx.close()
// Return response headers for streaming
return StreamResponse{
status_code: 200
headers: {
'Transfer-Encoding': 'chunked'
'Connection': 'keep-alive'
}
is_stream: true
}
}
// stream_with_headers - Stream with custom initial headers
// Allows setting additional headers before streaming begins
pub fn stream_with_headers(mut writer StreamWriter, headers map[string]string, callback StreamCallback, error_handler ...StreamErrorHandler) StreamResponse {
// Create StreamContext with the writer
mut ctx := StreamContext.new(writer)
// Execute the callback and handle errors
callback(mut ctx) or {
// Handle error
if error_handler.len > 0 {
// Call custom error handler
error_handler[0](err, mut ctx)
} else {
// Default: log error to console
eprintln('[SSE Stream Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
ctx.close()
// Merge headers with required streaming headers
mut response_headers := headers.clone()
response_headers['Transfer-Encoding'] = 'chunked'
if 'Connection' !in response_headers {
response_headers['Connection'] = 'keep-alive'
}
return StreamResponse{
status_code: 200
headers: response_headers
is_stream: true
}
}
// get_stream_headers - Get the headers required for basic streaming
// Returns a map of headers that should be set for chunked transfer encoding
pub fn get_stream_headers() map[string]string {
return {
'Transfer-Encoding': 'chunked'
'Connection': 'keep-alive'
}
}
// write_stream_headers - Write HTTP headers for streaming response
// This is a helper function to write the initial HTTP response line and headers
// before starting chunked data transfer
//
// Parameters:
// writer: The StreamWriter to write headers to
// status_code: HTTP status code (default 200)
// headers: Map of header name to value
pub fn write_stream_headers(mut writer StreamWriter, status_code int, headers map[string]string) ! {
// Build HTTP response line
status_text := get_status_text(status_code)
mut response := 'HTTP/1.1 ${status_code} ${status_text}\r\n'
// Add headers
for key, value in headers {
response += '${key}: ${value}\r\n'
}
// Add required streaming headers if not present
if 'Transfer-Encoding' !in headers {
response += 'Transfer-Encoding: chunked\r\n'
}
if 'Connection' !in headers {
response += 'Connection: keep-alive\r\n'
}
// End headers section
response += '\r\n'
// Write headers directly (not chunked)
// Note: Headers are written raw, not using chunked encoding
// The chunked encoding starts after the headers
writer.write_string(response)!
}
// stream_text - Text streaming function
// Sets Content-Type: text/plain; charset=utf-8, Transfer-Encoding: chunked,
// and X-Content-Type-Options: nosniff headers, then executes the callback
// with a StreamContext for text streaming.
//
// Parameters:
// writer: The StreamWriter implementation (PicoevStreamWriter or UsocketsStreamWriter)
// callback: The callback function that writes text data to the stream
// error_handler: Optional error handler called if an error occurs
//
// Returns:
// StreamResponse with the appropriate headers for text streaming
//
// Example:
// stream_text(writer, fn (mut stream) ! {
// stream.write_string('Hello ')!
// stream.sleep(100)
// stream.writeln('World')!
// })
pub fn stream_text(mut writer StreamWriter, callback StreamCallback, error_handler ...StreamErrorHandler) StreamResponse {
// Create StreamContext with the writer
mut ctx := StreamContext.new(writer)
// Execute the callback and handle errors
callback(mut ctx) or {
// Handle error
if error_handler.len > 0 {
// Call custom error handler
error_handler[0](err, mut ctx)
} else {
// Default: log error to console
eprintln('[SSE StreamText Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
ctx.close()
// Return response headers for text streaming
return StreamResponse{
status_code: 200
headers: {
'Content-Type': 'text/plain; charset=utf-8'
'Transfer-Encoding': 'chunked'
'X-Content-Type-Options': 'nosniff'
'Connection': 'keep-alive'
}
is_stream: true
}
}
// get_stream_text_headers - Get the headers required for text streaming
// Returns a map of headers that should be set for text streaming with chunked transfer encoding
pub fn get_stream_text_headers() map[string]string {
return {
'Content-Type': 'text/plain; charset=utf-8'
'Transfer-Encoding': 'chunked'
'X-Content-Type-Options': 'nosniff'
'Connection': 'keep-alive'
}
}
// stream_sse - Server-Sent Events streaming function
// Sets Content-Type: text/event-stream, Cache-Control: no-cache,
// and Connection: keep-alive headers, then executes the callback
// with a StreamContext for SSE streaming.
//
// Parameters:
// writer: The StreamWriter implementation (PicoevStreamWriter or UsocketsStreamWriter)
// callback: The callback function that writes SSE events to the stream
// error_handler: Optional error handler called if an error occurs
//
// Returns:
// StreamResponse with the appropriate headers for SSE streaming
//
// Example:
// stream_sse(writer, fn (mut stream) ! {
// stream.write_sse(SSEEvent{
// data: 'Hello World'
// event: 'message'
// id: '1'
// })!
// stream.sleep(1000)
// stream.write_sse(SSEEvent{
// data: 'Another message'
// event: 'update'
// id: '2'
// })!
// })
pub fn stream_sse(mut writer StreamWriter, callback StreamCallback, error_handler ...StreamErrorHandler) StreamResponse {
// Create StreamContext with the writer
mut ctx := StreamContext.new(writer)
// Execute the callback and handle errors
callback(mut ctx) or {
// Handle error
if error_handler.len > 0 {
// Call custom error handler
error_handler[0](err, mut ctx)
} else {
// Default: log error to console
eprintln('[SSE StreamSSE Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
ctx.close()
// Return response headers for SSE streaming
// Requirements 3.1, 3.2, 3.3:
// - Content-Type: text/event-stream
// - Cache-Control: no-cache
// - Connection: keep-alive
return StreamResponse{
status_code: 200
headers: {
'Content-Type': 'text/event-stream'
'Cache-Control': 'no-cache'
'Connection': 'keep-alive'
'Transfer-Encoding': 'chunked'
}
is_stream: true
}
}
// get_stream_sse_headers - Get the headers required for SSE streaming
// Returns a map of headers that should be set for Server-Sent Events streaming
pub fn get_stream_sse_headers() map[string]string {
return {
'Content-Type': 'text/event-stream'
'Cache-Control': 'no-cache'
'Connection': 'keep-alive'
'Transfer-Encoding': 'chunked'
}
}
// ============================================================================
// Context-Based Streaming Functions
// ============================================================================
// These functions are designed to be used in route handlers and integrate
// with the picoev and uSockets server backends.
// StreamType - Type of streaming response
pub enum StreamType {
basic // Basic binary streaming
text // Text streaming with text/plain content type
sse // Server-Sent Events streaming
}
// StreamConfig - Configuration stored in Context for streaming responses
// This is used internally by the server backends to detect and handle streaming
pub struct StreamConfig {
pub:
stream_type StreamType
callback StreamCallback = unsafe { nil }
error_handler ?StreamErrorHandler
}
// Global storage for stream callbacks (workaround for V's limitations with storing functions in maps)
// Key is a unique identifier stored in Context.store['_stream_id']
// Using shared map for thread-safe access
struct StreamCallbackStorage {
mut:
callbacks shared map[string]StreamConfig
}
// Module-level storage instance
const stream_storage = &StreamCallbackStorage{}
// store_stream_config - Store a stream configuration
fn store_stream_config(id string, config StreamConfig) {
lock stream_storage.callbacks {
stream_storage.callbacks[id] = config
}
}
// retrieve_stream_config - Retrieve a stream configuration by ID
fn retrieve_stream_config(id string) ?StreamConfig {
rlock stream_storage.callbacks {
if id in stream_storage.callbacks {
return stream_storage.callbacks[id]
}
}
return none
}
// remove_stream_config - Remove a stream configuration by ID
fn remove_stream_config(id string) {
lock stream_storage.callbacks {
stream_storage.callbacks.delete(id)
}
}
// generate_stream_id - Generate a unique stream ID
fn generate_stream_id() string {
return '${time.now().unix_milli()}_${rand_int()}'
}
// rand_int - Simple random integer generator
fn rand_int() int {
return int(time.now().unix_nano() % 1000000)
}
// c_stream - Basic binary streaming function for use in route handlers
// Sets Transfer-Encoding: chunked and stores the callback for execution by the server backend
//
// Parameters:
// c: The request Context
// callback: The callback function that writes data to the stream
// error_handler: Optional error handler called if an error occurs
//
// Returns:
// http.Response with streaming marker headers
//
// Example:
// app.get('/stream', fn (mut c hono.Context) http.Response {
// return hono.c_stream(mut c, fn (mut stream hono.StreamContext) ! {
// stream.write('Hello'.bytes())!
// stream.sleep(100)
// stream.write('World'.bytes())!
// })
// })
pub fn c_stream(mut c Context, callback StreamCallback, error_handler ...StreamErrorHandler) http.Response {
// Generate unique stream ID
stream_id := generate_stream_id()
// Store stream configuration
config := StreamConfig{
stream_type: .basic
callback: callback
error_handler: if error_handler.len > 0 { error_handler[0] } else { none }
}
store_stream_config(stream_id, config)
// Mark context as streaming
c.store['_stream'] = 'true'
c.store['_stream_id'] = stream_id
c.store['_stream_type'] = 'basic'
// Set streaming headers
c.headers['Transfer-Encoding'] = 'chunked'
c.headers['Connection'] = 'keep-alive'
// Return a marker response (body will be ignored, streaming will be handled by server)
mut headers := http.new_header()
headers.add_custom('Transfer-Encoding', 'chunked') or {}
headers.add_custom('Connection', 'keep-alive') or {}
return http.Response{
status_code: 200
header: headers
body: ''
}
}
// c_stream_text - Text streaming function for use in route handlers
// Sets Content-Type: text/plain; charset=utf-8, Transfer-Encoding: chunked,
// and X-Content-Type-Options: nosniff headers
//
// Parameters:
// c: The request Context
// callback: The callback function that writes text data to the stream
// error_handler: Optional error handler called if an error occurs
//
// Returns:
// http.Response with streaming marker headers
//
// Example:
// app.get('/stream-text', fn (mut c hono.Context) http.Response {
// return hono.c_stream_text(mut c, fn (mut stream hono.StreamContext) ! {
// stream.write_string('Hello ')!
// stream.sleep(100)
// stream.writeln('World')!
// })
// })
pub fn c_stream_text(mut c Context, callback StreamCallback, error_handler ...StreamErrorHandler) http.Response {
// Generate unique stream ID
stream_id := generate_stream_id()
// Store stream configuration
config := StreamConfig{
stream_type: .text
callback: callback
error_handler: if error_handler.len > 0 { error_handler[0] } else { none }
}
store_stream_config(stream_id, config)
// Mark context as streaming
c.store['_stream'] = 'true'
c.store['_stream_id'] = stream_id
c.store['_stream_type'] = 'text'
// Set streaming headers
c.headers['Content-Type'] = 'text/plain; charset=utf-8'
c.headers['Transfer-Encoding'] = 'chunked'
c.headers['X-Content-Type-Options'] = 'nosniff'
c.headers['Connection'] = 'keep-alive'
// Return a marker response
mut headers := http.new_header()
headers.add_custom('Content-Type', 'text/plain; charset=utf-8') or {}
headers.add_custom('Transfer-Encoding', 'chunked') or {}
headers.add_custom('X-Content-Type-Options', 'nosniff') or {}
headers.add_custom('Connection', 'keep-alive') or {}
return http.Response{
status_code: 200
header: headers
body: ''
}
}
// c_stream_sse - Server-Sent Events streaming function for use in route handlers
// Sets Content-Type: text/event-stream, Cache-Control: no-cache,
// and Connection: keep-alive headers
//
// Parameters:
// c: The request Context
// callback: The callback function that writes SSE events to the stream
// error_handler: Optional error handler called if an error occurs
//
// Returns:
// http.Response with streaming marker headers
//
// Example:
// app.get('/sse', fn (mut c hono.Context) http.Response {
// return hono.c_stream_sse(mut c, fn (mut stream hono.StreamContext) ! {
// stream.write_sse(hono.SSEEvent{
// data: 'Hello World'
// event: 'message'
// id: '1'
// })!
// stream.sleep(1000)
// stream.write_sse(hono.SSEEvent{
// data: 'Another message'
// event: 'update'
// id: '2'
// })!
// })
// })
pub fn c_stream_sse(mut c Context, callback StreamCallback, error_handler ...StreamErrorHandler) http.Response {
// Generate unique stream ID
stream_id := generate_stream_id()
// Store stream configuration
config := StreamConfig{
stream_type: .sse
callback: callback
error_handler: if error_handler.len > 0 { error_handler[0] } else { none }
}
store_stream_config(stream_id, config)
// Mark context as streaming
c.store['_stream'] = 'true'
c.store['_stream_id'] = stream_id
c.store['_stream_type'] = 'sse'
// Set SSE headers (Requirements 3.1, 3.2, 3.3)
c.headers['Content-Type'] = 'text/event-stream'
c.headers['Cache-Control'] = 'no-cache'
c.headers['Connection'] = 'keep-alive'
c.headers['Transfer-Encoding'] = 'chunked'
// Return a marker response
mut headers := http.new_header()
headers.add_custom('Content-Type', 'text/event-stream') or {}
headers.add_custom('Cache-Control', 'no-cache') or {}
headers.add_custom('Connection', 'keep-alive') or {}
headers.add_custom('Transfer-Encoding', 'chunked') or {}
return http.Response{
status_code: 200
header: headers
body: ''
}
}
// is_streaming_response - Check if a Context represents a streaming response
// Used by server backends to detect streaming responses
pub fn is_streaming_response(ctx Context) bool {
return '_stream' in ctx.store && ctx.store['_stream'] == 'true'
}
// get_stream_config - Get the stream configuration from Context
// Returns the StreamConfig if this is a streaming response, none otherwise
pub fn get_stream_config(ctx Context) ?StreamConfig {
if !is_streaming_response(ctx) {
return none
}
stream_id := ctx.store['_stream_id'] or { return none }
return retrieve_stream_config(stream_id)
}
// cleanup_stream_config - Remove stream configuration after use
// Should be called by server backends after streaming is complete
pub fn cleanup_stream_config(ctx Context) {
if stream_id := ctx.store['_stream_id'] {
remove_stream_config(stream_id)
}
}
// execute_stream - Execute a streaming response with the given writer
// This is the main entry point for server backends to handle streaming
//
// Parameters:
// ctx: The request Context with streaming configuration
// writer: The StreamWriter implementation for the server backend
//
// Returns:
// true if streaming was executed, false if not a streaming response
pub fn execute_stream(ctx Context, mut writer StreamWriter) bool {
config := get_stream_config(ctx) or { return false }
// Create StreamContext with the writer
mut stream_ctx := StreamContext.new(writer)
// Execute the callback and handle errors
config.callback(mut stream_ctx) or {
// Handle error
if error_handler := config.error_handler {
error_handler(err, mut stream_ctx)
} else {
// Default: log error to console
eprintln('[SSE Stream Error] ${err.msg()}')
}
}
// Auto-close the stream when callback completes
stream_ctx.close()
// Cleanup the stored configuration
cleanup_stream_config(ctx)
return true
}
+251
View File
@@ -0,0 +1,251 @@
// 高并发基准测试 - 用于对比 v-hono 内外性能
// 参数与外层 benchmark.go 保持一致: 500 并发, 100万请求
//
// 使用方法:
// 1. 启动服务器: v -enable-globals run v-hono/tests/test_picoev_server.v
// 2. 运行测试: go run v-hono/tests/benchmark_compare.go
package main
import (
"fmt"
"io"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
const (
baseURL = "http://127.0.0.1:9999"
connections = 500
requests = 1000000
timeout = 10 * time.Second
)
type Metrics struct {
TotalRequests int64
SuccessRequests int64
FailedRequests int64
TotalBytes int64
MinLatency time.Duration
MaxLatency time.Duration
TotalLatencyNs int64
StartTime time.Time
EndTime time.Time
}
func main() {
fmt.Println("🔥 V-Hono 高并发基准测试")
fmt.Println(strings.Repeat("=", 50))
fmt.Printf("服务器: %s\n", baseURL)
fmt.Printf("并发数: %d\n", connections)
fmt.Printf("请求数: %d\n", requests)
fmt.Printf("超时: %v\n", timeout)
fmt.Println()
// 检查服务器
if !checkServer() {
fmt.Println("❌ 服务器未运行")
fmt.Println(" 请先启动: v -enable-globals run v-hono/tests/test_picoev_server.v")
return
}
fmt.Println("✅ 服务器已就绪")
// 预热
fmt.Println("\n🔥 预热服务器...")
warmup()
fmt.Println("✅ 预热完成")
// 运行测试
testCases := []struct {
name string
path string
}{
{"健康检查", "/health"},
{"根路径", "/"},
{"JSON接口", "/json"},
{"动态路由", "/users/123"},
}
allResults := make(map[string]*Metrics)
for _, tc := range testCases {
fmt.Printf("\n🚀 测试: %s (%s)\n", tc.name, tc.path)
fmt.Println(strings.Repeat("-", 50))
metrics := runBenchmark(tc.path)
allResults[tc.name] = metrics
printMetrics(tc.name, metrics)
time.Sleep(2 * time.Second)
}
// 汇总
printSummary(allResults)
}
func checkServer() bool {
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 3; i++ {
resp, err := client.Get(baseURL + "/health")
if err == nil && resp.StatusCode == 200 {
resp.Body.Close()
return true
}
time.Sleep(500 * time.Millisecond)
}
return false
}
func warmup() {
client := &http.Client{Timeout: 5 * time.Second}
for i := 0; i < 100; i++ {
resp, err := client.Get(baseURL + "/health")
if err == nil {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
}
func runBenchmark(endpoint string) *Metrics {
url := baseURL + endpoint
metrics := &Metrics{
StartTime: time.Now(),
MinLatency: time.Hour,
}
client := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
MaxIdleConns: connections,
MaxIdleConnsPerHost: connections,
IdleConnTimeout: 30 * time.Second,
},
}
requestsPerWorker := requests / connections
var wg sync.WaitGroup
var latencyMutex sync.Mutex
// 进度显示
done := make(chan bool)
go showProgress(metrics, done)
// 启动工作协程
for i := 0; i < connections; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < requestsPerWorker; j++ {
start := time.Now()
resp, err := client.Get(url)
latency := time.Since(start)
atomic.AddInt64(&metrics.TotalRequests, 1)
atomic.AddInt64(&metrics.TotalLatencyNs, int64(latency))
latencyMutex.Lock()
if latency < metrics.MinLatency {
metrics.MinLatency = latency
}
if latency > metrics.MaxLatency {
metrics.MaxLatency = latency
}
latencyMutex.Unlock()
if err != nil {
atomic.AddInt64(&metrics.FailedRequests, 1)
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil || resp.StatusCode >= 400 {
atomic.AddInt64(&metrics.FailedRequests, 1)
} else {
atomic.AddInt64(&metrics.SuccessRequests, 1)
atomic.AddInt64(&metrics.TotalBytes, int64(len(body)))
}
}
}()
}
wg.Wait()
metrics.EndTime = time.Now()
done <- true
return metrics
}
func showProgress(metrics *Metrics, done chan bool) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-done:
fmt.Println()
return
case <-ticker.C:
current := atomic.LoadInt64(&metrics.TotalRequests)
elapsed := time.Since(metrics.StartTime).Seconds()
rps := float64(current) / elapsed
progress := float64(current) / float64(requests) * 100
fmt.Printf("\r进度: %.1f%% | 完成: %d | RPS: %.0f | 耗时: %.1fs",
progress, current, rps, elapsed)
}
}
}
func printMetrics(name string, m *Metrics) {
duration := m.EndTime.Sub(m.StartTime)
avgLatency := time.Duration(m.TotalLatencyNs / m.TotalRequests)
fmt.Printf("\n📊 %s - 测试结果\n", name)
fmt.Println(strings.Repeat("=", 40))
fmt.Printf("总请求数: %d\n", m.TotalRequests)
fmt.Printf("成功请求: %d\n", m.SuccessRequests)
fmt.Printf("失败请求: %d\n", m.FailedRequests)
fmt.Printf("成功率: %.2f%%\n", float64(m.SuccessRequests)/float64(m.TotalRequests)*100)
fmt.Printf("总耗时: %.2f 秒\n", duration.Seconds())
fmt.Printf("RPS: %.0f 请求/秒\n", float64(m.TotalRequests)/duration.Seconds())
fmt.Printf("最小延迟: %v\n", m.MinLatency)
fmt.Printf("最大延迟: %v\n", m.MaxLatency)
fmt.Printf("平均延迟: %v\n", avgLatency)
if m.TotalBytes > 0 {
throughputMB := float64(m.TotalBytes) / 1024 / 1024 / duration.Seconds()
fmt.Printf("数据传输: %.2f MB\n", float64(m.TotalBytes)/1024/1024)
fmt.Printf("吞吐量: %.2f MB/s\n", throughputMB)
}
}
func printSummary(results map[string]*Metrics) {
fmt.Printf("\n🏆 性能汇总报告\n")
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("%-15s %-12s %-10s %-12s %-10s\n", "接口", "RPS", "成功率", "平均延迟", "吞吐量")
fmt.Println(strings.Repeat("-", 60))
for name, metrics := range results {
duration := metrics.EndTime.Sub(metrics.StartTime)
rps := float64(metrics.TotalRequests) / duration.Seconds()
successRate := float64(metrics.SuccessRequests) / float64(metrics.TotalRequests) * 100
avgLatency := time.Duration(metrics.TotalLatencyNs / metrics.TotalRequests)
throughput := float64(metrics.TotalBytes) / 1024 / 1024 / duration.Seconds()
fmt.Printf("%-15s %-12.0f %-9.1f%% %-12v %-8.1fMB/s\n",
name, rps, successRate, avgLatency, throughput)
}
fmt.Println("\n✅ 测试完成!")
}
+1 -1
View File
@@ -3,7 +3,7 @@
module main
import time
import hono
import meiseayoung.hono
import net.http
const iterations = 100_000
+1 -1
View File
@@ -4,7 +4,7 @@
module main
import time
import hono
import meiseayoung.hono
import net.http
// ============================================
+102
View File
@@ -0,0 +1,102 @@
package main
import (
"flag"
"fmt"
"io"
"math/rand"
"net/http"
"sort"
"sync"
"sync/atomic"
"time"
)
func main() {
url := flag.String("url", "http://127.0.0.1:3000", "Target base URL")
connections := flag.Int("c", 500, "Number of connections")
requests := flag.Int("n", 1000000, "Total number of requests")
flag.Parse()
fmt.Printf("Benchmarking %s/users/:id\n", *url)
fmt.Printf("Connections: %d, Requests: %d\n\n", *connections, *requests)
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: *connections,
MaxIdleConnsPerHost: *connections,
MaxConnsPerHost: *connections,
IdleConnTimeout: 90 * time.Second,
},
Timeout: 10 * time.Second,
}
var completed int64
var errors int64
var latencies []int64
var mu sync.Mutex
totalReqs := int64(*requests)
start := time.Now()
var wg sync.WaitGroup
for i := 0; i < *connections; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
if atomic.LoadInt64(&completed)+atomic.LoadInt64(&errors) >= totalReqs {
return
}
reqStart := time.Now()
userId := rand.Intn(10000)
targetUrl := fmt.Sprintf("%s/users/%d", *url, userId)
resp, err := client.Get(targetUrl)
if err != nil {
atomic.AddInt64(&errors, 1)
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
latency := time.Since(reqStart).Microseconds()
mu.Lock()
latencies = append(latencies, latency)
mu.Unlock()
atomic.AddInt64(&completed, 1)
}
}()
}
wg.Wait()
elapsed := time.Since(start).Seconds()
total := atomic.LoadInt64(&completed)
errs := atomic.LoadInt64(&errors)
rps := float64(total) / elapsed
mu.Lock()
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
var avg, p50, p95, p99 int64
if len(latencies) > 0 {
var sum int64
for _, l := range latencies {
sum += l
}
avg = sum / int64(len(latencies))
p50 = latencies[len(latencies)*50/100]
p95 = latencies[len(latencies)*95/100]
p99 = latencies[len(latencies)*99/100]
}
mu.Unlock()
fmt.Println("Results:")
fmt.Printf(" Requests: %d\n", total)
fmt.Printf(" Errors: %d\n", errs)
fmt.Printf(" Duration: %.2fs\n", elapsed)
fmt.Printf(" RPS: %.0f\n", rps)
fmt.Printf(" Latency avg: %.2fms\n", float64(avg)/1000)
fmt.Printf(" Latency p50: %.2fms\n", float64(p50)/1000)
fmt.Printf(" Latency p95: %.2fms\n", float64(p95)/1000)
fmt.Printf(" Latency p99: %.2fms\n", float64(p99)/1000)
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
import time
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
import os
import time
+380
View File
@@ -0,0 +1,380 @@
import meiseayoung.hono
import net.http
import rand
import time
// Bearer Auth Middleware 属性测试
// Property-Based Testing for Bearer Auth functionality
const test_iterations = 100
struct BearerAuthTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats BearerAuthTestStats) run_property_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 BearerAuthTestStats) print_summary() {
println('\n=== Bearer Auth Middleware 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// 生成随机的 token
fn generate_random_token() string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(16, 64) or { 32 }
mut token := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
token += chars[idx].ascii_str()
}
return token
}
// 生成随机的 realm
fn generate_random_realm() string {
realms := ['api', 'admin', 'user', 'protected', 'secure', 'private']
idx := rand.int_in_range(0, realms.len) or { 0 }
return realms[idx]
}
// 生成随机的 prefix
fn generate_random_prefix() string {
prefixes := ['Bearer', 'Token', 'ApiKey', 'Auth']
idx := rand.int_in_range(0, prefixes.len) or { 0 }
return prefixes[idx]
}
// 创建带 Authorization 头的测试 Context
fn create_bearer_context_with_auth(auth_header string) hono.Context {
mut headers := http.new_header()
if auth_header.len > 0 {
headers.add_custom('Authorization', auth_header) or {}
}
req := http.Request{
method: .get
url: '/api/protected'
header: headers
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 创建带自定义 header 的测试 Context
fn create_bearer_context_with_custom_header(header_name string, header_value string) hono.Context {
mut headers := http.new_header()
if header_value.len > 0 {
headers.add_custom(header_name, header_value) or {}
}
req := http.Request{
method: .get
url: '/api/protected'
header: headers
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 模拟 next 函数,返回一个简单的响应
fn bearer_mock_next(mut c hono.Context) http.Response {
c.status(200)
return c.text('OK')
}
// ============================================================================
// Property 9: Bearer Token Validation
// Feature: builtin-middleware, Property 9: Bearer Token Validation
// Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10
//
// *For any* configured token(s), only requests with matching Bearer token in
// Authorization header SHALL be allowed to proceed.
// ============================================================================
fn test_property_9_bearer_token_validation() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
// Test 1: Valid single token should be allowed
for _ in 0 .. test_iterations / 5 {
valid_token := generate_random_token()
auth_header := 'Bearer ${valid_token}'
mut ctx := create_bearer_context_with_auth(auth_header)
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: valid_token
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 200 {
println(' Single token test failed: valid token rejected, status ${response.status_code}')
return false
}
// Verify token is stored in context
stored_token := ctx.get('bearer_token') or { '' }
if stored_token != valid_token {
println(' Single token test failed: token not stored in context')
return false
}
}
// Test 2: Invalid single token should be rejected
for _ in 0 .. test_iterations / 5 {
valid_token := generate_random_token()
invalid_token := generate_random_token()
// Ensure tokens are different
if valid_token == invalid_token {
continue
}
auth_header := 'Bearer ${invalid_token}'
mut ctx := create_bearer_context_with_auth(auth_header)
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: valid_token
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 401 {
println(' Invalid token test failed: invalid token accepted, status ${response.status_code}')
return false
}
// Verify WWW-Authenticate header is set
www_auth := response.header.get_custom('WWW-Authenticate') or { '' }
if !www_auth.starts_with('Bearer') {
println(' Invalid token test failed: WWW-Authenticate header not set correctly')
return false
}
}
// Test 3: Multiple tokens - any valid token should be allowed
for _ in 0 .. test_iterations / 5 {
tokens := [generate_random_token(), generate_random_token(), generate_random_token()]
// Pick a random valid token
valid_idx := rand.int_in_range(0, tokens.len) or { 0 }
auth_header := 'Bearer ${tokens[valid_idx]}'
mut ctx := create_bearer_context_with_auth(auth_header)
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: tokens
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 200 {
println(' Multiple tokens test failed: valid token rejected, status ${response.status_code}')
return false
}
}
// Test 4: Missing Authorization header should be rejected
for _ in 0 .. test_iterations / 5 {
valid_token := generate_random_token()
mut ctx := create_bearer_context_with_auth('')
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: valid_token
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 401 {
println(' Missing header test failed: request without auth accepted, status ${response.status_code}')
return false
}
// Verify WWW-Authenticate header is set
www_auth := response.header.get_custom('WWW-Authenticate') or { '' }
if !www_auth.starts_with('Bearer') {
println(' Missing header test failed: WWW-Authenticate header not set')
return false
}
}
// Test 5: Custom prefix should work correctly
for _ in 0 .. test_iterations / 5 {
valid_token := generate_random_token()
custom_prefix := generate_random_prefix()
auth_header := '${custom_prefix} ${valid_token}'
mut ctx := create_bearer_context_with_auth(auth_header)
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: valid_token
prefix: custom_prefix
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 200 {
println(' Custom prefix test failed: valid token with custom prefix rejected')
return false
}
}
return true
}
// Test custom verify_token callback
fn test_property_9_custom_verify_callback() bool {
rand.seed([u32(time.now().unix()), u32(54321)])
for _ in 0 .. test_iterations / 2 {
// Generate a token that starts with "valid_"
valid_token := 'valid_${generate_random_token()}'
auth_header := 'Bearer ${valid_token}'
mut ctx := create_bearer_context_with_auth(auth_header)
// Custom verify function that only accepts tokens starting with "valid_"
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: ''
verify_token: fn (token string, c hono.Context) bool {
return token.starts_with('valid_')
}
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 200 {
println(' Custom verify callback test failed: valid token rejected')
return false
}
}
for _ in 0 .. test_iterations / 2 {
// Generate a token that does NOT start with "valid_"
invalid_token := 'invalid_${generate_random_token()}'
auth_header := 'Bearer ${invalid_token}'
mut ctx := create_bearer_context_with_auth(auth_header)
// Custom verify function that only accepts tokens starting with "valid_"
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: ''
verify_token: fn (token string, c hono.Context) bool {
return token.starts_with('valid_')
}
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 401 {
println(' Custom verify callback test failed: invalid token accepted')
return false
}
}
return true
}
// Test realm configuration
fn test_property_9_realm_configuration() bool {
rand.seed([u32(time.now().unix()), u32(98765)])
for _ in 0 .. test_iterations {
valid_token := generate_random_token()
invalid_token := generate_random_token()
realm := generate_random_realm()
// Ensure tokens are different
if valid_token == invalid_token {
continue
}
auth_header := 'Bearer ${invalid_token}'
mut ctx := create_bearer_context_with_auth(auth_header)
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: valid_token
realm: realm
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 401 {
println(' Realm test failed: invalid token accepted')
return false
}
// Verify WWW-Authenticate header includes realm
www_auth := response.header.get_custom('WWW-Authenticate') or { '' }
expected_www_auth := 'Bearer realm="${realm}"'
if www_auth != expected_www_auth {
println(' Realm test failed: expected "${expected_www_auth}", got "${www_auth}"')
return false
}
}
return true
}
// Test custom header name
fn test_property_9_custom_header_name() bool {
rand.seed([u32(time.now().unix()), u32(11111)])
custom_headers := ['X-API-Key', 'X-Auth-Token', 'X-Access-Token']
for _ in 0 .. test_iterations {
valid_token := generate_random_token()
header_idx := rand.int_in_range(0, custom_headers.len) or { 0 }
custom_header := custom_headers[header_idx]
header_value := 'Bearer ${valid_token}'
mut ctx := create_bearer_context_with_custom_header(custom_header, header_value)
bearer_mw := hono.bearer_auth(hono.BearerAuthOptions{
token: valid_token
header_name: custom_header
})
response := bearer_mw(mut ctx, bearer_mock_next)
if response.status_code != 200 {
println(' Custom header name test failed: valid token rejected with custom header "${custom_header}"')
return false
}
}
return true
}
fn main() {
println('🚀 开始 Bearer Auth Middleware 属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := BearerAuthTestStats{}
// 运行属性测试
// Feature: builtin-middleware, Property 9: Bearer Token Validation
// Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10
stats.run_property_test('Property 9: Bearer Token Validation', test_property_9_bearer_token_validation)
stats.run_property_test('Property 9: Custom Verify Callback', test_property_9_custom_verify_callback)
stats.run_property_test('Property 9: Realm Configuration', test_property_9_realm_configuration)
stats.run_property_test('Property 9: Custom Header Name', test_property_9_custom_header_name)
// 打印测试总结
stats.print_summary()
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
fn main() {
println('=== 测试缓存内存泄漏修复 ===')
+81
View File
@@ -0,0 +1,81 @@
// 测试缓存命中率
module main
import meiseayoung.hono
import net.http
import time
fn main() {
println('=== 缓存命中率测试 ===')
mut app := hono.Hono.new()
app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id'] or { 'unknown' }
return c.json('{"user_id": "${user_id}"}')
})
// 获取 FastRouter 引用
mut router := app.fast_router
// 预热:先匹配一次让缓存生效
println('\n1. 预热阶段')
_ := router.match_route('GET', '/users/123')
// 获取初始统计
cache_size, cache_capacity := router.get_cache_stats()
println(' 缓存大小: ${cache_size}/${cache_capacity}')
// 测试相同路径的匹配
println('\n2. 测试相同路径 /users/123 匹配 1000 次')
sw := time.new_stopwatch()
for _ in 0 .. 1000 {
_ := router.match_route('GET', '/users/123')
}
elapsed := sw.elapsed()
println(' 耗时: ${elapsed.microseconds()}μs')
println(' 平均: ${elapsed.microseconds() / 1000}μs/次')
// 获取缓存统计
cache_size2, _ := router.get_cache_stats()
println(' 缓存大小: ${cache_size2}')
// 测试不同路径
println('\n3. 测试不同路径 /users/1 到 /users/1000')
sw2 := time.new_stopwatch()
for i in 1 .. 1001 {
_ := router.match_route('GET', '/users/${i}')
}
elapsed2 := sw2.elapsed()
println(' 耗时: ${elapsed2.microseconds()}μs')
println(' 平均: ${elapsed2.microseconds() / 1000}μs/次')
// 获取最终缓存统计
cache_size3, _ := router.get_cache_stats()
println(' 缓存大小: ${cache_size3}')
// 再次测试相同路径(应该命中缓存)
println('\n4. 再次测试 /users/500(应该命中缓存)')
sw3 := time.new_stopwatch()
for _ in 0 .. 1000 {
_ := router.match_route('GET', '/users/500')
}
elapsed3 := sw3.elapsed()
println(' 耗时: ${elapsed3.microseconds()}μs')
println(' 平均: ${elapsed3.microseconds() / 1000}μs/次')
// 详细统计
println('\n5. 详细统计')
stats := router.get_detailed_stats()
for key, value in stats {
println(' ${key}: ${value}')
}
println('\n=== 测试完成 ===')
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import strings
import net.http
+435
View File
@@ -0,0 +1,435 @@
import compress.gzip
import compress.zlib
import rand
import time
// Compression Middleware 属性测试
// Property-Based Testing for Compression functionality
//
// Note: These tests validate the compression round-trip properties
// using V's standard library compression functions, which are the
// same functions used by the compress middleware.
const test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_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 PropertyTestStats) print_summary() {
println('\n=== Compression Middleware 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// 生成随机字符串(用于测试压缩)
fn generate_random_string(min_len int, max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 \t\n!@#\$%^&*()_+-=[]{}|;:,.<>?'
len := rand.int_in_range(min_len, max_len) or { min_len }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx].ascii_str()
}
return result
}
// 生成可压缩的文本(重复模式更容易压缩)
fn generate_compressible_text(min_len int, max_len int) string {
patterns := [
'Hello World! ',
'This is a test. ',
'Lorem ipsum dolor sit amet. ',
'The quick brown fox jumps over the lazy dog. ',
'AAAAAAAAAA',
'1234567890',
]
target_len := rand.int_in_range(min_len, max_len) or { min_len }
mut result := ''
for result.len < target_len {
idx := rand.int_in_range(0, patterns.len) or { 0 }
result += patterns[idx]
}
if result.len > target_len {
return result[..target_len]
}
return result
}
// ============================================================================
// Property 10: Compression Round-Trip
// Feature: builtin-middleware, Property 10: Compression Round-Trip
// Validates: Requirements 5.1, 5.2, 5.6
//
// *For any* response body and supported encoding (gzip/deflate),
// compressing then decompressing SHALL produce the original body.
// ============================================================================
fn test_property_10_gzip_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
for i in 0 .. test_iterations {
// Generate random data
original := generate_random_string(100, 5000)
// Test gzip round-trip
gzip_compressed := gzip.compress(original.bytes(), gzip.CompressParams{
compression_level: 128
}) or {
println(' Iteration ${i}: Gzip compression failed: ${err}')
return false
}
gzip_decompressed := gzip.decompress(gzip_compressed) or {
println(' Iteration ${i}: Gzip decompression failed: ${err}')
return false
}
if gzip_decompressed.bytestr() != original {
println(' Iteration ${i}: Gzip round-trip mismatch')
println(' Original length: ${original.len}')
println(' Decompressed length: ${gzip_decompressed.len}')
return false
}
}
return true
}
fn test_property_10_deflate_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(54321)])
for i in 0 .. test_iterations {
// Generate random data
original := generate_random_string(100, 5000)
// Test deflate round-trip
deflate_compressed := zlib.compress(original.bytes()) or {
println(' Iteration ${i}: Deflate compression failed: ${err}')
return false
}
deflate_decompressed := zlib.decompress(deflate_compressed) or {
println(' Iteration ${i}: Deflate decompression failed: ${err}')
return false
}
if deflate_decompressed.bytestr() != original {
println(' Iteration ${i}: Deflate round-trip mismatch')
println(' Original length: ${original.len}')
println(' Decompressed length: ${deflate_decompressed.len}')
return false
}
}
return true
}
fn test_property_10_compressible_text_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(98765)])
for i in 0 .. test_iterations {
original := generate_compressible_text(500, 3000)
// Test gzip with compressible text
gzip_compressed := gzip.compress(original.bytes(), gzip.CompressParams{
compression_level: 128
}) or {
println(' Iteration ${i}: Gzip compression failed')
return false
}
gzip_decompressed := gzip.decompress(gzip_compressed) or {
println(' Iteration ${i}: Gzip decompression failed: ${err}')
return false
}
if gzip_decompressed.bytestr() != original {
println(' Iteration ${i}: Gzip compressible text round-trip mismatch')
return false
}
// Test deflate with compressible text
deflate_compressed := zlib.compress(original.bytes()) or {
println(' Iteration ${i}: Deflate compression failed')
return false
}
deflate_decompressed := zlib.decompress(deflate_compressed) or {
println(' Iteration ${i}: Deflate decompression failed: ${err}')
return false
}
if deflate_decompressed.bytestr() != original {
println(' Iteration ${i}: Deflate compressible text round-trip mismatch')
return false
}
}
return true
}
// ============================================================================
// Property 11: Compression Threshold Enforcement
// Feature: builtin-middleware, Property 11: Compression Threshold Enforcement
// Validates: Requirements 5.7
//
// *For any* response body smaller than the configured threshold,
// the Compression middleware SHALL NOT compress the response.
//
// This test validates the threshold logic by checking that:
// 1. Small data (below threshold) should not be compressed
// 2. Large data (above threshold) can be compressed
// ============================================================================
fn test_property_11_threshold_logic() bool {
rand.seed([u32(time.now().unix()), u32(11111)])
// Test various threshold values
thresholds := [512, 1024, 2048, 4096]
for threshold in thresholds {
for i in 0 .. 25 {
// Generate data smaller than threshold
small_data_len := rand.int_in_range(1, threshold) or { threshold / 2 }
small_data := generate_random_string(small_data_len, small_data_len + 1)
// Verify the data is indeed smaller than threshold
if small_data.len >= threshold {
continue // Skip if data is not smaller
}
// For small data, the middleware would skip compression
// We verify that the threshold check works correctly
should_compress := small_data.len >= threshold
if should_compress {
println(' Threshold logic error: small data (${small_data.len}) should not be compressed with threshold ${threshold}')
return false
}
// Generate data larger than threshold
large_data_len := rand.int_in_range(threshold + 100, threshold * 2) or { threshold + 500 }
large_data := generate_compressible_text(large_data_len, large_data_len + 1)
// Verify the data is indeed larger than threshold
if large_data.len < threshold {
continue // Skip if data is not larger
}
// For large data, compression should be attempted
should_compress_large := large_data.len >= threshold
if !should_compress_large {
println(' Threshold logic error: large data (${large_data.len}) should be compressed with threshold ${threshold}')
return false
}
// Verify compression actually works for large data
compressed := gzip.compress(large_data.bytes(), gzip.CompressParams{
compression_level: 128
}) or {
println(' Iteration ${i}: Compression of large data failed')
return false
}
// Verify we can decompress it back
decompressed := gzip.decompress(compressed) or {
println(' Iteration ${i}: Decompression of large data failed')
return false
}
if decompressed.bytestr() != large_data {
println(' Iteration ${i}: Large data round-trip failed')
return false
}
}
}
return true
}
fn test_property_11_boundary_conditions() bool {
rand.seed([u32(time.now().unix()), u32(22222)])
threshold := 1024
for i in 0 .. test_iterations {
// Test data exactly at threshold boundary
// Data at exactly threshold should be compressed (>= threshold)
exact_data := generate_compressible_text(threshold, threshold + 1)
// Verify exact boundary
if exact_data.len < threshold {
continue
}
should_compress_exact := exact_data.len >= threshold
if !should_compress_exact {
println(' Iteration ${i}: Boundary condition error at exact threshold')
return false
}
// Test data just below threshold
below_threshold := threshold - 1
below_data := generate_random_string(below_threshold, below_threshold + 1)
if below_data.len >= threshold {
continue
}
should_compress_below := below_data.len >= threshold
if should_compress_below {
println(' Iteration ${i}: Boundary condition error below threshold')
return false
}
// Test data just above threshold
above_data := generate_compressible_text(threshold + 1, threshold + 100)
if above_data.len <= threshold {
continue
}
should_compress_above := above_data.len >= threshold
if !should_compress_above {
println(' Iteration ${i}: Boundary condition error above threshold')
return false
}
}
return true
}
// Unit tests for basic compression functionality
fn test_gzip_basic() {
original := 'Hello, World! This is a test string for compression.'
compressed := gzip.compress(original.bytes(), gzip.CompressParams{
compression_level: 128
}) or {
assert false, 'Gzip compression failed: ${err}'
return
}
decompressed := gzip.decompress(compressed) or {
assert false, 'Gzip decompression failed: ${err}'
return
}
assert decompressed.bytestr() == original, 'Gzip round-trip failed'
}
fn test_deflate_basic() {
original := 'Hello, World! This is a test string for compression.'
compressed := zlib.compress(original.bytes()) or {
assert false, 'Deflate compression failed: ${err}'
return
}
decompressed := zlib.decompress(compressed) or {
assert false, 'Deflate decompression failed: ${err}'
return
}
assert decompressed.bytestr() == original, 'Deflate round-trip failed'
}
fn test_compression_reduces_size() {
// Highly compressible data
original := 'AAAAAAAAAA'.repeat(1000)
gzip_compressed := gzip.compress(original.bytes(), gzip.CompressParams{
compression_level: 128
}) or {
assert false, 'Gzip compression failed'
return
}
deflate_compressed := zlib.compress(original.bytes()) or {
assert false, 'Deflate compression failed'
return
}
// Compressed size should be smaller than original
assert gzip_compressed.len < original.len, 'Gzip should reduce size for compressible data'
assert deflate_compressed.len < original.len, 'Deflate should reduce size for compressible data'
}
fn test_empty_data_compression() {
original := ''
// Gzip should handle empty data
gzip_compressed := gzip.compress(original.bytes(), gzip.CompressParams{
compression_level: 128
}) or {
assert false, 'Gzip compression of empty data failed: ${err}'
return
}
gzip_decompressed := gzip.decompress(gzip_compressed) or {
assert false, 'Gzip decompression of empty data failed: ${err}'
return
}
assert gzip_decompressed.bytestr() == original, 'Gzip empty data round-trip failed'
// Deflate should handle empty data
deflate_compressed := zlib.compress(original.bytes()) or {
assert false, 'Deflate compression of empty data failed: ${err}'
return
}
deflate_decompressed := zlib.decompress(deflate_compressed) or {
assert false, 'Deflate decompression of empty data failed: ${err}'
return
}
assert deflate_decompressed.bytestr() == original, 'Deflate empty data round-trip failed'
}
// Main function for running property tests
fn main() {
println('🚀 开始 Compression Middleware 属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := PropertyTestStats{}
// Run property tests
// Feature: builtin-middleware, Property 10: Compression Round-Trip
// Validates: Requirements 5.1, 5.2, 5.6
stats.run_property_test('Property 10: Gzip Round-Trip', test_property_10_gzip_roundtrip)
stats.run_property_test('Property 10: Deflate Round-Trip', test_property_10_deflate_roundtrip)
stats.run_property_test('Property 10: Compressible Text Round-Trip', test_property_10_compressible_text_roundtrip)
// Feature: builtin-middleware, Property 11: Compression Threshold Enforcement
// Validates: Requirements 5.7
stats.run_property_test('Property 11: Threshold Logic', test_property_11_threshold_logic)
stats.run_property_test('Property 11: Boundary Conditions', test_property_11_boundary_conditions)
// Print test summary
stats.print_summary()
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import os
fn test_default_config() {
+237
View File
@@ -0,0 +1,237 @@
import meiseayoung.hono
import net.http
// Context Store 功能测试
// 测试 Context 的 store 字段和 get/set/get_client_ip 方法
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=== Context Store 测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个测试失败')
}
}
// 创建测试用的 Context
fn create_test_context() hono.Context {
req := http.Request{
method: .get
url: '/test'
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 测试 1: 基本的 set 和 get 操作
fn test_basic_set_get() bool {
mut ctx := create_test_context()
// 设置值
ctx.set('user_id', '12345')
ctx.set('role', 'admin')
// 获取值
if user_id := ctx.get('user_id') {
if user_id != '12345' {
return false
}
} else {
return false
}
if role := ctx.get('role') {
if role != 'admin' {
return false
}
} else {
return false
}
return true
}
// 测试 2: 获取不存在的 key 返回 none
fn test_get_nonexistent_key() bool {
ctx := create_test_context()
// 获取不存在的 key
if _ := ctx.get('nonexistent') {
return false // 应该返回 none
}
return true
}
// 测试 3: 覆盖已存在的值
fn test_overwrite_value() bool {
mut ctx := create_test_context()
// 设置初始值
ctx.set('key', 'value1')
// 覆盖值
ctx.set('key', 'value2')
// 验证新值
if val := ctx.get('key') {
return val == 'value2'
}
return false
}
// 测试 4: 空字符串值
fn test_empty_string_value() bool {
mut ctx := create_test_context()
// 设置空字符串
ctx.set('empty', '')
// 获取空字符串
if val := ctx.get('empty') {
return val == ''
}
return false
}
// 测试 5: 多个键值对
fn test_multiple_keys() bool {
mut ctx := create_test_context()
// 设置多个值
for i in 0 .. 10 {
ctx.set('key${i}', 'value${i}')
}
// 验证所有值
for i in 0 .. 10 {
if val := ctx.get('key${i}') {
if val != 'value${i}' {
return false
}
} else {
return false
}
}
return true
}
// 测试 6: get_client_ip 默认返回值
fn test_get_client_ip_default() bool {
ctx := create_test_context()
// 没有设置任何 IP 相关头时,应返回默认值
ip := ctx.get_client_ip()
return ip == '127.0.0.1'
}
// 测试 7: get_client_ip 从 X-Forwarded-For 获取
fn test_get_client_ip_forwarded_for() bool {
mut headers := http.new_header()
headers.add_custom('X-Forwarded-For', '192.168.1.100, 10.0.0.1') or { return false }
req := http.Request{
method: .get
url: '/test'
header: headers
}
ctx := hono.Context.new(req, map[string]string{}, map[string]string{}, '')
// 应该返回第一个 IP
ip := ctx.get_client_ip()
return ip == '192.168.1.100'
}
// 测试 8: get_client_ip 从 X-Real-IP 获取
fn test_get_client_ip_real_ip() bool {
mut headers := http.new_header()
headers.add_custom('X-Real-IP', '10.20.30.40') or { return false }
req := http.Request{
method: .get
url: '/test'
header: headers
}
ctx := hono.Context.new(req, map[string]string{}, map[string]string{}, '')
ip := ctx.get_client_ip()
return ip == '10.20.30.40'
}
// 测试 9: X-Forwarded-For 优先于 X-Real-IP
fn test_get_client_ip_priority() bool {
mut headers := http.new_header()
headers.add_custom('X-Forwarded-For', '1.2.3.4') or { return false }
headers.add_custom('X-Real-IP', '5.6.7.8') or { return false }
req := http.Request{
method: .get
url: '/test'
header: headers
}
ctx := hono.Context.new(req, map[string]string{}, map[string]string{}, '')
// X-Forwarded-For 应该优先
ip := ctx.get_client_ip()
return ip == '1.2.3.4'
}
// 测试 10: store 初始化为空
fn test_store_initialized_empty() bool {
ctx := create_test_context()
// 新创建的 Context 的 store 应该为空
// 尝试获取任何 key 都应该返回 none
if _ := ctx.get('any_key') {
return false
}
return true
}
fn main() {
println('🚀 开始 Context Store 功能测试...\n')
mut stats := TestStats{}
// 运行所有测试
stats.run_test('基本 set/get 操作', test_basic_set_get)
stats.run_test('获取不存在的 key', test_get_nonexistent_key)
stats.run_test('覆盖已存在的值', test_overwrite_value)
stats.run_test('空字符串值', test_empty_string_value)
stats.run_test('多个键值对', test_multiple_keys)
stats.run_test('get_client_ip 默认值', test_get_client_ip_default)
stats.run_test('get_client_ip X-Forwarded-For', test_get_client_ip_forwarded_for)
stats.run_test('get_client_ip X-Real-IP', test_get_client_ip_real_ip)
stats.run_test('X-Forwarded-For 优先级', test_get_client_ip_priority)
stats.run_test('store 初始化为空', test_store_initialized_empty)
// 打印测试总结
stats.print_summary()
}
+320
View File
@@ -0,0 +1,320 @@
import meiseayoung.hono
import net.http
// Cookie Helper 功能测试
// 测试 Cookie 的 get/set/delete 和签名功能
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=== Cookie Helper 测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个测试失败')
}
}
// 创建带 Cookie 头的测试 Context
fn create_test_context_with_cookies(cookie_header string) hono.Context {
mut headers := http.new_header()
if cookie_header.len > 0 {
headers.add_custom('Cookie', cookie_header) or {}
}
req := http.Request{
method: .get
url: '/test'
header: headers
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 创建空的测试 Context
fn create_empty_context() hono.Context {
req := http.Request{
method: .get
url: '/test'
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 测试 1: 获取单个 Cookie
fn test_get_single_cookie() bool {
ctx := create_test_context_with_cookies('session_id=abc123')
if value := hono.get_cookie(ctx, 'session_id') {
return value == 'abc123'
}
return false
}
// 测试 2: 获取不存在的 Cookie 返回 none
fn test_get_nonexistent_cookie() bool {
ctx := create_test_context_with_cookies('session_id=abc123')
if _ := hono.get_cookie(ctx, 'nonexistent') {
return false // 应该返回 none
}
return true
}
// 测试 3: 获取多个 Cookie
fn test_get_multiple_cookies() bool {
ctx := create_test_context_with_cookies('session_id=abc123; user_id=456; theme=dark')
session := hono.get_cookie(ctx, 'session_id') or { return false }
user := hono.get_cookie(ctx, 'user_id') or { return false }
theme := hono.get_cookie(ctx, 'theme') or { return false }
return session == 'abc123' && user == '456' && theme == 'dark'
}
// 测试 4: 获取所有 Cookie
fn test_get_all_cookies() bool {
ctx := create_test_context_with_cookies('a=1; b=2; c=3')
cookies := hono.get_all_cookies(ctx)
return cookies.len == 3 &&
cookies['a'] == '1' &&
cookies['b'] == '2' &&
cookies['c'] == '3'
}
// 测试 5: 空 Cookie 头返回空 map
fn test_get_all_cookies_empty() bool {
ctx := create_empty_context()
cookies := hono.get_all_cookies(ctx)
return cookies.len == 0
}
// 测试 6: 设置 Cookie
fn test_set_cookie_basic() bool {
mut ctx := create_empty_context()
hono.set_cookie(mut ctx, 'session', 'xyz789')
// 检查 Set-Cookie 头是否被设置
if set_cookie := ctx.headers['Set-Cookie'] {
return set_cookie.contains('session=xyz789')
}
return false
}
// 测试 7: 设置 Cookie 带选项
fn test_set_cookie_with_options() bool {
mut ctx := create_empty_context()
hono.set_cookie(mut ctx, 'token', 'secret123', hono.CookieOptions{
path: '/api'
http_only: true
secure: true
max_age: 3600
same_site: .strict
})
if set_cookie := ctx.headers['Set-Cookie'] {
return set_cookie.contains('token=secret123') &&
set_cookie.contains('Path=/api') &&
set_cookie.contains('HttpOnly') &&
set_cookie.contains('Secure') &&
set_cookie.contains('Max-Age=3600') &&
set_cookie.contains('SameSite=Strict')
}
return false
}
// 测试 8: 删除 Cookie
fn test_delete_cookie() bool {
mut ctx := create_empty_context()
hono.delete_cookie(mut ctx, 'session')
if set_cookie := ctx.headers['Set-Cookie'] {
// 删除 Cookie 应该设置 Max-Age=0 或过期时间
return set_cookie.contains('session=') &&
(set_cookie.contains('Max-Age=0') || set_cookie.contains('Expires='))
}
return false
}
// 测试 9: Cookie 值带空格
fn test_cookie_with_spaces() bool {
ctx := create_test_context_with_cookies('name=John Doe')
if value := hono.get_cookie(ctx, 'name') {
return value == 'John Doe'
}
return false
}
// 测试 10: Cookie 值带引号
fn test_cookie_with_quotes() bool {
ctx := create_test_context_with_cookies('data="hello world"')
if value := hono.get_cookie(ctx, 'data') {
return value == 'hello world'
}
return false
}
// 测试 11: 签名 Cookie 设置和获取
fn test_signed_cookie_roundtrip() bool {
mut ctx := create_empty_context()
secret := 'my-secret-key-12345'
// 设置签名 Cookie
hono.set_signed_cookie(mut ctx, 'auth', 'user123', secret) or {
println('Failed to set signed cookie: ${err}')
return false
}
// 获取 Set-Cookie 头中的值
set_cookie_header := ctx.headers['Set-Cookie'] or { return false }
// 提取 Cookie 值(格式: auth=value.signature; ...)
mut cookie_value := ''
parts := set_cookie_header.split(';')
if parts.len > 0 {
name_value := parts[0].trim_space()
eq_pos := name_value.index('=') or { return false }
cookie_value = name_value[eq_pos + 1..]
}
// 创建一个带有这个 Cookie 的新 Context 来验证
verify_ctx := create_test_context_with_cookies('auth=${cookie_value}')
// 验证签名 Cookie
if value := hono.get_signed_cookie(verify_ctx, 'auth', secret) {
return value == 'user123'
} else {
println('Failed to get signed cookie: ${err}')
return false
}
}
// 测试 12: 签名 Cookie 篡改检测
fn test_signed_cookie_tamper_detection() bool {
mut ctx := create_empty_context()
secret := 'my-secret-key-12345'
// 设置签名 Cookie
hono.set_signed_cookie(mut ctx, 'auth', 'user123', secret) or { return false }
// 获取 Set-Cookie 头中的值
set_cookie_header := ctx.headers['Set-Cookie'] or { return false }
// 提取 Cookie 值
mut cookie_value := ''
parts := set_cookie_header.split(';')
if parts.len > 0 {
name_value := parts[0].trim_space()
eq_pos := name_value.index('=') or { return false }
cookie_value = name_value[eq_pos + 1..]
}
// 篡改 Cookie 值
tampered_value := 'tampered' + cookie_value[8..] // 修改值部分
// 创建带篡改 Cookie 的 Context
verify_ctx := create_test_context_with_cookies('auth=${tampered_value}')
// 验证应该失败
if _ := hono.get_signed_cookie(verify_ctx, 'auth', secret) {
return false // 不应该成功
}
return true // 验证失败是预期的
}
// 测试 13: 签名 Cookie 错误密钥
fn test_signed_cookie_wrong_secret() bool {
mut ctx := create_empty_context()
// 使用一个密钥设置
hono.set_signed_cookie(mut ctx, 'auth', 'user123', 'secret1') or { return false }
// 获取 Set-Cookie 头中的值
set_cookie_header := ctx.headers['Set-Cookie'] or { return false }
// 提取 Cookie 值
mut cookie_value := ''
parts := set_cookie_header.split(';')
if parts.len > 0 {
name_value := parts[0].trim_space()
eq_pos := name_value.index('=') or { return false }
cookie_value = name_value[eq_pos + 1..]
}
// 创建带 Cookie 的 Context
verify_ctx := create_test_context_with_cookies('auth=${cookie_value}')
// 使用不同的密钥验证应该失败
if _ := hono.get_signed_cookie(verify_ctx, 'auth', 'secret2') {
return false // 不应该成功
}
return true // 验证失败是预期的
}
// 测试 14: 空密钥应该返回错误
fn test_signed_cookie_empty_secret() bool {
mut ctx := create_empty_context()
// 空密钥应该返回错误
if _ := hono.set_signed_cookie(mut ctx, 'auth', 'value', '') {
return false // 不应该成功
}
return true
}
fn main() {
println('🚀 开始 Cookie Helper 功能测试...\n')
mut stats := TestStats{}
// 运行所有测试
stats.run_test('获取单个 Cookie', test_get_single_cookie)
stats.run_test('获取不存在的 Cookie', test_get_nonexistent_cookie)
stats.run_test('获取多个 Cookie', test_get_multiple_cookies)
stats.run_test('获取所有 Cookie', test_get_all_cookies)
stats.run_test('空 Cookie 头返回空 map', test_get_all_cookies_empty)
stats.run_test('设置 Cookie 基本功能', test_set_cookie_basic)
stats.run_test('设置 Cookie 带选项', test_set_cookie_with_options)
stats.run_test('删除 Cookie', test_delete_cookie)
stats.run_test('Cookie 值带空格', test_cookie_with_spaces)
stats.run_test('Cookie 值带引号', test_cookie_with_quotes)
stats.run_test('签名 Cookie 往返一致性', test_signed_cookie_roundtrip)
stats.run_test('签名 Cookie 篡改检测', test_signed_cookie_tamper_detection)
stats.run_test('签名 Cookie 错误密钥', test_signed_cookie_wrong_secret)
stats.run_test('签名 Cookie 空密钥错误', test_signed_cookie_empty_secret)
// 打印测试总结
stats.print_summary()
}
+320
View File
@@ -0,0 +1,320 @@
import meiseayoung.hono
import net.http
import rand
import time
// Cookie Helper 属性测试
// Property-Based Testing for Cookie functionality
const test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_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 PropertyTestStats) print_summary() {
println('\n=== Cookie Helper 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// 创建带 Cookie 头的测试 Context
fn create_context_with_cookies(cookie_header string) hono.Context {
mut headers := http.new_header()
if cookie_header.len > 0 {
headers.add_custom('Cookie', cookie_header) or {}
}
req := http.Request{
method: .get
url: '/test'
header: headers
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 创建空的测试 Context
fn create_empty_context() hono.Context {
req := http.Request{
method: .get
url: '/test'
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 生成随机的 Cookie 名称(只包含有效字符)
fn generate_random_cookie_name() string {
valid_chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-'
len := rand.int_in_range(1, 20) or { 5 }
mut name := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, valid_chars.len) or { 0 }
name += valid_chars[idx].ascii_str()
}
return name
}
// 生成随机的 Cookie 值(不包含分号和等号)
fn generate_random_cookie_value() string {
// 使用安全的字符集,避免特殊字符
valid_chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.'
len := rand.int_in_range(1, 50) or { 10 }
mut value := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, valid_chars.len) or { 0 }
value += valid_chars[idx].ascii_str()
}
return value
}
// 生成随机的密钥
fn generate_random_secret() string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'
len := rand.int_in_range(16, 64) or { 32 }
mut secret := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
secret += chars[idx].ascii_str()
}
return secret
}
// ============================================================================
// Property 3: Cookie Round-Trip Consistency
// Feature: builtin-middleware, Property 3: Cookie Round-Trip Consistency
// Validates: Requirements 2.1, 2.3
//
// *For any* cookie name and value, calling set_cookie followed by get_cookie
// on the same name SHALL return the original value.
// ============================================================================
fn test_property_3_cookie_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
for i in 0 .. test_iterations {
name := generate_random_cookie_name()
value := generate_random_cookie_value()
// 设置 Cookie
mut ctx := create_empty_context()
hono.set_cookie(mut ctx, name, value)
// 获取 Set-Cookie 头中的值
set_cookie_header := ctx.headers['Set-Cookie'] or {
println(' Iteration ${i}: Failed to get Set-Cookie header')
return false
}
// 提取 Cookie 值
mut cookie_value := ''
parts := set_cookie_header.split(';')
if parts.len > 0 {
name_value := parts[0].trim_space()
eq_pos := name_value.index('=') or {
println(' Iteration ${i}: Invalid cookie format')
return false
}
cookie_value = name_value[eq_pos + 1..]
}
// 创建带 Cookie 的 Context 来验证
verify_ctx := create_context_with_cookies('${name}=${cookie_value}')
// 获取 Cookie 并验证
retrieved := hono.get_cookie(verify_ctx, name) or {
println(' Iteration ${i}: Failed to get cookie')
return false
}
if retrieved != value {
println(' Iteration ${i}: Value mismatch - expected "${value}", got "${retrieved}"')
return false
}
}
return true
}
// ============================================================================
// Property 4: Signed Cookie Round-Trip
// Feature: builtin-middleware, Property 4: Signed Cookie Round-Trip
// Validates: Requirements 2.7, 2.8
//
// *For any* cookie name, value, and secret, calling set_signed_cookie followed
// by get_signed_cookie with the same secret SHALL return the original value.
// ============================================================================
fn test_property_4_signed_cookie_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(54321)])
for i in 0 .. test_iterations {
name := generate_random_cookie_name()
value := generate_random_cookie_value()
secret := generate_random_secret()
// 设置签名 Cookie
mut ctx := create_empty_context()
hono.set_signed_cookie(mut ctx, name, value, secret) or {
println(' Iteration ${i}: Failed to set signed cookie: ${err}')
return false
}
// 获取 Set-Cookie 头中的值
set_cookie_header := ctx.headers['Set-Cookie'] or {
println(' Iteration ${i}: Failed to get Set-Cookie header')
return false
}
// 提取 Cookie 值(包含签名)
mut cookie_value := ''
parts := set_cookie_header.split(';')
if parts.len > 0 {
name_value := parts[0].trim_space()
eq_pos := name_value.index('=') or {
println(' Iteration ${i}: Invalid cookie format')
return false
}
cookie_value = name_value[eq_pos + 1..]
}
// 创建带签名 Cookie 的 Context 来验证
verify_ctx := create_context_with_cookies('${name}=${cookie_value}')
// 获取并验证签名 Cookie
retrieved := hono.get_signed_cookie(verify_ctx, name, secret) or {
println(' Iteration ${i}: Failed to get signed cookie: ${err}')
return false
}
if retrieved != value {
println(' Iteration ${i}: Value mismatch - expected "${value}", got "${retrieved}"')
return false
}
}
return true
}
// ============================================================================
// Property 5: Signed Cookie Tamper Detection
// Feature: builtin-middleware, Property 5: Signed Cookie Tamper Detection
// Validates: Requirements 2.9
//
// *For any* signed cookie, modifying the cookie value or signature SHALL cause
// get_signed_cookie to return an error.
// ============================================================================
fn test_property_5_signed_cookie_tamper_detection() bool {
rand.seed([u32(time.now().unix()), u32(98765)])
for i in 0 .. test_iterations {
name := generate_random_cookie_name()
value := generate_random_cookie_value()
secret := generate_random_secret()
// 设置签名 Cookie
mut ctx := create_empty_context()
hono.set_signed_cookie(mut ctx, name, value, secret) or {
println(' Iteration ${i}: Failed to set signed cookie: ${err}')
return false
}
// 获取 Set-Cookie 头中的值
set_cookie_header := ctx.headers['Set-Cookie'] or {
println(' Iteration ${i}: Failed to get Set-Cookie header')
return false
}
// 提取 Cookie 值(包含签名)
mut cookie_value := ''
parts := set_cookie_header.split(';')
if parts.len > 0 {
name_value := parts[0].trim_space()
eq_pos := name_value.index('=') or {
println(' Iteration ${i}: Invalid cookie format')
return false
}
cookie_value = name_value[eq_pos + 1..]
}
// 篡改 Cookie 值(修改第一个字符)
if cookie_value.len > 1 {
// 找到签名分隔符
dot_pos := cookie_value.last_index('.') or {
println(' Iteration ${i}: No signature separator found')
return false
}
// 篡改值部分
original_value_part := cookie_value[..dot_pos]
signature_part := cookie_value[dot_pos..]
// 修改值的第一个字符
mut tampered_value := ''
if original_value_part.len > 0 {
first_char := original_value_part[0]
new_char := if first_char == `a` { 'b' } else { 'a' }
tampered_value = new_char + original_value_part[1..] + signature_part
} else {
tampered_value = 'x' + signature_part
}
// 创建带篡改 Cookie 的 Context
verify_ctx := create_context_with_cookies('${name}=${tampered_value}')
// 验证应该失败
if _ := hono.get_signed_cookie(verify_ctx, name, secret) {
println(' Iteration ${i}: Tampered cookie was accepted (should have been rejected)')
return false
}
}
}
return true
}
fn main() {
println('🚀 开始 Cookie Helper 属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := PropertyTestStats{}
// 运行属性测试
// Feature: builtin-middleware, Property 3: Cookie Round-Trip Consistency
// Validates: Requirements 2.1, 2.3
stats.run_property_test('Property 3: Cookie Round-Trip Consistency', test_property_3_cookie_roundtrip)
// Feature: builtin-middleware, Property 4: Signed Cookie Round-Trip
// Validates: Requirements 2.7, 2.8
stats.run_property_test('Property 4: Signed Cookie Round-Trip', test_property_4_signed_cookie_roundtrip)
// Feature: builtin-middleware, Property 5: Signed Cookie Tamper Detection
// Validates: Requirements 2.9
stats.run_property_test('Property 5: Signed Cookie Tamper Detection', test_property_5_signed_cookie_tamper_detection)
// 打印测试总结
stats.print_summary()
}
+341
View File
@@ -0,0 +1,341 @@
import meiseayoung.hono
import net.http
import rand
import time
// CORS Middleware 属性测试
// Property-Based Testing for CORS functionality
const test_iterations = 100
struct CorsTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats CorsTestStats) run_property_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 CorsTestStats) print_summary() {
println('\n=== CORS Middleware 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// 创建带 Origin 头的测试 Context
fn create_cors_context_with_origin(origin string, method http.Method) hono.Context {
mut headers := http.new_header()
if origin.len > 0 {
headers.add_custom('Origin', origin) or {}
}
req := http.Request{
method: method
url: '/test'
header: headers
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 创建带 Origin 和 Access-Control-Request-Headers 的预检请求 Context
fn create_cors_preflight_context(origin string, request_headers string) hono.Context {
mut headers := http.new_header()
if origin.len > 0 {
headers.add_custom('Origin', origin) or {}
}
if request_headers.len > 0 {
headers.add_custom('Access-Control-Request-Headers', request_headers) or {}
}
req := http.Request{
method: .options
url: '/test'
header: headers
}
return hono.Context.new(req, map[string]string{}, map[string]string{}, '')
}
// 生成随机的域名
fn generate_cors_random_origin() string {
protocols := ['http', 'https']
domains := ['example', 'test', 'api', 'app', 'web', 'service', 'backend', 'frontend']
tlds := ['com', 'org', 'net', 'io', 'dev', 'app']
protocol := protocols[rand.int_in_range(0, protocols.len) or { 0 }]
domain := domains[rand.int_in_range(0, domains.len) or { 0 }]
tld := tlds[rand.int_in_range(0, tlds.len) or { 0 }]
// 有时添加端口
port := if rand.int_in_range(0, 3) or { 0 } == 0 {
':${rand.int_in_range(3000, 9000) or { 8080 }}'
} else {
''
}
return '${protocol}://${domain}.${tld}${port}'
}
// 生成随机的 HTTP 方法
fn generate_cors_random_method() http.Method {
methods := [http.Method.get, http.Method.post, http.Method.put, http.Method.delete, http.Method.patch]
return methods[rand.int_in_range(0, methods.len) or { 0 }]
}
// 生成随机的请求头列表
fn generate_cors_random_headers() []string {
all_headers := ['Content-Type', 'Authorization', 'X-Custom-Header', 'X-Request-ID', 'Accept', 'Cache-Control']
count := rand.int_in_range(1, all_headers.len) or { 2 }
mut result := []string{}
for i in 0 .. count {
result << all_headers[i]
}
return result
}
// 模拟 next 函数,返回一个简单的响应
fn cors_mock_next(mut c hono.Context) http.Response {
return c.text('OK')
}
// ============================================================================
// Property 1: CORS Origin Header Consistency
// Feature: builtin-middleware, Property 1: CORS Origin Header Consistency
// Validates: Requirements 1.1, 1.3, 1.4, 1.5, 1.6
//
// *For any* request with an Origin header and any CORS configuration, the
// Access-Control-Allow-Origin response header SHALL match the configured
// origin policy (wildcard "*", specific domain, or callback result).
// ============================================================================
fn test_property_1_cors_origin_header_consistency() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
// Test 1: Wildcard origin ("*") should allow all origins
for _ in 0 .. test_iterations / 4 {
origin := generate_cors_random_origin()
mut ctx := create_cors_context_with_origin(origin, generate_cors_random_method())
cors_mw := hono.cors()
_ := cors_mw(mut ctx, cors_mock_next)
allowed_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
if allowed_origin != '*' {
println(' Wildcard test failed: expected "*", got "${allowed_origin}"')
return false
}
}
// Test 2: Specific domain should only allow that domain
for _ in 0 .. test_iterations / 4 {
allowed_domain := generate_cors_random_origin()
request_origin := generate_cors_random_origin()
mut ctx := create_cors_context_with_origin(request_origin, generate_cors_random_method())
cors_mw := hono.cors(hono.CorsOptions{
origin: allowed_domain
})
_ := cors_mw(mut ctx, cors_mock_next)
result_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
if request_origin == allowed_domain {
// Should be allowed
if result_origin != request_origin {
println(' Specific domain test failed: expected "${request_origin}", got "${result_origin}"')
return false
}
} else {
// Should not be allowed (no header set)
if result_origin != '' {
println(' Specific domain test failed: expected empty, got "${result_origin}"')
return false
}
}
}
// Test 3: Array of domains should allow any domain in the array
for _ in 0 .. test_iterations / 4 {
allowed_domains := [generate_cors_random_origin(), generate_cors_random_origin(), generate_cors_random_origin()]
// Test with an allowed domain
allowed_idx := rand.int_in_range(0, allowed_domains.len) or { 0 }
request_origin := allowed_domains[allowed_idx]
mut ctx := create_cors_context_with_origin(request_origin, generate_cors_random_method())
cors_mw := hono.cors(hono.CorsOptions{
origin: allowed_domains
})
_ := cors_mw(mut ctx, cors_mock_next)
result_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
if result_origin != request_origin {
println(' Array domain test failed: expected "${request_origin}", got "${result_origin}"')
return false
}
}
// Test 4: Callback function should use callback result
for _ in 0 .. test_iterations / 4 {
request_origin := generate_cors_random_origin()
mut ctx := create_cors_context_with_origin(request_origin, generate_cors_random_method())
// Callback that returns the origin if it starts with "https"
cors_mw := hono.cors(hono.CorsOptions{
origin: fn (origin string, c hono.Context) string {
if origin.starts_with('https') {
return origin
}
return ''
}
})
_ := cors_mw(mut ctx, cors_mock_next)
result_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
if request_origin.starts_with('https') {
if result_origin != request_origin {
println(' Callback test failed: expected "${request_origin}", got "${result_origin}"')
return false
}
} else {
if result_origin != '' {
println(' Callback test failed: expected empty for non-https, got "${result_origin}"')
return false
}
}
}
return true
}
// ============================================================================
// Property 2: CORS Preflight Response
// Feature: builtin-middleware, Property 2: CORS Preflight Response
// Validates: Requirements 1.2, 1.7, 1.8, 1.9, 1.10, 1.11
//
// *For any* OPTIONS preflight request with Origin header, the CORS middleware
// SHALL return status 204 with all configured CORS headers (Allow-Methods,
// Allow-Headers, Expose-Headers, Max-Age, Credentials).
// ============================================================================
fn test_property_2_cors_preflight_response() bool {
rand.seed([u32(time.now().unix()), u32(54321)])
for _ in 0 .. test_iterations {
origin := generate_cors_random_origin()
allow_methods := ['GET', 'POST', 'PUT', 'DELETE']
allow_headers := generate_cors_random_headers()
expose_headers := ['X-Custom-Response', 'X-Request-ID']
max_age := rand.int_in_range(60, 86400) or { 3600 }
credentials := rand.int_in_range(0, 2) or { 0 } == 1
mut ctx := create_cors_preflight_context(origin, allow_headers.join(', '))
cors_mw := hono.cors(hono.CorsOptions{
origin: '*'
allow_methods: allow_methods
allow_headers: allow_headers
expose_headers: expose_headers
max_age: max_age
credentials: credentials
})
response := cors_mw(mut ctx, cors_mock_next)
// Check status code is 204
if response.status_code != 204 {
println(' Preflight status test failed: expected 204, got ${response.status_code}')
return false
}
// Check Access-Control-Allow-Origin
allowed_origin := ctx.headers['Access-Control-Allow-Origin'] or { '' }
if allowed_origin != '*' {
println(' Preflight origin test failed: expected "*", got "${allowed_origin}"')
return false
}
// Check Access-Control-Allow-Methods
allowed_methods := ctx.headers['Access-Control-Allow-Methods'] or { '' }
if allowed_methods != allow_methods.join(', ') {
println(' Preflight methods test failed: expected "${allow_methods.join(', ')}", got "${allowed_methods}"')
return false
}
// Check Access-Control-Allow-Headers
result_headers := ctx.headers['Access-Control-Allow-Headers'] or { '' }
if result_headers != allow_headers.join(', ') {
println(' Preflight headers test failed: expected "${allow_headers.join(', ')}", got "${result_headers}"')
return false
}
// Check Access-Control-Expose-Headers
result_expose := ctx.headers['Access-Control-Expose-Headers'] or { '' }
if result_expose != expose_headers.join(', ') {
println(' Preflight expose headers test failed: expected "${expose_headers.join(', ')}", got "${result_expose}"')
return false
}
// Check Access-Control-Max-Age
result_max_age := ctx.headers['Access-Control-Max-Age'] or { '' }
if result_max_age != max_age.str() {
println(' Preflight max-age test failed: expected "${max_age}", got "${result_max_age}"')
return false
}
// Check Access-Control-Allow-Credentials
result_credentials := ctx.headers['Access-Control-Allow-Credentials'] or { '' }
if credentials {
if result_credentials != 'true' {
println(' Preflight credentials test failed: expected "true", got "${result_credentials}"')
return false
}
} else {
if result_credentials != '' {
println(' Preflight credentials test failed: expected empty, got "${result_credentials}"')
return false
}
}
}
return true
}
fn main() {
println('🚀 开始 CORS Middleware 属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := CorsTestStats{}
// 运行属性测试
// Feature: builtin-middleware, Property 1: CORS Origin Header Consistency
// Validates: Requirements 1.1, 1.3, 1.4, 1.5, 1.6
stats.run_property_test('Property 1: CORS Origin Header Consistency', test_property_1_cors_origin_header_consistency)
// Feature: builtin-middleware, Property 2: CORS Preflight Response
// Validates: Requirements 1.2, 1.7, 1.8, 1.9, 1.10, 1.11
stats.run_property_test('Property 2: CORS Preflight Response', test_property_2_cors_preflight_response)
// 打印测试总结
stats.print_summary()
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
fn main() {
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
import time
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
fn main() {
+402
View File
@@ -0,0 +1,402 @@
// FastCache 优化测试
// 验证高性能缓存的正确性和性能(通过 FastRouter API)
module main
import meiseayoung.hono
import net.http
import time
struct TestResult {
mut:
name string
passed bool
detail string
}
fn main() {
println('╔═══════════════════════════════════════════════════════════════╗')
println('║ FastCache 优化测试 ║')
println('╚═══════════════════════════════════════════════════════════════╝')
println('')
mut results := []TestResult{}
// 1. 路由匹配正确性测试
results << test_route_matching_correctness()
// 2. 参数提取正确性测试
results << test_param_extraction()
// 3. 缓存命中测试
results << test_cache_hit()
// 4. 多路由并发测试
results << test_multiple_routes()
// 5. 性能基准测试
results << test_performance_benchmark()
// 6. 缓存一致性测试
results << test_cache_consistency()
// 7. 缓存清理测试
results << test_cache_clear()
// 8. 缓存健康检查测试
results << test_cache_health()
// 打印结果
println('')
println('═══════════════════════════════════════════════════════════════')
println('📊 测试结果汇总')
println('═══════════════════════════════════════════════════════════════')
mut passed := 0
mut failed := 0
for result in results {
status := if result.passed { '✅' } else { '❌' }
println('${status} ${result.name}')
if result.detail.len > 0 {
println(' ${result.detail}')
}
if result.passed {
passed++
} else {
failed++
}
}
println('')
println('总计: ${passed}/${results.len} 通过')
if failed == 0 {
println('🎉 所有测试通过!FastCache 优化验证成功!')
} else {
println('⚠️ 有 ${failed} 个测试失败')
}
}
// 1. 路由匹配正确性测试
fn test_route_matching_correctness() TestResult {
mut app := hono.Hono.new()
app.get('/users/:id', fn (mut c hono.Context) http.Response {
return c.text('user')
})
app.get('/posts/:post_id/comments/:comment_id', fn (mut c hono.Context) http.Response {
return c.text('comment')
})
mut router := app.fast_router
// 测试单参数路由
if match1 := router.match_route('GET', '/users/456') {
if match1.params['id'] or { '' } != '456' {
return TestResult{name: '路由匹配正确性', passed: false, detail: '单参数提取错误'}
}
} else {
return TestResult{name: '路由匹配正确性', passed: false, detail: '单参数路由匹配失败'}
}
// 测试多参数路由
if match2 := router.match_route('GET', '/posts/100/comments/200') {
post_id := match2.params['post_id'] or { '' }
comment_id := match2.params['comment_id'] or { '' }
if post_id != '100' || comment_id != '200' {
return TestResult{name: '路由匹配正确性', passed: false, detail: '多参数提取错误: post_id=${post_id}, comment_id=${comment_id}'}
}
} else {
return TestResult{name: '路由匹配正确性', passed: false, detail: '多参数路由匹配失败'}
}
return TestResult{name: '路由匹配正确性', passed: true, detail: '单参数和多参数路由均正确'}
}
// 2. 参数提取正确性测试
fn test_param_extraction() TestResult {
mut app := hono.Hono.new()
app.get('/api/:version/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response {
return c.text('ok')
})
mut router := app.fast_router
test_cases := [
['/api/v1/users/100/posts/200', 'v1', '100', '200'],
['/api/v2/users/abc/posts/xyz', 'v2', 'abc', 'xyz'],
['/api/beta/users/user-123/posts/post-456', 'beta', 'user-123', 'post-456'],
]
for tc in test_cases {
path := tc[0]
expected_version := tc[1]
expected_user := tc[2]
expected_post := tc[3]
if match_result := router.match_route('GET', path) {
version := match_result.params['version'] or { '' }
user_id := match_result.params['user_id'] or { '' }
post_id := match_result.params['post_id'] or { '' }
if version != expected_version || user_id != expected_user || post_id != expected_post {
return TestResult{
name: '参数提取正确性'
passed: false
detail: '路径 ${path} 参数提取错误'
}
}
} else {
return TestResult{name: '参数提取正确性', passed: false, detail: '路径 ${path} 匹配失败'}
}
}
return TestResult{name: '参数提取正确性', passed: true, detail: '${test_cases.len} 个测试用例全部通过'}
}
// 3. 缓存命中测试
fn test_cache_hit() TestResult {
mut app := hono.Hono.new()
app.get('/users/:id', fn (mut c hono.Context) http.Response {
return c.text('user')
})
mut router := app.fast_router
// 第一次匹配(缓存未命中,需要正则匹配)
_ := router.match_route('GET', '/users/123')
// 获取缓存统计
cache_size1, _ := router.get_cache_stats()
// 第二次匹配(应该命中缓存)
if match2 := router.match_route('GET', '/users/123') {
if match2.params['id'] or { '' } == '123' {
cache_size2, _ := router.get_cache_stats()
return TestResult{
name: '缓存命中测试'
passed: true
detail: '缓存大小: ${cache_size1} -> ${cache_size2},参数正确'
}
}
}
return TestResult{name: '缓存命中测试', passed: false, detail: '缓存命中后参数错误'}
}
// 4. 多路由并发测试
fn test_multiple_routes() TestResult {
mut app := hono.Hono.new()
// 注册多个路由
app.get('/users/:id', fn (mut c hono.Context) http.Response { return c.text('user') })
app.get('/posts/:id', fn (mut c hono.Context) http.Response { return c.text('post') })
app.get('/comments/:id', fn (mut c hono.Context) http.Response { return c.text('comment') })
app.get('/tags/:name', fn (mut c hono.Context) http.Response { return c.text('tag') })
mut router := app.fast_router
// 交替匹配不同路由
test_paths := [
['/users/1', 'id', '1'],
['/posts/2', 'id', '2'],
['/comments/3', 'id', '3'],
['/tags/golang', 'name', 'golang'],
['/users/100', 'id', '100'],
['/posts/200', 'id', '200'],
]
for tc in test_paths {
path := tc[0]
param_name := tc[1]
expected_value := tc[2]
if match_result := router.match_route('GET', path) {
actual_value := match_result.params[param_name] or { '' }
if actual_value != expected_value {
return TestResult{
name: '多路由并发测试'
passed: false
detail: '路径 ${path} 参数错误: 期望 ${expected_value}, 实际 ${actual_value}'
}
}
} else {
return TestResult{name: '多路由并发测试', passed: false, detail: '路径 ${path} 匹配失败'}
}
}
return TestResult{name: '多路由并发测试', passed: true, detail: '${test_paths.len} 个路径全部正确'}
}
// 5. 性能基准测试
fn test_performance_benchmark() TestResult {
mut app := hono.Hono.new()
app.get('/users/:id', fn (mut c hono.Context) http.Response {
return c.text('user')
})
mut router := app.fast_router
// 预热
for i in 0 .. 100 {
_ := router.match_route('GET', '/users/${i}')
}
// 测试缓存命中性能
iterations := 10000
sw := time.new_stopwatch()
for _ in 0 .. iterations {
_ := router.match_route('GET', '/users/50') // 命中缓存
}
elapsed := sw.elapsed()
avg_ns := elapsed.nanoseconds() / iterations
avg_us := f64(avg_ns) / 1000.0
// 缓存命中性能评估(考虑不同平台差异)
if avg_us < 1.0 {
return TestResult{
name: '性能基准测试'
passed: true
detail: '缓存命中平均耗时: ${avg_us:.3f}μs (< 1μs, 优秀 ✓)'
}
} else if avg_us < 10.0 {
return TestResult{
name: '性能基准测试'
passed: true
detail: '缓存命中平均耗时: ${avg_us:.3f}μs (< 10μs, 良好)'
}
} else if avg_us < 50.0 {
return TestResult{
name: '性能基准测试'
passed: true
detail: '缓存命中平均耗时: ${avg_us:.3f}μs (< 50μs, 可接受)'
}
}
return TestResult{
name: '性能基准测试'
passed: false
detail: '缓存命中平均耗时: ${avg_us:.3f}μs (> 50μs, 过慢)'
}
}
// 6. 缓存一致性测试
fn test_cache_consistency() TestResult {
mut app := hono.Hono.new()
app.get('/items/:id', fn (mut c hono.Context) http.Response {
return c.text('item')
})
mut router := app.fast_router
// 多次匹配相同路径,验证结果一致
mut results := []string{}
for _ in 0 .. 100 {
if match_result := router.match_route('GET', '/items/999') {
results << match_result.params['id'] or { 'error' }
} else {
results << 'no_match'
}
}
// 检查所有结果是否一致
first := results[0]
for i, r in results {
if r != first {
return TestResult{
name: '缓存一致性测试'
passed: false
detail: '第 ${i} 次结果不一致: ${r} != ${first}'
}
}
}
if first == '999' {
return TestResult{name: '缓存一致性测试', passed: true, detail: '100 次匹配结果完全一致'}
}
return TestResult{name: '缓存一致性测试', passed: false, detail: '参数值错误: ${first}'}
}
// 7. 缓存清理测试
fn test_cache_clear() TestResult {
mut app := hono.Hono.new()
app.get('/users/:id', fn (mut c hono.Context) http.Response {
return c.text('user')
})
mut router := app.fast_router
// 填充缓存
for i in 0 .. 10 {
_ := router.match_route('GET', '/users/${i}')
}
// 获取清理前的缓存大小
size_before, _ := router.get_cache_stats()
// 清理缓存
router.clear_cache()
// 获取清理后的缓存大小
size_after, _ := router.get_cache_stats()
if size_after == 0 {
return TestResult{
name: '缓存清理测试'
passed: true
detail: '清理前: ${size_before}, 清理后: ${size_after}'
}
}
return TestResult{
name: '缓存清理测试'
passed: false
detail: '清理后缓存大小应为0,实际为: ${size_after}'
}
}
// 8. 缓存健康检查测试
fn test_cache_health() TestResult {
mut app := hono.Hono.new()
app.get('/users/:id', fn (mut c hono.Context) http.Response {
return c.text('user')
})
mut router := app.fast_router
// 填充一些缓存
for i in 0 .. 50 {
_ := router.match_route('GET', '/users/${i}')
}
// 检查健康状态
is_healthy := router.is_healthy()
// 获取详细统计
stats := router.get_detailed_stats()
if is_healthy {
return TestResult{
name: '缓存健康检查测试'
passed: true
detail: '缓存健康,统计项数: ${stats.len}'
}
}
return TestResult{
name: '缓存健康检查测试'
passed: false
detail: '缓存不健康'
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+245
View File
@@ -0,0 +1,245 @@
// 高并发测试 (5000-10000 并发)
//
// 使用方法:
// 1. 启动服务器 (必须设置 ulimit): ulimit -n 65535 && ./bench_server_usockets
// 2. 运行测试: ulimit -n 65535 && go run v-hono/tests/test_high_concurrency.go
// 3. 单独测试某个级别: ulimit -n 65535 && go run v-hono/tests/test_high_concurrency.go 8000
//
// 重要提示:
// - 服务器和客户端都需要设置 ulimit -n 65535
// - 连续测试多个并发级别时,建议每次测试前重启服务器
// - TIME_WAIT 连接会影响后续测试,建议等待 10 秒后再测试下一个级别
//
// 系统配置要求 (macOS):
// sudo sysctl -w kern.ipc.somaxconn=8192
// ulimit -n 65535
package main
import (
"fmt"
"io"
"net/http"
"os"
"sort"
"strconv"
"sync"
"sync/atomic"
"time"
)
type BenchResult struct {
Concurrency int
TotalReqs int64
SuccessReqs int64
RPS float64
AvgLatency float64
P99Latency float64
SuccessRate float64
}
func main() {
baseURL := "http://127.0.0.1:8080/"
duration := 10 * time.Second
// 默认测试级别: 5000, 6000, 7000, 8000, 9000, 10000
concurrencyLevels := []int{5000, 6000, 7000, 8000, 9000, 10000}
// 支持命令行参数指定单个并发级别
if len(os.Args) > 1 {
level, err := strconv.Atoi(os.Args[1])
if err == nil {
concurrencyLevels = []int{level}
}
}
fmt.Println("╔═══════════════════════════════════════════════════════════════════════════╗")
fmt.Println("║ v-hono 高并发测试 (5000-10000 并发) ║")
fmt.Println("╚═══════════════════════════════════════════════════════════════════════════╝")
fmt.Println()
// 检查服务器是否运行
if !checkServer(baseURL) {
fmt.Println("❌ 服务器未运行")
fmt.Println(" 请先启动: ./bench_server_usockets")
fmt.Println()
fmt.Println(" 系统配置要求:")
fmt.Println(" - macOS: sudo sysctl -w kern.ipc.somaxconn=8192")
fmt.Println(" - ulimit -n 65535")
return
}
fmt.Println("✅ 服务器已就绪")
fmt.Println()
results := make([]BenchResult, 0, len(concurrencyLevels))
for _, conns := range concurrencyLevels {
result := runBenchmark(baseURL, conns, duration)
results = append(results, result)
// 测试间隔,让系统恢复 (TIME_WAIT 连接需要时间释放)
// macOS TIME_WAIT 默认 15-30 秒,建议等待 60 秒
if len(concurrencyLevels) > 1 {
fmt.Println(" ⏳ 等待 60 秒让系统恢复 (TIME_WAIT 连接释放)...")
time.Sleep(60 * time.Second)
}
}
// 输出汇总报告
printSummary(results)
}
func checkServer(baseURL string) bool {
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 5; i++ {
resp, err := client.Get(baseURL)
if err == nil && resp.StatusCode < 400 {
resp.Body.Close()
return true
}
time.Sleep(200 * time.Millisecond)
}
return false
}
func runBenchmark(baseURL string, conns int, duration time.Duration) BenchResult {
fmt.Printf("🔥 测试 %d 并发连接 (持续 %v)\n", conns, duration)
var total, success int64
var totalLatencyNs int64
latencies := make([]int64, 0, 1000000)
var mu sync.Mutex
client := &http.Client{
Timeout: 60 * time.Second,
Transport: &http.Transport{
MaxIdleConns: conns * 2,
MaxIdleConnsPerHost: conns * 2,
MaxConnsPerHost: 0,
IdleConnTimeout: 120 * time.Second,
DisableKeepAlives: false,
ForceAttemptHTTP2: false,
},
}
var wg sync.WaitGroup
stop := make(chan struct{})
start := time.Now()
// 同时启动所有连接
for i := 0; i < conns; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
t0 := time.Now()
resp, err := client.Get(baseURL)
lat := time.Since(t0).Nanoseconds()
atomic.AddInt64(&total, 1)
atomic.AddInt64(&totalLatencyNs, lat)
mu.Lock()
latencies = append(latencies, lat)
mu.Unlock()
if err != nil {
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode < 400 {
atomic.AddInt64(&success, 1)
}
}
}
}()
}
time.Sleep(duration)
close(stop)
wg.Wait()
elapsed := time.Since(start)
// 计算 P99
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
p99 := int64(0)
if len(latencies) > 0 {
idx := len(latencies) * 99 / 100
p99 = latencies[idx]
}
avgLat := float64(totalLatencyNs) / float64(total) / 1e6
rps := float64(total) / elapsed.Seconds()
successRate := float64(success) / float64(total) * 100
// 判断测试结果
status := "✅"
if successRate < 99.0 {
status = "⚠️"
}
if successRate < 90.0 {
status = "❌"
}
fmt.Printf(" %s RPS: %8.0f Avg: %6.2fms P99: %6.2fms Success: %.1f%%\n",
status, rps, avgLat, float64(p99)/1e6, successRate)
fmt.Println()
return BenchResult{
Concurrency: conns,
TotalReqs: total,
SuccessReqs: success,
RPS: rps,
AvgLatency: avgLat,
P99Latency: float64(p99) / 1e6,
SuccessRate: successRate,
}
}
func printSummary(results []BenchResult) {
if len(results) <= 1 {
return
}
fmt.Println("═══════════════════════════════════════════════════════════════════════════")
fmt.Println(" 📊 测试结果汇总")
fmt.Println("═══════════════════════════════════════════════════════════════════════════")
fmt.Println()
fmt.Println("┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐")
fmt.Println("│ 并发数 │ RPS │ 平均延迟 │ P99延迟 │ 成功率 │")
fmt.Println("├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤")
allPassed := true
for _, r := range results {
status := "✅"
if r.SuccessRate < 99.0 {
status = "⚠️"
allPassed = false
}
if r.SuccessRate < 90.0 {
status = "❌"
allPassed = false
}
fmt.Printf("│ %s %6d │ %10.0f │ %8.2fms │ %8.2fms │ %8.1f%% │\n",
status, r.Concurrency, r.RPS, r.AvgLatency, r.P99Latency, r.SuccessRate)
}
fmt.Println("└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘")
fmt.Println()
if allPassed {
fmt.Println("🎉 所有高并发测试通过!v-hono uSockets 后端表现优秀!")
} else {
fmt.Println("⚠️ 部分测试未达到 99% 成功率,请检查系统配置")
fmt.Println(" - macOS: sudo sysctl -w kern.ipc.somaxconn=8192")
fmt.Println(" - ulimit -n 65535")
}
fmt.Println()
}
+282
View File
@@ -0,0 +1,282 @@
module main
import time
// 测试 HTTP 解析优化
// 原始版本 - 使用 split 和 index
fn parse_path_and_query_original(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
}
// 优化版本 - 零分配单次遍历
fn parse_path_and_query_optimized(full_path string) (string, map[string]string) {
mut query_map := map[string]string{}
len := full_path.len
if len == 0 {
return full_path, query_map
}
// 1. 查找 '?' 位置(单次遍历)
mut query_start := -1
for i in 0 .. len {
if full_path[i] == `?` {
query_start = i
break
}
}
// 没有查询参数,直接返回
if query_start == -1 {
return full_path, query_map
}
path := full_path[..query_start]
// 2. 解析查询参数(单次遍历,避免 split)
mut key_start := query_start + 1
mut key_end := -1
mut value_start := -1
for i := query_start + 1; i <= len; i++ {
ch := if i < len { full_path[i] } else { `&` } // 末尾视为分隔符
if ch == `=` && key_end == -1 {
key_end = i
value_start = i + 1
} else if ch == `&` {
// 完成一个键值对
if key_end > key_start && value_start > 0 {
key := full_path[key_start..key_end]
value := if value_start < i { full_path[value_start..i] } else { '' }
query_map[key] = value
} else if key_end == -1 && i > key_start {
// 只有 key 没有 value(如 ?foo&bar=1)
key := full_path[key_start..i]
query_map[key] = ''
}
// 重置状态
key_start = i + 1
key_end = -1
value_start = -1
}
}
return path, query_map
}
// Keep-Alive 检查 - 原始版本
fn check_keepalive_original(name string, value string) bool {
return name.to_lower() == 'connection' && value.to_lower().contains('keep-alive')
}
// 大小写不敏感比较
@[inline]
fn eq_ignore_case(a string, b string) bool {
if a.len != b.len {
return false
}
for i in 0 .. a.len {
ca := a[i]
cb := b[i]
la := if ca >= `A` && ca <= `Z` { ca + 32 } else { ca }
lb := if cb >= `A` && cb <= `Z` { cb + 32 } else { cb }
if la != lb {
return false
}
}
return true
}
// 大小写不敏感 contains
@[inline]
fn contains_ignore_case(haystack string, needle string) bool {
if needle.len > haystack.len {
return false
}
max_start := haystack.len - needle.len
for i := 0; i <= max_start; i++ {
mut found := true
for j in 0 .. needle.len {
ch := haystack[i + j]
cn := needle[j]
lh := if ch >= `A` && ch <= `Z` { ch + 32 } else { ch }
ln := if cn >= `A` && cn <= `Z` { cn + 32 } else { cn }
if lh != ln {
found = false
break
}
}
if found {
return true
}
}
return false
}
// Keep-Alive 检查 - 优化版本
fn check_keepalive_optimized(name string, value string) bool {
return name.len == 10 && eq_ignore_case(name, 'connection') && contains_ignore_case(value, 'keep-alive')
}
// 测试正确性
fn test_correctness() {
println('=== 测试正确性 ===')
test_cases := [
'/api/users',
'/api/users?id=123',
'/api/users?id=123&name=test',
'/api/users?id=123&name=test&active=true',
'/search?q=hello+world&page=1&limit=10',
'/path?empty=&key=value',
'/path?novalue&key=value',
'/',
'/?single=param',
]
mut all_passed := true
for tc in test_cases {
path_orig, query_orig := parse_path_and_query_original(tc)
path_opt, query_opt := parse_path_and_query_optimized(tc)
if path_orig != path_opt {
println('❌ 路径不匹配: ${tc}')
println(' 原始: ${path_orig}')
println(' 优化: ${path_opt}')
all_passed = false
} else if query_orig.len != query_opt.len {
println('❌ 查询参数数量不匹配: ${tc}')
println(' 原始: ${query_orig}')
println(' 优化: ${query_opt}')
all_passed = false
} else {
mut params_match := true
for k, v in query_orig {
if k !in query_opt || query_opt[k] != v {
params_match = false
break
}
}
if !params_match {
println('❌ 查询参数值不匹配: ${tc}')
println(' 原始: ${query_orig}')
println(' 优化: ${query_opt}')
all_passed = false
} else {
println('✓ ${tc}')
}
}
}
if all_passed {
println('\n✅ 所有正确性测试通过!')
} else {
println('\n❌ 部分测试失败')
}
}
// 性能基准测试
fn benchmark_performance() {
println('\n=== 性能基准测试 ===')
// 测试用例
test_paths := [
'/api/users', // 无查询参数
'/api/users?id=123', // 单个参数
'/api/users?id=123&name=test&active=true', // 多个参数
'/search?q=hello+world&page=1&limit=10&sort=desc&filter=active', // 复杂查询
]
iterations := 100000
for test_path in test_paths {
// 原始版本
sw_orig := time.new_stopwatch()
for _ in 0 .. iterations {
_, _ := parse_path_and_query_original(test_path)
}
time_orig := sw_orig.elapsed().microseconds()
// 优化版本
sw_opt := time.new_stopwatch()
for _ in 0 .. iterations {
_, _ := parse_path_and_query_optimized(test_path)
}
time_opt := sw_opt.elapsed().microseconds()
speedup := f64(time_orig) / f64(time_opt)
println('\n路径: ${test_path}')
println(' 原始版本: ${time_orig} μs (${iterations} 次)')
println(' 优化版本: ${time_opt} μs (${iterations} 次)')
println(' 提升: ${speedup:.2f}x')
}
}
// 测试 Keep-Alive 检查优化
fn test_keepalive_optimization() {
println('\n=== Keep-Alive 检查优化测试 ===')
test_cases := [
['Connection', 'keep-alive'],
['connection', 'Keep-Alive'],
['CONNECTION', 'KEEP-ALIVE'],
['Content-Type', 'application/json'],
['Connection', 'close'],
]
iterations := 100000
// 原始版本
sw_orig := time.new_stopwatch()
for _ in 0 .. iterations {
for tc in test_cases {
_ := check_keepalive_original(tc[0], tc[1])
}
}
time_orig := sw_orig.elapsed().microseconds()
// 优化版本
sw_opt := time.new_stopwatch()
for _ in 0 .. iterations {
for tc in test_cases {
_ := check_keepalive_optimized(tc[0], tc[1])
}
}
time_opt := sw_opt.elapsed().microseconds()
speedup := f64(time_orig) / f64(time_opt)
println('原始版本: ${time_orig} μs (${iterations * test_cases.len} 次检查)')
println('优化版本: ${time_opt} μs (${iterations * test_cases.len} 次检查)')
println('提升: ${speedup:.2f}x')
}
fn main() {
println('HTTP 解析优化测试')
println('=' .repeat(50))
test_correctness()
benchmark_performance()
test_keepalive_optimization()
println('\n' + '=' .repeat(50))
println('测试完成!')
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
import os
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+310
View File
@@ -0,0 +1,310 @@
import meiseayoung.hono
import rand
import time
// JWT 中间件属性测试
// Property-Based Testing for JWT functionality
const test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_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 PropertyTestStats) print_summary() {
println('\n=== JWT 中间件属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// 生成随机的 subject
fn generate_random_subject() string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
len := rand.int_in_range(5, 30) or { 10 }
mut subject := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
subject += chars[idx].ascii_str()
}
return subject
}
// 生成随机的 issuer
fn generate_random_issuer() string {
prefixes := ['https://auth.', 'https://api.', 'https://']
domains := ['example.com', 'test.org', 'myapp.io', 'service.net']
prefix_idx := rand.int_in_range(0, prefixes.len) or { 0 }
domain_idx := rand.int_in_range(0, domains.len) or { 0 }
return prefixes[prefix_idx] + domains[domain_idx]
}
// 生成随机的密钥
fn generate_random_secret() string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'
len := rand.int_in_range(32, 64) or { 32 }
mut secret := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
secret += chars[idx].ascii_str()
}
return secret
}
// 生成随机的自定义声明
fn generate_random_claims() map[string]string {
mut claims := map[string]string{}
num_claims := rand.int_in_range(0, 5) or { 2 }
for i in 0 .. num_claims {
key := 'claim_${i}'
value := generate_random_subject()
claims[key] = value
}
return claims
}
// 生成随机的 JwtPayload
fn generate_random_payload() hono.JwtPayload {
now := time.now().unix()
return hono.JwtPayload{
sub: generate_random_subject()
iss: generate_random_issuer()
aud: 'test-audience'
exp: now + 3600 // 1 hour from now
nbf: now - 60 // 1 minute ago
iat: now
jti: generate_random_subject()
claims: generate_random_claims()
}
}
// 随机选择算法
fn random_algorithm() hono.JwtAlgorithm {
idx := rand.int_in_range(0, 3) or { 0 }
return match idx {
0 { hono.JwtAlgorithm.hs256 }
1 { hono.JwtAlgorithm.hs384 }
else { hono.JwtAlgorithm.hs512 }
}
}
// ============================================================================
// Property 6: JWT Sign-Verify Round-Trip
// Feature: builtin-middleware, Property 6: JWT Sign-Verify Round-Trip
// Validates: Requirements 3.1, 3.4, 3.11
//
// *For any* valid JwtPayload and secret, calling sign_jwt followed by verify_jwt
// with the same secret SHALL return an equivalent payload.
// ============================================================================
fn test_property_6_jwt_sign_verify_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
for i in 0 .. test_iterations {
payload := generate_random_payload()
secret := generate_random_secret()
alg := random_algorithm()
// 签名 JWT
token := hono.sign_jwt(payload, secret, alg) or {
println(' Iteration ${i}: Failed to sign JWT: ${err}')
return false
}
// 验证 JWT
verified_payload := hono.verify_jwt(token, secret, alg) or {
println(' Iteration ${i}: Failed to verify JWT: ${err}')
return false
}
// 验证 payload 字段
if verified_payload.sub != payload.sub {
println(' Iteration ${i}: Subject mismatch - expected "${payload.sub}", got "${verified_payload.sub}"')
return false
}
if verified_payload.iss != payload.iss {
println(' Iteration ${i}: Issuer mismatch - expected "${payload.iss}", got "${verified_payload.iss}"')
return false
}
if verified_payload.aud != payload.aud {
println(' Iteration ${i}: Audience mismatch - expected "${payload.aud}", got "${verified_payload.aud}"')
return false
}
if verified_payload.exp != payload.exp {
println(' Iteration ${i}: Exp mismatch - expected ${payload.exp}, got ${verified_payload.exp}')
return false
}
if verified_payload.nbf != payload.nbf {
println(' Iteration ${i}: Nbf mismatch - expected ${payload.nbf}, got ${verified_payload.nbf}')
return false
}
if verified_payload.iat != payload.iat {
println(' Iteration ${i}: Iat mismatch - expected ${payload.iat}, got ${verified_payload.iat}')
return false
}
if verified_payload.jti != payload.jti {
println(' Iteration ${i}: JTI mismatch - expected "${payload.jti}", got "${verified_payload.jti}"')
return false
}
// 验证自定义声明
for key, value in payload.claims {
if key !in verified_payload.claims {
println(' Iteration ${i}: Missing claim "${key}"')
return false
}
if verified_payload.claims[key] != value {
println(' Iteration ${i}: Claim "${key}" mismatch - expected "${value}", got "${verified_payload.claims[key]}"')
return false
}
}
}
return true
}
// ============================================================================
// Property 7: JWT Expiration Enforcement
// Feature: builtin-middleware, Property 7: JWT Expiration Enforcement
// Validates: Requirements 3.2, 3.9
//
// *For any* JWT with an exp claim in the past, verify_jwt SHALL return an error
// when verifyOptions.exp is true.
// ============================================================================
fn test_property_7_jwt_expiration_enforcement() bool {
rand.seed([u32(time.now().unix()), u32(54321)])
for i in 0 .. test_iterations {
now := time.now().unix()
// 创建已过期的 payload
mut payload := generate_random_payload()
// 设置过期时间为过去(1秒到1小时前)
seconds_ago := rand.int_in_range(1, 3600) or { 60 }
payload.exp = now - seconds_ago
secret := generate_random_secret()
alg := random_algorithm()
// 签名 JWT
token := hono.sign_jwt(payload, secret, alg) or {
println(' Iteration ${i}: Failed to sign JWT: ${err}')
return false
}
// 使用带过期验证的选项验证 JWT
verify_options := hono.JwtVerifyOptions{
exp: true
nbf: false
iat: false
}
// 验证应该失败(因为 token 已过期)
if _ := hono.verify_jwt_with_options(token, secret, alg, verify_options) {
println(' Iteration ${i}: Expired JWT was accepted (should have been rejected)')
return false
}
}
return true
}
// ============================================================================
// Property 8: JWT Algorithm Consistency
// Feature: builtin-middleware, Property 8: JWT Algorithm Consistency
// Validates: Requirements 3.7
//
// *For any* JWT signed with algorithm A, verification with algorithm B
// (where A ≠ B) SHALL fail.
// ============================================================================
fn test_property_8_jwt_algorithm_consistency() bool {
rand.seed([u32(time.now().unix()), u32(98765)])
algorithms := [hono.JwtAlgorithm.hs256, hono.JwtAlgorithm.hs384, hono.JwtAlgorithm.hs512]
for i in 0 .. test_iterations {
payload := generate_random_payload()
secret := generate_random_secret()
// 随机选择签名算法
sign_alg_idx := rand.int_in_range(0, algorithms.len) or { 0 }
sign_alg := algorithms[sign_alg_idx]
// 选择不同的验证算法
mut verify_alg_idx := rand.int_in_range(0, algorithms.len) or { 0 }
// 确保验证算法与签名算法不同
for verify_alg_idx == sign_alg_idx {
verify_alg_idx = rand.int_in_range(0, algorithms.len) or { 0 }
}
verify_alg := algorithms[verify_alg_idx]
// 签名 JWT
token := hono.sign_jwt(payload, secret, sign_alg) or {
println(' Iteration ${i}: Failed to sign JWT: ${err}')
return false
}
// 使用不同算法验证应该失败
if _ := hono.verify_jwt(token, secret, verify_alg) {
println(' Iteration ${i}: JWT verified with wrong algorithm (signed with ${sign_alg}, verified with ${verify_alg})')
return false
}
}
return true
}
fn main() {
println('🚀 开始 JWT 中间件属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := PropertyTestStats{}
// 运行属性测试
// Feature: builtin-middleware, Property 6: JWT Sign-Verify Round-Trip
// Validates: Requirements 3.1, 3.4, 3.11
stats.run_property_test('Property 6: JWT Sign-Verify Round-Trip', test_property_6_jwt_sign_verify_roundtrip)
// Feature: builtin-middleware, Property 7: JWT Expiration Enforcement
// Validates: Requirements 3.2, 3.9
stats.run_property_test('Property 7: JWT Expiration Enforcement', test_property_7_jwt_expiration_enforcement)
// Feature: builtin-middleware, Property 8: JWT Algorithm Consistency
// Validates: Requirements 3.7
stats.run_property_test('Property 8: JWT Algorithm Consistency', test_property_8_jwt_algorithm_consistency)
// 打印测试总结
stats.print_summary()
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import os
fn main() {
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import os
import time
+355
View File
@@ -0,0 +1,355 @@
// 中间件优化测试
// 测试三个优化点:
// 1. 零中间件快速路径
// 2. 中间件预计算(启动时排序)
// 3. 复用 cache key
import meiseayoung.hono
import time
import net.http
// 测试统计
struct TestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats TestStats) start_test(test_name string) {
stats.total_tests++
print('🧪 ${test_name}... ')
}
fn (mut stats TestStats) pass_test() {
stats.passed_tests++
println('✅')
}
fn (mut stats TestStats) fail_test(error string) {
stats.failed_tests++
println('❌ ${error}')
}
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} 个测试失败')
}
}
// 测试1:零中间件快速路径 - has_middlewares 标志
fn test_zero_middleware_flag(mut stats TestStats) {
stats.start_test('零中间件标志 (has_middlewares)')
mut app := hono.Hono.new()
// 新创建的应用应该没有中间件
if app.has_middlewares {
stats.fail_test('新应用不应该有中间件标志')
return
}
// 添加中间件后标志应该为 true
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
return next(mut c)
})
if !app.has_middlewares {
stats.fail_test('添加中间件后标志应该为 true')
return
}
stats.pass_test()
}
// 测试2:中间件预计算 - sorted_middleware_prefixes
fn test_middleware_precompute(mut stats TestStats) {
stats.start_test('中间件预计算 (sorted_middleware_prefixes)')
mut app := hono.Hono.new()
// 添加路由
app.get('/test', fn (mut c hono.Context) http.Response {
return c.text('test')
})
// 创建子应用并添加中间件
mut sub_app := hono.Hono.new()
sub_app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
return next(mut c)
})
sub_app.get('/hello', fn (mut c hono.Context) http.Response {
return c.text('hello')
})
// 挂载子应用
app.route('/api', mut sub_app)
// 预计算前,sorted_middleware_prefixes 应该为空
if app.sorted_middleware_prefixes.len != 0 {
stats.fail_test('预计算前 sorted_middleware_prefixes 应该为空')
return
}
// 调用预计算
app.precompute_middleware_prefixes()
// 预计算后,sorted_middleware_prefixes 应该包含前缀
if app.sorted_middleware_prefixes.len == 0 && app.route_middlewares.len > 0 {
stats.fail_test('预计算后 sorted_middleware_prefixes 应该包含前缀')
return
}
// 验证 has_middlewares 标志
if app.route_middlewares.len > 0 && !app.has_middlewares {
stats.fail_test('有路由中间件时 has_middlewares 应该为 true')
return
}
stats.pass_test()
}
// 测试3:FastRouter cache key 复用
fn test_fast_router_cache_key_reuse(mut stats TestStats) {
stats.start_test('FastRouter cache key 复用')
mut router := hono.FastRouter.new()
// 添加静态路由
static_handler := hono.ContextHandler{
path: '/users'
handler: fn (mut c hono.Context) http.Response {
return c.text('users')
}
}
router.add_route('GET', static_handler, '') or {
stats.fail_test('添加静态路由失败')
return
}
// 添加动态路由
dynamic_handler := hono.ContextHandler{
path: '/users/:id'
handler: fn (mut c hono.Context) http.Response {
return c.text('user')
}
}
router.add_route('GET', dynamic_handler, '') or {
stats.fail_test('添加动态路由失败')
return
}
// 测试静态路由匹配
if _ := router.match_route('GET', '/users') {
// 成功
} else {
stats.fail_test('静态路由匹配失败')
return
}
// 测试动态路由匹配(第一次,会缓存)
if match1 := router.match_route('GET', '/users/123') {
if match1.params['id'] != '123' {
stats.fail_test('参数提取失败')
return
}
} else {
stats.fail_test('动态路由匹配失败')
return
}
// 测试动态路由匹配(第二次,应该命中缓存)
if match2 := router.match_route('GET', '/users/123') {
if match2.params['id'] != '123' {
stats.fail_test('缓存命中后参数提取失败')
return
}
} else {
stats.fail_test('缓存命中失败')
return
}
// 验证缓存状态
_, _, cache_count := router.get_stats()
if cache_count == 0 {
stats.fail_test('缓存应该有条目')
return
}
stats.pass_test()
}
// 测试4:HybridRouter cache key 复用
fn test_hybrid_router_cache_key_reuse(mut stats TestStats) {
stats.start_test('HybridRouter cache key 复用')
mut router := hono.ContextHybridRouter.new()
// 添加动态路由
dynamic_handler := hono.ContextHandler{
path: '/posts/:id'
handler: fn (mut c hono.Context) http.Response {
return c.text('post')
}
}
router.add_route('GET', dynamic_handler, '')
// 第一次匹配(无缓存)
if match1 := router.match_route('GET', '/posts/456') {
if match1.params['id'] != '456' {
stats.fail_test('参数提取失败')
return
}
} else {
stats.fail_test('动态路由匹配失败')
return
}
// 第二次匹配(应该命中缓存)
if match2 := router.match_route('GET', '/posts/456') {
if match2.params['id'] != '456' {
stats.fail_test('缓存命中后参数提取失败')
return
}
} else {
stats.fail_test('缓存命中失败')
return
}
stats.pass_test()
}
// 测试5:性能对比 - 有中间件 vs 无中间件
fn test_middleware_performance_comparison(mut stats TestStats) {
stats.start_test('中间件性能对比')
// 创建无中间件的路由器
mut router_no_mw := hono.FastRouter.new()
handler := hono.ContextHandler{
path: '/api/users/:id'
handler: fn (mut c hono.Context) http.Response {
return c.text('user')
}
}
router_no_mw.add_route('GET', handler, '') or {
stats.fail_test('添加路由失败')
return
}
// 预热
for _ in 0 .. 100 {
_ := router_no_mw.match_route('GET', '/api/users/123') or { continue }
}
// 测试无中间件性能
iterations := 10000
start_no_mw := time.now()
for _ in 0 .. iterations {
_ := router_no_mw.match_route('GET', '/api/users/123') or { continue }
}
duration_no_mw := time.since(start_no_mw)
// 创建有中间件的应用
mut app_with_mw := hono.Hono.new()
app_with_mw.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
return next(mut c)
})
app_with_mw.get('/api/users/:id', fn (mut c hono.Context) http.Response {
return c.text('user')
})
app_with_mw.precompute_middleware_prefixes()
// 预热
for _ in 0 .. 100 {
_ := app_with_mw.fast_router.match_route('GET', '/api/users/123') or { continue }
}
// 测试有中间件性能
start_with_mw := time.now()
for _ in 0 .. iterations {
_ := app_with_mw.fast_router.match_route('GET', '/api/users/123') or { continue }
}
duration_with_mw := time.since(start_with_mw)
println('')
println(' 📊 无中间件: ${duration_no_mw.milliseconds()}ms (${iterations} 次)')
println(' 📊 有中间件: ${duration_with_mw.milliseconds()}ms (${iterations} 次)')
// 性能差异不应该太大(路由匹配本身不受中间件影响)
stats.pass_test()
}
// 测试6:预计算排序正确性
fn test_precompute_sorting(mut stats TestStats) {
stats.start_test('预计算排序正确性')
mut app := hono.Hono.new()
// 添加多个子应用,模拟不同长度的前缀
mut sub1 := hono.Hono.new()
sub1.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
return next(mut c)
})
sub1.get('/test', fn (mut c hono.Context) http.Response {
return c.text('test')
})
mut sub2 := hono.Hono.new()
sub2.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
return next(mut c)
})
sub2.get('/test', fn (mut c hono.Context) http.Response {
return c.text('test')
})
mut sub3 := hono.Hono.new()
sub3.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
return next(mut c)
})
sub3.get('/test', fn (mut c hono.Context) http.Response {
return c.text('test')
})
// 按不同顺序挂载(长前缀先挂载)
app.route('/api/v1/users', mut sub1)
app.route('/api', mut sub2)
app.route('/api/v1', mut sub3)
// 预计算
app.precompute_middleware_prefixes()
// 验证排序:短前缀应该在前面
if app.sorted_middleware_prefixes.len >= 2 {
for i in 0 .. app.sorted_middleware_prefixes.len - 1 {
if app.sorted_middleware_prefixes[i].len > app.sorted_middleware_prefixes[i + 1].len {
stats.fail_test('前缀排序不正确:${app.sorted_middleware_prefixes[i]} 应该在 ${app.sorted_middleware_prefixes[i + 1]} 之后')
return
}
}
}
stats.pass_test()
}
fn main() {
println('🚀 开始中间件优化测试...\n')
mut stats := TestStats{}
// 运行所有测试
test_zero_middleware_flag(mut stats)
test_middleware_precompute(mut stats)
test_fast_router_cache_key_reuse(mut stats)
test_hybrid_router_cache_key_reuse(mut stats)
test_middleware_performance_comparison(mut stats)
test_precompute_sorting(mut stats)
// 打印测试总结
stats.print_summary()
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
BIN
View File
Binary file not shown.
+360
View File
@@ -0,0 +1,360 @@
// picoev 优化集成测试 (Go 版本)
// 测试 picoev 服务器的完整功能,确保优化没有引入新问题
//
// 使用方法:
// 1. 先启动测试服务器: v -enable-globals run v-hono/tests/test_picoev_server.v
// 2. 运行测试: go run v-hono/tests/test_picoev_integration.go
package main
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
const baseURL = "http://127.0.0.1:9999"
type TestStats struct {
total int
passed int
failed int
errors []string
}
func (s *TestStats) run(name string, testFn func() bool) {
s.total++
fmt.Printf(" 🧪 %s... ", name)
if testFn() {
s.passed++
fmt.Println("✅")
} else {
s.failed++
s.errors = append(s.errors, name)
fmt.Println("❌")
}
}
func (s *TestStats) summary() {
fmt.Println()
fmt.Println("═══════════════════════════════════════════════════════════════")
fmt.Printf("📊 测试结果: %d/%d 通过\n", s.passed, s.total)
if s.failed > 0 {
fmt.Println("❌ 失败的测试:")
for _, err := range s.errors {
fmt.Printf(" - %s\n", err)
}
} else {
fmt.Println("🎉 所有测试通过!picoev 优化验证成功!")
}
fmt.Println("═══════════════════════════════════════════════════════════════")
}
func main() {
fmt.Println("╔═══════════════════════════════════════════════════════════════╗")
fmt.Println("║ picoev 优化集成测试 (Go) ║")
fmt.Println("╚═══════════════════════════════════════════════════════════════╝")
fmt.Println()
// 验证服务器是否运行
fmt.Println("🔍 检查测试服务器...")
if !checkServerReady() {
fmt.Println("❌ 服务器未运行")
fmt.Println(" 请先启动: v -enable-globals run v-hono/tests/test_picoev_server.v")
return
}
fmt.Println("✅ 服务器已就绪")
fmt.Println()
stats := &TestStats{}
// 1. 基本 GET 路由测试
fmt.Println("📦 1. 基本 GET 路由测试")
stats.run("GET 根路径", testGetRoot)
stats.run("GET 健康检查", testGetHealth)
stats.run("GET 静态路由", testGetStatic)
fmt.Println()
// 2. 动态路由测试
fmt.Println("📦 2. 动态路由测试")
stats.run("单参数路由", testSingleParam)
stats.run("多参数路由", testMultiParams)
stats.run("嵌套参数路由", testNestedParams)
fmt.Println()
// 3. 查询参数测试
fmt.Println("📦 3. 查询参数测试")
stats.run("单个查询参数", testSingleQuery)
stats.run("多个查询参数", testMultiQuery)
fmt.Println()
// 4. 响应格式测试
fmt.Println("📦 4. 响应格式测试")
stats.run("JSON 响应", testJSONResponse)
stats.run("HTML 响应", testHTMLResponse)
stats.run("自定义状态码 201", testCustomStatus)
fmt.Println()
// 5. 中间件测试
fmt.Println("📦 5. 中间件测试")
stats.run("全局中间件响应头", testMiddlewareHeader)
fmt.Println()
// 6. 404 处理测试
fmt.Println("📦 6. 错误处理测试")
stats.run("404 未找到", testNotFound)
fmt.Println()
// 7. Keep-Alive 测试
fmt.Println("📦 7. Keep-Alive 连接测试")
stats.run("连接复用", testKeepAlive)
fmt.Println()
// 8. 性能测试
fmt.Println("📦 8. 性能测试")
stats.run("响应时间 < 50ms", testResponseTime)
stats.run("吞吐量测试", testThroughput)
fmt.Println()
// 9. 高并发基准测试
fmt.Println("📦 9. Keep-Alive 基准测试")
testBenchmark()
fmt.Println()
// 输出总结
stats.summary()
}
// 检查服务器是否就绪
func checkServerReady() bool {
client := &http.Client{Timeout: 2 * time.Second}
for i := 0; i < 5; i++ {
resp, err := client.Get(baseURL + "/health")
if err == nil && resp.StatusCode == 200 {
resp.Body.Close()
return true
}
time.Sleep(200 * time.Millisecond)
}
return false
}
// ==================== 基本 GET 路由测试 ====================
func testGetRoot() bool {
resp, body := doGet("/")
return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "Hello")
}
func testGetHealth() bool {
resp, body := doGet("/health")
return resp != nil && resp.StatusCode == 200 && body == "OK"
}
func testGetStatic() bool {
resp, body := doGet("/api/health")
return resp != nil && resp.StatusCode == 200 && body == "OK"
}
// ==================== 动态路由测试 ====================
func testSingleParam() bool {
resp, body := doGet("/api/users/456")
return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "456")
}
func testMultiParams() bool {
resp, body := doGet("/api/users/123/posts/789")
return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "123") && strings.Contains(body, "789")
}
func testNestedParams() bool {
resp, body := doGet("/api/categories/tech/items/laptop")
return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "tech") && strings.Contains(body, "laptop")
}
// ==================== 查询参数测试 ====================
func testSingleQuery() bool {
resp, body := doGet("/api/search?q=test")
return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "test")
}
func testMultiQuery() bool {
resp, body := doGet("/api/search?q=hello&limit=10&page=1")
return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "hello")
}
// ==================== 响应格式测试 ====================
func testJSONResponse() bool {
resp, _ := doGet("/api/json")
if resp == nil {
return false
}
contentType := resp.Header.Get("Content-Type")
return resp.StatusCode == 200 && strings.Contains(contentType, "application/json")
}
func testHTMLResponse() bool {
resp, _ := doGet("/api/html")
if resp == nil {
return false
}
contentType := resp.Header.Get("Content-Type")
return resp.StatusCode == 200 && strings.Contains(contentType, "text/html")
}
func testCustomStatus() bool {
resp, _ := doGet("/api/created")
return resp != nil && resp.StatusCode == 201
}
// ==================== 中间件测试 ====================
func testMiddlewareHeader() bool {
resp, body := doGet("/api/health")
// 检查中间件添加的响应头
if resp == nil {
return false
}
middlewareHeader := resp.Header.Get("X-Middleware")
return resp.StatusCode == 200 && body == "OK" && middlewareHeader == "applied"
}
// ==================== 错误处理测试 ====================
func testNotFound() bool {
resp, _ := doGet("/nonexistent/path/here")
return resp != nil && resp.StatusCode == 404
}
// ==================== Keep-Alive 测试 ====================
func testKeepAlive() bool {
// 使用同一个 transport 发送多个请求
transport := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: false,
}
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
for i := 0; i < 5; i++ {
resp, err := client.Get(baseURL + "/api/health")
if err != nil || resp.StatusCode != 200 {
return false
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
return true
}
// ==================== 性能测试 ====================
func testResponseTime() bool {
client := &http.Client{Timeout: 5 * time.Second}
start := time.Now()
for i := 0; i < 10; i++ {
resp, err := client.Get(baseURL + "/api/health")
if err != nil {
return false
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
elapsed := time.Since(start)
avgMs := float64(elapsed.Milliseconds()) / 10.0
// 平均响应时间应小于 50ms
return avgMs < 50.0
}
func testThroughput() bool {
client := &http.Client{Timeout: 5 * time.Second}
requests := 50
success := 0
start := time.Now()
for i := 0; i < requests; i++ {
resp, err := client.Get(baseURL + "/api/health")
if err == nil && resp.StatusCode == 200 {
success++
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
}
elapsed := time.Since(start)
rps := float64(success) * 1000.0 / float64(elapsed.Milliseconds())
fmt.Printf("(%.0f req/s) ", rps)
// 吞吐量应大于 20 req/s,成功率 > 90%
return rps > 20.0 && success >= requests*9/10
}
// Keep-Alive 基准测试
func testBenchmark() {
requests := 5000
conn, err := net.Dial("tcp", "127.0.0.1:9999")
if err != nil {
fmt.Println(" ❌ 连接失败:", err)
return
}
defer conn.Close()
// 预热
for i := 0; i < 100; i++ {
fmt.Fprintf(conn, "GET /health HTTP/1.1\r\nHost: 127.0.0.1:9999\r\nConnection: keep-alive\r\n\r\n")
buf := make([]byte, 256)
conn.Read(buf)
}
// 正式测试
start := time.Now()
success := 0
for i := 0; i < requests; i++ {
_, err := fmt.Fprintf(conn, "GET /health HTTP/1.1\r\nHost: 127.0.0.1:9999\r\nConnection: keep-alive\r\n\r\n")
if err != nil {
continue
}
buf := make([]byte, 256)
_, err = conn.Read(buf)
if err != nil {
continue
}
success++
}
elapsed := time.Since(start)
elapsedMs := float64(elapsed.Milliseconds())
rps := float64(success) * 1000.0 / elapsedMs
avgUs := elapsedMs * 1000.0 / float64(success)
fmt.Printf(" 成功请求: %d/%d\n", success, requests)
fmt.Printf(" 吞吐量: %.0f req/s\n", rps)
fmt.Printf(" 平均延迟: %.2fμs\n", avgUs)
}
// 辅助函数
func doGet(path string) (*http.Response, string) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(baseURL + path)
if err != nil {
return nil, ""
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return resp, string(body)
}
-270
View File
@@ -1,270 +0,0 @@
// 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
}
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -3,7 +3,7 @@
module main
import hono
import meiseayoung.hono
import net.http
fn main() {
@@ -0,0 +1,434 @@
// test_picoev_stream_writer_properties.v
// Property-Based Testing for PicoevStreamWriter chunked encoding
// Feature: sse-streaming-helper, Property 10: Chunked 编码格式正确性
// Validates: Requirements 6.2
module main
import rand
import time
const test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_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 PropertyTestStats) print_summary() {
println('\n=== PicoevStreamWriter 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// ============================================================================
// Chunked Encoding Functions (copied from streaming.v for testing)
// ============================================================================
// format_chunk_size - Format the chunk size as hex followed by CRLF
@[inline]
fn format_chunk_size(size int) string {
return '${size:x}\r\n'
}
// format_chunk - Format a complete chunk with size, data, and trailing CRLF
fn format_chunk(data []u8) string {
if data.len == 0 {
return ''
}
return '${data.len:x}\r\n${data.bytestr()}\r\n'
}
// format_chunk_string - Format a string as a complete chunk
fn format_chunk_string(data string) string {
if data.len == 0 {
return ''
}
return '${data.len:x}\r\n${data}\r\n'
}
// format_final_chunk - Return the final chunk marker for chunked encoding
fn format_final_chunk() string {
return '0\r\n\r\n'
}
// ============================================================================
// Random Data Generators
// ============================================================================
// Generate random bytes for testing
fn generate_random_bytes(max_len int) []u8 {
len := rand.int_in_range(1, max_len) or { 10 }
mut data := []u8{len: len}
for i in 0 .. len {
data[i] = u8(rand.int_in_range(0, 256) or { 0 })
}
return data
}
// Generate random string for testing (printable ASCII only to avoid encoding issues)
fn generate_random_string(max_len int) string {
chars := 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.'
len := rand.int_in_range(1, max_len) or { 10 }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx].ascii_str()
}
return result
}
// ============================================================================
// Chunk Parsing Helper
// ============================================================================
// Parse a chunked encoded string and extract the data
// Returns the extracted data and whether parsing was successful
fn parse_chunked_data(chunked string) !([]u8, bool) {
// Find the size part (before first \r\n)
mut crlf_pos := -1
for i := 0; i < chunked.len - 1; i++ {
if chunked[i] == `\r` && chunked[i + 1] == `\n` {
crlf_pos = i
break
}
}
if crlf_pos == -1 {
return error('No CRLF found in chunk header')
}
size_hex := chunked[..crlf_pos]
size := parse_hex(size_hex) or {
return error('Invalid hex size: ${size_hex}')
}
// Extract data after the first \r\n
data_start := crlf_pos + 2
data_end := data_start + size
if data_end > chunked.len {
return error('Chunk data incomplete')
}
data := chunked[data_start..data_end].bytes()
// Verify trailing \r\n
if data_end + 2 > chunked.len {
return error('Missing trailing CRLF')
}
trailing := chunked[data_end..data_end + 2]
if trailing != '\r\n' {
return error('Invalid trailing CRLF: got "${trailing}"')
}
return data, true
}
// Parse hex string to int
fn parse_hex(hex_str string) !int {
mut result := 0
for c in hex_str {
result *= 16
if c >= `0` && c <= `9` {
result += int(c - `0`)
} else if c >= `a` && c <= `f` {
result += int(c - `a` + 10)
} else if c >= `A` && c <= `F` {
result += int(c - `A` + 10)
} else {
return error('Invalid hex character: ${c.ascii_str()}')
}
}
return result
}
// ============================================================================
// Property 10: Chunked 编码格式正确性
// Feature: sse-streaming-helper, Property 10: Chunked 编码格式正确性
// Validates: Requirements 6.2
//
// *For any* data written to the stream, the output SHALL follow HTTP chunked
// transfer encoding format:
// - Each chunk starts with the hex size followed by \r\n
// - Chunk data followed by \r\n
// - Final chunk is 0\r\n\r\n
// ============================================================================
// Test 10a: format_chunk produces valid chunked encoding for any byte array
fn test_property_10a_format_chunk_bytes() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
for i in 0 .. test_iterations {
// Generate random data
data := generate_random_bytes(1000)
// Format as chunk
chunk := format_chunk(data)
// Verify chunk format
// 1. Should start with hex size
mut crlf_pos := -1
for j := 0; j < chunk.len - 1; j++ {
if chunk[j] == `\r` && chunk[j + 1] == `\n` {
crlf_pos = j
break
}
}
if crlf_pos == -1 {
println(' Iteration ${i}: No CRLF found in chunk header')
return false
}
size_hex := chunk[..crlf_pos]
parsed_size := parse_hex(size_hex) or {
println(' Iteration ${i}: Invalid hex size: ${size_hex}')
return false
}
// 2. Size should match data length
if parsed_size != data.len {
println(' Iteration ${i}: Size mismatch - expected ${data.len}, got ${parsed_size}')
return false
}
// 3. Data should be extractable and match original
extracted_data, _ := parse_chunked_data(chunk) or {
println(' Iteration ${i}: Failed to parse chunk: ${err}')
return false
}
if extracted_data != data {
println(' Iteration ${i}: Data mismatch after extraction')
return false
}
}
return true
}
// Test 10b: format_chunk_string produces valid chunked encoding for any string
fn test_property_10b_format_chunk_string() bool {
rand.seed([u32(time.now().unix()), u32(54321)])
for i in 0 .. test_iterations {
// Generate random string
data := generate_random_string(500)
// Format as chunk
chunk := format_chunk_string(data)
// Verify chunk format
mut crlf_pos := -1
for j := 0; j < chunk.len - 1; j++ {
if chunk[j] == `\r` && chunk[j + 1] == `\n` {
crlf_pos = j
break
}
}
if crlf_pos == -1 {
println(' Iteration ${i}: No CRLF found in chunk header')
return false
}
size_hex := chunk[..crlf_pos]
parsed_size := parse_hex(size_hex) or {
println(' Iteration ${i}: Invalid hex size: ${size_hex}')
return false
}
// Size should match string byte length
if parsed_size != data.len {
println(' Iteration ${i}: Size mismatch - expected ${data.len}, got ${parsed_size}')
return false
}
// Extract and verify data
extracted_data, _ := parse_chunked_data(chunk) or {
println(' Iteration ${i}: Failed to parse chunk: ${err}')
return false
}
if extracted_data.bytestr() != data {
println(' Iteration ${i}: String mismatch after extraction')
return false
}
}
return true
}
// Test 10c: format_final_chunk produces correct terminator
fn test_property_10c_final_chunk_format() bool {
final_chunk := format_final_chunk()
// Final chunk must be exactly "0\r\n\r\n"
expected := '0\r\n\r\n'
if final_chunk != expected {
println(' Final chunk mismatch - expected "${expected}", got "${final_chunk}"')
return false
}
// Verify it starts with zero size
if !final_chunk.starts_with('0\r\n') {
println(' Final chunk should start with "0\\r\\n"')
return false
}
// Verify it ends with empty line (double CRLF after size)
if !final_chunk.ends_with('\r\n\r\n') {
println(' Final chunk should end with "\\r\\n\\r\\n"')
return false
}
return true
}
// Test 10d: Empty data produces empty string (no chunk)
fn test_property_10d_empty_data_handling() bool {
// Empty byte array
empty_bytes := []u8{}
chunk_bytes := format_chunk(empty_bytes)
if chunk_bytes != '' {
println(' Empty bytes should produce empty string, got "${chunk_bytes}"')
return false
}
// Empty string
chunk_string := format_chunk_string('')
if chunk_string != '' {
println(' Empty string should produce empty string, got "${chunk_string}"')
return false
}
return true
}
// Test 10e: Chunk size is correctly formatted as lowercase hex
fn test_property_10e_hex_format() bool {
rand.seed([u32(time.now().unix()), u32(98765)])
// Test various sizes including edge cases
test_sizes := [1, 15, 16, 255, 256, 4095, 4096, 65535]
for size in test_sizes {
// Create data of specific size
mut data := []u8{len: size}
for i in 0 .. size {
data[i] = u8(i % 256)
}
chunk := format_chunk(data)
// Extract hex size
mut crlf_pos := -1
for j := 0; j < chunk.len - 1; j++ {
if chunk[j] == `\r` && chunk[j + 1] == `\n` {
crlf_pos = j
break
}
}
if crlf_pos == -1 {
println(' Size ${size}: No CRLF found')
return false
}
size_hex := chunk[..crlf_pos]
// Verify it's valid lowercase hex
for c in size_hex {
is_digit := c >= `0` && c <= `9`
is_lower_hex := c >= `a` && c <= `f`
if !is_digit && !is_lower_hex {
println(' Size ${size}: Invalid hex character "${c.ascii_str()}" in "${size_hex}"')
return false
}
}
// Verify parsed value matches
parsed := parse_hex(size_hex) or {
println(' Size ${size}: Failed to parse hex "${size_hex}"')
return false
}
if parsed != size {
println(' Size ${size}: Hex "${size_hex}" parsed as ${parsed}')
return false
}
}
return true
}
// Test 10f: Round-trip property - chunk then parse returns original data
fn test_property_10f_roundtrip() bool {
rand.seed([u32(time.now().unix()), u32(11111)])
for i in 0 .. test_iterations {
// Generate random data
original := generate_random_bytes(500)
// Format as chunk
chunk := format_chunk(original)
// Parse back
recovered, _ := parse_chunked_data(chunk) or {
println(' Iteration ${i}: Failed to parse: ${err}')
return false
}
// Verify round-trip
if recovered != original {
println(' Iteration ${i}: Round-trip failed - data mismatch')
return false
}
}
return true
}
fn main() {
println('🚀 开始 PicoevStreamWriter 属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := PropertyTestStats{}
// Run Property 10 tests
// Feature: sse-streaming-helper, Property 10: Chunked 编码格式正确性
// Validates: Requirements 6.2
stats.run_property_test('Property 10a: format_chunk bytes encoding', test_property_10a_format_chunk_bytes)
stats.run_property_test('Property 10b: format_chunk_string encoding', test_property_10b_format_chunk_string)
stats.run_property_test('Property 10c: Final chunk format', test_property_10c_final_chunk_format)
stats.run_property_test('Property 10d: Empty data handling', test_property_10d_empty_data_handling)
stats.run_property_test('Property 10e: Hex format correctness', test_property_10e_hex_format)
stats.run_property_test('Property 10f: Round-trip property', test_property_10f_roundtrip)
// Print summary
stats.print_summary()
}
+549
View File
@@ -0,0 +1,549 @@
import rand
import time
import sync
// Rate Limit Middleware 属性测试
// Property-Based Testing for Rate Limiting functionality
//
// Feature: builtin-middleware, Property 12: Rate Limit Counter Accuracy
// Feature: builtin-middleware, Property 13: Rate Limit Window Reset
// Validates: Requirements 6.1-6.12
//
// Note: These tests validate the rate limiting logic using a local
// implementation that mirrors the MemoryStore behavior.
const test_iterations = 100
struct PropertyTestStats {
mut:
total_tests int
passed_tests int
failed_tests int
}
fn (mut stats PropertyTestStats) run_property_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 PropertyTestStats) print_summary() {
println('\n=== Rate Limit Middleware 属性测试总结 ===')
println('总测试数: ${stats.total_tests}')
println('通过: ${stats.passed_tests}')
println('失败: ${stats.failed_tests}')
if stats.failed_tests == 0 {
println('🎉 所有属性测试通过!')
} else {
println('⚠️ 有 ${stats.failed_tests} 个属性测试失败')
}
}
// ============================================================================
// Test Rate Limit Store Implementation (mirrors the actual implementation)
// ============================================================================
struct TestRateLimitEntry {
mut:
count int
reset_at i64
}
struct TestMemoryStore {
mut:
data map[string]TestRateLimitEntry
mtx sync.Mutex
}
fn TestMemoryStore.new() &TestMemoryStore {
return &TestMemoryStore{
data: map[string]TestRateLimitEntry{}
}
}
fn (mut s TestMemoryStore) increment(key string, window_ms i64) (int, i64) {
s.mtx.@lock()
defer { s.mtx.unlock() }
now := time.now().unix_milli()
if key in s.data {
mut entry := s.data[key]
if now >= entry.reset_at {
entry.count = 1
entry.reset_at = now + window_ms
} else {
entry.count++
}
s.data[key] = entry
return entry.count, entry.reset_at
}
new_entry := TestRateLimitEntry{
count: 1
reset_at: now + window_ms
}
s.data[key] = new_entry
return new_entry.count, new_entry.reset_at
}
fn (mut s TestMemoryStore) reset(key string) {
s.mtx.@lock()
defer { s.mtx.unlock() }
s.data.delete(key)
}
fn (s TestMemoryStore) get_entry(key string) ?TestRateLimitEntry {
if key in s.data {
return s.data[key]
}
return none
}
// ============================================================================
// Helper Functions
// ============================================================================
// 生成随机 IP 地址
fn generate_random_ip() string {
a := rand.int_in_range(1, 255) or { 192 }
b := rand.int_in_range(0, 255) or { 168 }
c := rand.int_in_range(0, 255) or { 1 }
d := rand.int_in_range(1, 255) or { 100 }
return '${a}.${b}.${c}.${d}'
}
// 生成随机 key
fn generate_random_key() string {
chars := 'abcdefghijklmnopqrstuvwxyz0123456789'
len := rand.int_in_range(5, 20) or { 10 }
mut result := ''
for _ in 0 .. len {
idx := rand.int_in_range(0, chars.len) or { 0 }
result += chars[idx].ascii_str()
}
return result
}
// ============================================================================
// Property 12: Rate Limit Counter Accuracy
// Feature: builtin-middleware, Property 12: Rate Limit Counter Accuracy
// Validates: Requirements 6.1, 6.2, 6.4, 6.5, 6.6
//
// *For any* client key and request sequence, the X-RateLimit-Remaining header
// SHALL equal (limit - request_count) for requests within the window.
// ============================================================================
fn test_property_12_counter_accuracy() bool {
rand.seed([u32(time.now().unix()), u32(12345)])
for i in 0 .. test_iterations {
// Create a fresh store for each iteration
mut store := TestMemoryStore.new()
// Generate random limit and window
limit := rand.int_in_range(5, 50) or { 10 }
window_ms := i64(rand.int_in_range(1000, 10000) or { 5000 })
// Generate random key
key := generate_random_key()
// Make multiple requests and verify counter accuracy
num_requests := rand.int_in_range(1, limit * 2) or { limit }
for req_num in 1 .. (num_requests + 1) {
count, _ := store.increment(key, window_ms)
// Verify count matches request number
if count != req_num {
println(' Iteration ${i}: Counter mismatch at request ${req_num}')
println(' Expected count: ${req_num}, Got: ${count}')
return false
}
// Calculate expected remaining
expected_remaining := if count > limit { 0 } else { limit - count }
actual_remaining := if count > limit { 0 } else { limit - count }
if actual_remaining != expected_remaining {
println(' Iteration ${i}: Remaining mismatch at request ${req_num}')
println(' Expected remaining: ${expected_remaining}, Got: ${actual_remaining}')
return false
}
}
}
return true
}
fn test_property_12_multiple_clients() bool {
rand.seed([u32(time.now().unix()), u32(23456)])
for i in 0 .. test_iterations {
// Create a fresh store
mut store := TestMemoryStore.new()
limit := 10
window_ms := i64(60000)
// Generate multiple client keys
num_clients := rand.int_in_range(2, 10) or { 5 }
mut client_keys := []string{}
for _ in 0 .. num_clients {
client_keys << generate_random_ip()
}
// Each client makes requests independently
for client_idx, key in client_keys {
num_requests := rand.int_in_range(1, 15) or { 5 }
for req_num in 1 .. (num_requests + 1) {
count, _ := store.increment(key, window_ms)
// Each client's counter should be independent
if count != req_num {
println(' Iteration ${i}: Client ${client_idx} counter mismatch')
println(' Expected: ${req_num}, Got: ${count}')
return false
}
}
}
// Verify each client has independent count
for client_idx, key in client_keys {
entry := store.get_entry(key) or {
println(' Iteration ${i}: Client ${client_idx} entry not found')
return false
}
// Entry should exist with some count
if entry.count <= 0 {
println(' Iteration ${i}: Client ${client_idx} has invalid count: ${entry.count}')
return false
}
}
}
return true
}
fn test_property_12_limit_enforcement() bool {
rand.seed([u32(time.now().unix()), u32(34567)])
for i in 0 .. test_iterations {
mut store := TestMemoryStore.new()
// Random limit
limit := rand.int_in_range(3, 20) or { 10 }
window_ms := i64(60000)
key := generate_random_key()
// Make exactly limit + 5 requests
for req_num in 1 .. (limit + 6) {
count, _ := store.increment(key, window_ms)
// Verify count is accurate
if count != req_num {
println(' Iteration ${i}: Count mismatch at request ${req_num}')
return false
}
// Check if request should be allowed or blocked
should_be_blocked := count > limit
is_blocked := count > limit
if should_be_blocked != is_blocked {
println(' Iteration ${i}: Limit enforcement error at request ${req_num}')
println(' Limit: ${limit}, Count: ${count}')
println(' Should be blocked: ${should_be_blocked}, Is blocked: ${is_blocked}')
return false
}
}
}
return true
}
// ============================================================================
// Property 13: Rate Limit Window Reset
// Feature: builtin-middleware, Property 13: Rate Limit Window Reset
// Validates: Requirements 6.3, 6.7
//
// *For any* client key, after the window_ms time has elapsed,
// the request count SHALL reset to 0.
// ============================================================================
fn test_property_13_window_reset() bool {
rand.seed([u32(time.now().unix()), u32(45678)])
for i in 0 .. test_iterations {
mut store := TestMemoryStore.new()
// Use a very short window for testing (simulated)
window_ms := i64(100) // 100ms window
key := generate_random_key()
// Make some requests
initial_requests := rand.int_in_range(1, 10) or { 5 }
mut last_count := 0
mut reset_at := i64(0)
for _ in 0 .. initial_requests {
last_count, reset_at = store.increment(key, window_ms)
}
// Verify initial count
if last_count != initial_requests {
println(' Iteration ${i}: Initial count mismatch')
println(' Expected: ${initial_requests}, Got: ${last_count}')
return false
}
// Verify reset_at is in the future
now := time.now().unix_milli()
if reset_at <= now {
// This can happen if the test runs slowly, skip this iteration
continue
}
// The reset_at should be approximately now + window_ms
expected_reset_at_min := now - window_ms // Allow for timing variance
expected_reset_at_max := now + window_ms + 100 // Allow some tolerance
if reset_at < expected_reset_at_min || reset_at > expected_reset_at_max {
println(' Iteration ${i}: Reset time out of expected range')
println(' Expected range: [${expected_reset_at_min}, ${expected_reset_at_max}]')
println(' Got: ${reset_at}')
return false
}
}
return true
}
fn test_property_13_reset_behavior() bool {
rand.seed([u32(time.now().unix()), u32(56789)])
// Test that reset() properly clears the entry
for i in 0 .. test_iterations {
mut store := TestMemoryStore.new()
window_ms := i64(60000)
key := generate_random_key()
// Make some requests
num_requests := rand.int_in_range(1, 20) or { 10 }
for _ in 0 .. num_requests {
store.increment(key, window_ms)
}
// Verify entry exists
entry_before := store.get_entry(key) or {
println(' Iteration ${i}: Entry should exist before reset')
return false
}
if entry_before.count != num_requests {
println(' Iteration ${i}: Count mismatch before reset')
return false
}
// Reset the entry
store.reset(key)
// Verify entry is cleared
if _ := store.get_entry(key) {
println(' Iteration ${i}: Entry should not exist after reset')
return false
}
// New request should start from 1
new_count, _ := store.increment(key, window_ms)
if new_count != 1 {
println(' Iteration ${i}: Count should be 1 after reset, got ${new_count}')
return false
}
}
return true
}
fn test_property_13_window_expiry_resets_count() bool {
rand.seed([u32(time.now().unix()), u32(67890)])
// This test verifies that when the window expires, the count resets
// We simulate this by manipulating the store directly
for i in 0 .. test_iterations {
mut store := TestMemoryStore.new()
window_ms := i64(1000) // 1 second window
key := generate_random_key()
// Make initial requests
initial_requests := rand.int_in_range(5, 15) or { 10 }
for _ in 0 .. initial_requests {
store.increment(key, window_ms)
}
// Get the entry and verify count
entry := store.get_entry(key) or {
println(' Iteration ${i}: Entry should exist')
return false
}
if entry.count != initial_requests {
println(' Iteration ${i}: Initial count mismatch')
return false
}
// The increment function should reset count when window expires
// We can verify this by checking that reset_at is properly set
now := time.now().unix_milli()
// Verify the reset_at is reasonable
// It should be approximately now + window_ms (from first request)
// or already expired
if entry.reset_at > now + window_ms + 1000 {
println(' Iteration ${i}: Reset time too far in future')
println(' Now: ${now}, Reset at: ${entry.reset_at}')
return false
}
}
return true
}
// ============================================================================
// Unit Tests for Rate Limit Store
// ============================================================================
fn test_memory_store_basic() {
mut store := TestMemoryStore.new()
// First request
count1, reset1 := store.increment('client1', 60000)
assert count1 == 1, 'First request should have count 1'
assert reset1 > time.now().unix_milli(), 'Reset time should be in future'
// Second request
count2, reset2 := store.increment('client1', 60000)
assert count2 == 2, 'Second request should have count 2'
assert reset2 == reset1, 'Reset time should not change within window'
// Different client
count3, _ := store.increment('client2', 60000)
assert count3 == 1, 'Different client should have count 1'
}
fn test_memory_store_reset() {
mut store := TestMemoryStore.new()
// Make some requests
store.increment('client1', 60000)
store.increment('client1', 60000)
store.increment('client1', 60000)
// Verify count
entry := store.get_entry('client1') or {
assert false, 'Entry should exist'
return
}
assert entry.count == 3, 'Count should be 3'
// Reset
store.reset('client1')
// Verify entry is gone
if _ := store.get_entry('client1') {
assert false, 'Entry should not exist after reset'
}
}
fn test_memory_store_multiple_clients() {
mut store := TestMemoryStore.new()
// Multiple clients
for i in 0 .. 5 {
key := 'client${i}'
for j in 0 .. (i + 1) {
count, _ := store.increment(key, 60000)
assert count == j + 1, 'Count should match request number'
}
}
// Verify each client has correct count
for i in 0 .. 5 {
key := 'client${i}'
entry := store.get_entry(key) or {
assert false, 'Entry should exist for ${key}'
return
}
assert entry.count == i + 1, 'Client ${i} should have count ${i + 1}'
}
}
fn test_rate_limit_remaining_calculation() {
// Test the remaining calculation logic
limits := [10, 50, 100, 1000]
for limit in limits {
for count in 0 .. (limit + 10) {
expected_remaining := if count > limit { 0 } else { limit - count }
actual_remaining := if count > limit { 0 } else { limit - count }
assert actual_remaining == expected_remaining, 'Remaining calculation error for limit=${limit}, count=${count}'
}
}
}
fn test_rate_limit_blocking_logic() {
// Test the blocking logic
limits := [5, 10, 20, 100]
for limit in limits {
for count in 1 .. (limit + 5) {
should_block := count > limit
is_blocked := count > limit
assert should_block == is_blocked, 'Blocking logic error for limit=${limit}, count=${count}'
}
}
}
// ============================================================================
// Main function for running property tests
// ============================================================================
fn main() {
println('🚀 开始 Rate Limit Middleware 属性测试...')
println('每个属性测试运行 ${test_iterations} 次迭代\n')
mut stats := PropertyTestStats{}
// Run property tests
// Feature: builtin-middleware, Property 12: Rate Limit Counter Accuracy
// Validates: Requirements 6.1, 6.2, 6.4, 6.5, 6.6
stats.run_property_test('Property 12: Counter Accuracy', test_property_12_counter_accuracy)
stats.run_property_test('Property 12: Multiple Clients', test_property_12_multiple_clients)
stats.run_property_test('Property 12: Limit Enforcement', test_property_12_limit_enforcement)
// Feature: builtin-middleware, Property 13: Rate Limit Window Reset
// Validates: Requirements 6.3, 6.7
stats.run_property_test('Property 13: Window Reset', test_property_13_window_reset)
stats.run_property_test('Property 13: Reset Behavior', test_property_13_reset_behavior)
stats.run_property_test('Property 13: Window Expiry Resets Count', test_property_13_window_expiry_resets_count)
// Print test summary
stats.print_summary()
}
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import net.http
// 重定向功能测试
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
import regex
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
import regex
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
import time
import net.http
+1 -1
View File
@@ -1,4 +1,4 @@
import hono
import meiseayoung.hono
fn main() {
println('=== 测试输入验证和路径安全 ===')

Some files were not shown because too many files have changed in this diff Show More