diff --git a/cache.v b/cache.v index 1d389d7..e10b57f 100644 --- a/cache.v +++ b/cache.v @@ -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() + } +} diff --git a/fast_router.v b/fast_router.v index 4e57f6c..d31f306 100644 --- a/fast_router.v +++ b/fast_router.v @@ -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,7 +177,7 @@ fn (router FastRouter) precompile_route(method string, handler IHandler) !Precom } } -// 快速路由匹配(优化版:静态路由跳过缓存 + 预分配结果 + 复用 cache key) +// 快速路由匹配(优化版:静态路由跳过缓存 + 预分配结果 + 高性能缓存) pub fn (mut router FastRouter) match_route(method string, path string) ?ContextRouteMatch { // 优化:只拼接一次 cache key,复用于静态路由和缓存查找 cache_key := '${method}:${path}' @@ -181,8 +187,13 @@ pub fn (mut router FastRouter) match_route(method string, path string) ?ContextR return result } - // 2. 动态路由先检查 LRU 缓存(避免重复正则匹配) - if router.cache_enabled { + // 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 } @@ -209,9 +220,13 @@ pub fn (mut router FastRouter) match_route(method string, path string) ?ContextR base_path: '' } - // 只缓存动态路由结果(复用 cache_key) + // 缓存动态路由结果 if router.cache_enabled { - router.lru_cache.put(cache_key, result) + if router.use_fast_cache { + router.fast_cache.put(cache_key, result) + } else { + router.lru_cache.put(cache_key, result) + } } return result diff --git a/tests/benchmark_compare.go b/tests/benchmark_compare.go new file mode 100644 index 0000000..ab679de --- /dev/null +++ b/tests/benchmark_compare.go @@ -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✅ 测试完成!") +} diff --git a/tests/test_cache_hit_rate.v b/tests/test_cache_hit_rate.v new file mode 100644 index 0000000..de5c10f --- /dev/null +++ b/tests/test_cache_hit_rate.v @@ -0,0 +1,81 @@ +// 测试缓存命中率 +module main + +import 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=== 测试完成 ===') +} diff --git a/tests/test_fast_cache_optimization.v b/tests/test_fast_cache_optimization.v new file mode 100644 index 0000000..38d48b1 --- /dev/null +++ b/tests/test_fast_cache_optimization.v @@ -0,0 +1,381 @@ +// FastCache 优化测试 +// 验证高性能缓存的正确性和性能 +module main + +import 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_basic_cache_operations() + + // 2. 路由匹配正确性测试 + results << test_route_matching_correctness() + + // 3. 参数提取正确性测试 + results << test_param_extraction() + + // 4. 缓存命中测试 + results << test_cache_hit() + + // 5. 缓存容量测试 + results << test_cache_capacity() + + // 6. 多路由并发测试 + results << test_multiple_routes() + + // 7. 性能基准测试 + results << test_performance_benchmark() + + // 8. 缓存一致性测试 + results << test_cache_consistency() + + // 打印结果 + 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_basic_cache_operations() TestResult { + mut cache := hono.FastRouteCache.new(100) + + // 测试 put 和 get + test_match := hono.ContextRouteMatch{ + params: {'id': '123'} + path: '/users/:id' + } + + cache.put('GET:/users/123', test_match) + + if result := cache.get('GET:/users/123') { + if result.params['id'] or { '' } == '123' { + size, capacity := cache.get_stats() + return TestResult{ + name: '基本缓存操作' + passed: true + detail: '缓存大小: ${size}/${capacity}' + } + } + } + + return TestResult{ + name: '基本缓存操作' + passed: false + detail: '缓存 get/put 失败' + } +} + +// 2. 路由匹配正确性测试 +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: '单参数和多参数路由均正确'} +} + +// 3. 参数提取正确性测试 +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} 个测试用例全部通过'} +} + +// 4. 缓存命中测试 +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.fast_cache.get_stats() + + // 第二次匹配(应该命中缓存) + if match2 := router.match_route('GET', '/users/123') { + if match2.params['id'] or { '' } == '123' { + cache_size2, _ := router.fast_cache.get_stats() + return TestResult{ + name: '缓存命中测试' + passed: true + detail: '缓存大小: ${cache_size1} -> ${cache_size2},参数正确' + } + } + } + + return TestResult{name: '缓存命中测试', passed: false, detail: '缓存命中后参数错误'} +} + +// 5. 缓存容量测试 +fn test_cache_capacity() TestResult { + mut cache := hono.FastRouteCache.new(10) // 小容量测试 + + // 填满缓存 + for i in 0 .. 10 { + cache.put('key${i}', hono.ContextRouteMatch{path: '/test/${i}'}) + } + + size1, _ := cache.get_stats() + if size1 != 10 { + return TestResult{name: '缓存容量测试', passed: false, detail: '填充后大小错误: ${size1}'} + } + + // 超出容量,应该清空重建 + cache.put('key_overflow', hono.ContextRouteMatch{path: '/overflow'}) + + size2, _ := cache.get_stats() + if size2 != 1 { + return TestResult{name: '缓存容量测试', passed: false, detail: '溢出后大小错误: ${size2}'} + } + + // 验证新数据存在 + if _ := cache.get('key_overflow') { + return TestResult{name: '缓存容量测试', passed: true, detail: '容量限制和清空重建正常'} + } + + return TestResult{name: '缓存容量测试', passed: false, detail: '溢出后数据丢失'} +} + +// 6. 多路由并发测试 +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} 个路径全部正确'} +} + +// 7. 性能基准测试 +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 + + // 缓存命中应该 < 1μs + if avg_us < 1.0 { + return TestResult{ + name: '性能基准测试' + passed: true + detail: '缓存命中平均耗时: ${avg_us:.3f}μs (< 1μs ✓)' + } + } else if avg_us < 5.0 { + return TestResult{ + name: '性能基准测试' + passed: true + detail: '缓存命中平均耗时: ${avg_us:.3f}μs (< 5μs, 可接受)' + } + } + + return TestResult{ + name: '性能基准测试' + passed: false + detail: '缓存命中平均耗时: ${avg_us:.3f}μs (> 5μs, 过慢)' + } +} + +// 8. 缓存一致性测试 +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}'} +} diff --git a/tests/test_picoev_integration b/tests/test_picoev_integration new file mode 100755 index 0000000..25e990e Binary files /dev/null and b/tests/test_picoev_integration differ diff --git a/tests/test_picoev_integration.go b/tests/test_picoev_integration.go new file mode 100644 index 0000000..80a9d5b --- /dev/null +++ b/tests/test_picoev_integration.go @@ -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) +} diff --git a/tests/test_picoev_integration.v b/tests/test_picoev_integration.v deleted file mode 100644 index 75acff2..0000000 --- a/tests/test_picoev_integration.v +++ /dev/null @@ -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 -} diff --git a/tests/test_picoev_server b/tests/test_picoev_server new file mode 100755 index 0000000..0b9c29e Binary files /dev/null and b/tests/test_picoev_server differ diff --git a/tests/test_usockets_integration.go b/tests/test_usockets_integration.go new file mode 100644 index 0000000..fdc614d --- /dev/null +++ b/tests/test_usockets_integration.go @@ -0,0 +1,268 @@ +// uSockets 集成测试 (Go 版本) +// +// 使用方法: +// 1. 先启动测试服务器: v -enable-globals run v-hono/tests/test_usockets_server.v +// 2. 运行测试: go run v-hono/tests/test_usockets_integration.go + +package main + +import ( + "fmt" + "io" + "net" + "net/http" + "strings" + "time" +) + +const baseURL = "http://127.0.0.1:9998" + +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("🎉 所有测试通过!uSockets 后端验证成功!") + } + fmt.Println("═══════════════════════════════════════════════════════════════") +} + +func main() { + fmt.Println("╔═══════════════════════════════════════════════════════════════╗") + fmt.Println("║ uSockets 集成测试 (Go) ║") + fmt.Println("╚═══════════════════════════════════════════════════════════════╝") + fmt.Println() + + // 验证服务器是否运行 + fmt.Println("🔍 检查 uSockets 测试服务器...") + if !checkServerReady() { + fmt.Println("❌ 服务器未运行") + fmt.Println(" 请先启动: v -enable-globals run v-hono/tests/test_usockets_server.v") + return + } + fmt.Println("✅ 服务器已就绪") + fmt.Println() + + stats := &TestStats{} + + // 1. 基本路由测试 + fmt.Println("📦 1. 基本路由测试") + stats.run("GET /", testGetRoot) + stats.run("GET /health", testGetHealth) + fmt.Println() + + // 2. JSON 响应测试 + fmt.Println("📦 2. JSON 响应测试") + stats.run("GET /api/json", testJSONResponse) + fmt.Println() + + // 3. 动态路由测试 + fmt.Println("📦 3. 动态路由测试") + stats.run("GET /api/users/:id", testSingleParam) + stats.run("GET /api/users/:user_id/posts/:post_id", testMultiParams) + fmt.Println() + + // 4. 查询参数测试 + fmt.Println("📦 4. 查询参数测试") + stats.run("GET /api/search?q=hello", testQueryParam) + fmt.Println() + + // 5. 404 测试 + fmt.Println("📦 5. 404 测试") + stats.run("404 Not Found", testNotFound) + fmt.Println() + + // 6. Keep-Alive 测试 + fmt.Println("📦 6. Keep-Alive 连接测试") + stats.run("Keep-Alive 连接复用", testKeepAlive) + fmt.Println() + + // 7. 性能测试 + fmt.Println("📦 7. 性能测试") + stats.run("吞吐量测试", testThroughput) + fmt.Println() + + // 8. 高并发基准测试 + fmt.Println("📦 8. 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 +} + +func testGetRoot() bool { + resp, body := doGet("/") + return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "Hello from uSockets") +} + +func testGetHealth() bool { + resp, body := doGet("/health") + return resp != nil && resp.StatusCode == 200 && body == "OK" +} + +func testJSONResponse() bool { + resp, body := doGet("/api/json") + if resp == nil { + return false + } + contentType := resp.Header.Get("Content-Type") + return resp.StatusCode == 200 && strings.Contains(contentType, "application/json") && strings.Contains(body, "Hello JSON") +} + +func testSingleParam() bool { + resp, body := doGet("/api/users/123") + return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "123") +} + +func testMultiParams() bool { + resp, body := doGet("/api/users/100/posts/200") + return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "100") && strings.Contains(body, "200") +} + +func testQueryParam() bool { + resp, body := doGet("/api/search?q=hello") + return resp != nil && resp.StatusCode == 200 && strings.Contains(body, "hello") +} + +func testNotFound() bool { + resp, _ := doGet("/nonexistent") + return resp != nil && resp.StatusCode == 404 +} + +func testKeepAlive() bool { + 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 + "/health") + if err != nil || resp.StatusCode != 200 { + return false + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + return true +} + +func testThroughput() bool { + client := &http.Client{Timeout: 5 * time.Second} + requests := 100 + success := 0 + + start := time.Now() + for i := 0; i < requests; i++ { + resp, err := client.Get(baseURL + "/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) + + return success >= requests*9/10 +} + +func testBenchmark() { + requests := 5000 + + conn, err := net.Dial("tcp", "127.0.0.1:9998") + 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:9998\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:9998\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) +} diff --git a/tests/test_usockets_server.v b/tests/test_usockets_server.v new file mode 100644 index 0000000..b5c6fbb --- /dev/null +++ b/tests/test_usockets_server.v @@ -0,0 +1,50 @@ +// uSockets 测试服务器 +module main + +import net.http +import hono + +fn main() { + mut app := hono.Hono.new() + + // 基本路由 + app.get('/', fn (mut c hono.Context) http.Response { + return c.text('Hello from uSockets!') + }) + + app.get('/health', fn (mut c hono.Context) http.Response { + return c.text('OK') + }) + + // JSON 响应 + app.get('/api/json', fn (mut c hono.Context) http.Response { + return c.json('{"message": "Hello JSON"}') + }) + + // 动态路由 + app.get('/api/users/:id', fn (mut c hono.Context) http.Response { + id := c.params['id'] or { '' } + return c.json('{"id": "${id}"}') + }) + + app.get('/api/users/: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/search', fn (mut c hono.Context) http.Response { + q := c.query['q'] or { '' } + return c.json('{"query": "${q}"}') + }) + + // 404 处理 + app.not_found(fn (mut c hono.Context) http.Response { + c.status(404) + return c.json('{"error": "Not Found"}') + }) + + println('[usockets-test-server] Starting on port 9998...') + app.listen_usockets(9998) +} diff --git a/tests/test_usockets_server_simple.v b/tests/test_usockets_server_simple.v new file mode 100644 index 0000000..a6b763e --- /dev/null +++ b/tests/test_usockets_server_simple.v @@ -0,0 +1,44 @@ +// uSockets 简化测试服务器 - 与外层 main.v 配置一致 +module main + +import hono +import net.http +import x.json2 + +// JSON 响应结构体 +struct JsonResponse { + message string +} + +struct UserResponse { + user_id string +} + +fn main() { + mut app := hono.Hono.new() + + // 与外层 main.v 完全一致的路由配置(无中间件) + app.get('/', fn (mut c hono.Context) http.Response { + return c.text('Hello, World!') + }) + + app.get('/json', fn (mut c hono.Context) http.Response { + data := JsonResponse{message: 'Hello, JSON!'} + json_str := json2.encode(data) + return c.json(json_str) + }) + + app.get('/users/:id', fn (mut c hono.Context) http.Response { + user_id := c.params['id'] or { 'unknown' } + data := UserResponse{user_id: user_id} + json_str := json2.encode(data) + return c.json(json_str) + }) + + app.get('/health', fn (mut c hono.Context) http.Response { + return c.text('OK') + }) + + println('[usockets-simple] Starting on port 9999 (uSockets)...') + app.listen_usockets(9999) +} diff --git a/usockets/usockets.v b/usockets/usockets.v index 8a46583..5495cac 100644 --- a/usockets/usockets.v +++ b/usockets/usockets.v @@ -1,11 +1,43 @@ module usockets -#flag -DLIBUS_USE_LIBUV -#flag -DLIBUS_NO_SSL -#flag -IC:/Users/admin/.vmodules/hono/usockets/include -#flag -LC:/Users/admin/.vmodules/hono/usockets/lib -#flag C:/Users/admin/.vmodules/hono/usockets/lib/libusockets_full.a -#flag -lws2_32 -liphlpapi -lpsapi -luserenv -lole32 +// 条件编译:根据操作系统选择正确的路径和库 +// @VMODROOT 指向包含 v.mod 的模块根目录 (v-hono/) + +$if windows { + #flag -DLIBUS_USE_LIBUV + #flag -DLIBUS_NO_SSL + #flag -I@VMODROOT/usockets/include + #flag -L@VMODROOT/lib + #flag @VMODROOT/lib/libusockets_full.a + #flag -lws2_32 -liphlpapi -lpsapi -luserenv -lole32 +} $else $if macos { + // macOS: 支持 Apple Silicon (M1/M2/M3) 和 Intel Mac + #flag -DLIBUS_USE_LIBUV + #flag -DLIBUS_NO_SSL + + // libuv 路径 - 根据架构选择 Homebrew 路径 + $if arm64 { + #flag -I/opt/homebrew/include + #flag -L/opt/homebrew/lib + } $else { + #flag -I/usr/local/include + #flag -L/usr/local/lib + } + + // uSockets 库路径 + #flag -I@VMODROOT/usockets/include + #flag -L@VMODROOT/lib + #flag @VMODROOT/lib/libusockets_full.a + #flag -luv +} $else { + // Linux 和其他平台 + #flag -DLIBUS_USE_LIBUV + #flag -DLIBUS_NO_SSL + #flag -I@VMODROOT/usockets/include + #flag -L@VMODROOT/lib + #flag @VMODROOT/lib/libusockets_full.a + #flag -luv +} #include "libusockets.h" @@ -20,13 +52,13 @@ struct C.us_socket_t {} struct C.us_listen_socket_t {} struct C.us_socket_context_options_t { -key_file_name voidptr -cert_file_name voidptr -passphrase voidptr -dh_params_file_name voidptr -ca_file_name voidptr -ssl_ciphers voidptr -ssl_prefer_low_memory_usage int + key_file_name voidptr + cert_file_name voidptr + passphrase voidptr + dh_params_file_name voidptr + ca_file_name voidptr + ssl_ciphers voidptr + ssl_prefer_low_memory_usage int } fn C.us_create_loop(hint voidptr, wakeup_cb voidptr, pre_cb voidptr, post_cb voidptr, ext_size u32) Loop @@ -54,15 +86,15 @@ fn empty_pre(loop Loop) {} fn empty_post(loop Loop) {} pub fn create_loop() Loop { -return C.us_create_loop(unsafe { nil }, empty_wakeup, empty_pre, empty_post, 0) + return C.us_create_loop(unsafe { nil }, empty_wakeup, empty_pre, empty_post, 0) } pub fn (l Loop) run() { C.us_loop_run(l) } pub fn (l Loop) free() { C.us_loop_free(l) } pub fn create_socket_context(loop Loop) SocketContext { -options := C.us_socket_context_options_t{} -return C.us_create_socket_context(0, loop, 0, options) + options := C.us_socket_context_options_t{} + return C.us_create_socket_context(0, loop, 0, options) } pub fn (ctx SocketContext) free() { C.us_socket_context_free(0, ctx) } @@ -73,7 +105,7 @@ pub fn (ctx SocketContext) on_writable(h voidptr) { C.us_socket_context_on_writa pub fn (ctx SocketContext) on_timeout(h voidptr) { C.us_socket_context_on_timeout(0, ctx, h) } pub fn (ctx SocketContext) on_end(h voidptr) { C.us_socket_context_on_end(0, ctx, h) } pub fn (ctx SocketContext) listen(port int) ListenSocket { -return C.us_socket_context_listen(0, ctx, unsafe { nil }, port, 0, 0) + return C.us_socket_context_listen(0, ctx, unsafe { nil }, port, 0, 0) } pub fn (s Socket) write_bytes(data string) int { return C.us_socket_write(0, s, data.str, data.len, 0) }