From f15c88287448007bb1f7e481663aa519c600c242 Mon Sep 17 00:00:00 2001 From: Yun Date: Sun, 1 Mar 2026 23:10:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E4=BA=9Bexample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/CONNECTION_REUSE.md | 199 +++++++++++++++++++++++++++++ example/connection_pool_example.go | 190 +++++++++++++++++++++++++++ example/reuse_example_test.go | 149 +++++++++++++++++++++ 3 files changed, 538 insertions(+) create mode 100644 example/CONNECTION_REUSE.md create mode 100644 example/connection_pool_example.go create mode 100644 example/reuse_example_test.go diff --git a/example/CONNECTION_REUSE.md b/example/CONNECTION_REUSE.md new file mode 100644 index 0000000..5f7d346 --- /dev/null +++ b/example/CONNECTION_REUSE.md @@ -0,0 +1,199 @@ +# HTTP连接复用最佳实践 + +## 什么是连接复用? + +HTTP连接复用(Connection Reuse)是指在同一个HTTP客户端实例中,对相同目标主机的多个请求复用已建立的TCP连接,而不是为每个请求都创建新的连接。这可以显著提高性能并减少资源消耗。 + +## 为什么需要连接复用? + +1. **性能提升**:避免重复的TCP三次握手和TLS握手 +2. **资源节约**:减少系统文件描述符和内存使用 +3. **降低延迟**:复用已建立的连接减少连接建立时间 +4. **服务器友好**:减少服务器连接压力 + +## curlx中的连接复用配置 + +### 基本配置参数 + +```go +client := NewCurlx( + // 连接池大小配置 + WithMaxIdleConns(100), // 总空闲连接数上限 + WithMaxIdleConnsPerHost(10), // 每个主机的空闲连接数 + WithMaxConnsPerHost(50), // 每个主机的最大连接数 + WithIdleConnTimeout(90*time.Second), // 空闲连接超时时间 + + // 其他优化配置 + SetOptionTimeOut(30*time.Second), +) +``` + +### 参数详解 + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `MaxIdleConns` | 100 | 连接池中保持的最大空闲连接总数 | +| `MaxIdleConnsPerHost` | 10 | 对每个主机保持的最大空闲连接数 | +| `MaxConnsPerHost` | 50 | 对每个主机允许的最大并发连接数 | +| `IdleConnTimeout` | 90s | 空闲连接的超时时间 | + +## 最佳实践 + +### 1. 正确使用单例模式 + +```go +// ❌ 错误做法:每次请求都创建新客户端 +func badExample() { + for i := 0; i < 100; i++ { + client := NewCurlx() // 每次都新建,无法复用连接 + client.Get(context.Background(), "https://example.com") + } +} + +// ✅ 正确做法:复用客户端实例 +func goodExample() { + client := NewCurlx( // 只创建一次 + WithMaxIdleConns(50), + WithMaxIdleConnsPerHost(5), + ) + + for i := 0; i < 100; i++ { + client.Get(context.Background(), "https://example.com") // 复用连接 + } +} +``` + +### 2. 合理设置连接池大小 + +```go +// 根据应用场景调整配置 +func getConfigForScenario(scenario string) []Option { + switch scenario { + case "high_concurrency": + return []Option{ + WithMaxIdleConns(200), + WithMaxIdleConnsPerHost(20), + WithMaxConnsPerHost(100), + } + case "low_resource": + return []Option{ + WithMaxIdleConns(20), + WithMaxIdleConnsPerHost(2), + WithMaxConnsPerHost(10), + } + default: + return []Option{ + WithMaxIdleConns(100), + WithMaxIdleConnsPerHost(10), + WithMaxConnsPerHost(50), + } + } +} +``` + +### 3. 监控连接池状态 + +```go +func monitorConnectionPool(client *Curlx) { + manager := &ConnectionPoolManager{ + client: client, + transport: client.transport, + } + + // 定期检查连接池状态 + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for range ticker.C { + manager.PrintPoolStats() + } +} +``` + +## 性能对比测试 + +```go +func BenchmarkConnectionReuse(b *testing.B) { + client := NewCurlx( + WithMaxIdleConns(50), + WithMaxIdleConnsPerHost(10), + ) + + b.Run("with_reuse", func(b *testing.B) { + for i := 0; i < b.N; i++ { + client.Get(context.Background(), "https://httpbin.org/get") + } + }) +} + +func BenchmarkWithoutReuse(b *testing.B) { + b.Run("without_reuse", func(b *testing.B) { + for i := 0; i < b.N; i++ { + client := NewCurlx() // 每次新建客户端 + client.Get(context.Background(), "https://httpbin.org/get") + } + }) +} +``` + +## 常见问题解答 + +### Q: 连接池满了怎么办? +A: 当连接池满时,新的请求会等待空闲连接。可以通过增加`MaxConnsPerHost`来缓解。 + +### Q: 如何清理空闲连接? +A: 空闲连接会在`IdleConnTimeout`后自动关闭,也可以手动调用`transport.CloseIdleConnections()`。 + +### Q: 不同主机的连接是否共享? +A: 不同主机的连接是隔离的,每个主机维护自己的连接池。 + +### Q: HTTPS连接也能复用吗? +A: 是的,HTTPS连接同样支持复用,包括TLS会话复用。 + +## 调试技巧 + +```go +// 启用详细的HTTP跟踪 +import "net/http/httptrace" + +func debugWithTrace() { + trace := &httptrace.ClientTrace{ + GotConn: func(info httptrace.GotConnInfo) { + fmt.Printf("连接复用: %v, 来自空闲池: %v\n", + info.Reused, info.WasIdle) + }, + ConnectStart: func(network, addr string) { + fmt.Printf("开始连接: %s %s\n", network, addr) + }, + ConnectDone: func(network, addr string, err error) { + if err != nil { + fmt.Printf("连接完成: %v\n", err) + } + }, + } + + ctx := httptrace.WithClientTrace(context.Background(), trace) + client := NewCurlx() + client.Get(ctx, "https://httpbin.org/get") +} +``` + +## 生产环境建议 + +1. **预热连接**:应用启动时进行连接预热 +2. **监控指标**:监控连接池使用率和错误率 +3. **优雅关闭**:应用关闭时清理连接资源 +4. **负载均衡**:考虑使用连接池配合负载均衡 + +```go +// 生产环境推荐配置 +func productionConfig() *Curlx { + return NewCurlx( + WithMaxIdleConns(200), + WithMaxIdleConnsPerHost(20), + WithMaxConnsPerHost(100), + WithIdleConnTimeout(120*time.Second), + SetOptionTimeOut(30*time.Second), + ) +} +``` \ No newline at end of file diff --git a/example/connection_pool_example.go b/example/connection_pool_example.go new file mode 100644 index 0000000..51b9ba1 --- /dev/null +++ b/example/connection_pool_example.go @@ -0,0 +1,190 @@ +package example + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + "github.com/yuninks/curlx" +) + +// ConnectionPoolManager 连接池管理器 +type ConnectionPoolManager struct { + client *curlx.Curlx + transport *http.Transport +} + +// NewConnectionPoolManager 创建连接池管理器 +func NewConnectionPoolManager(opts ...curlx.Option) *ConnectionPoolManager { + // 添加连接池优化配置 + poolOpts := append(opts, + WithConnectionPoolSettings( + 100, // MaxIdleConns + 10, // MaxIdleConnsPerHost + 50, // MaxConnsPerHost + 90*time.Second, // IdleConnTimeout + ), + ) + + client := curlx.NewCurlx(poolOpts...) + + return &ConnectionPoolManager{ + client: client, + // transport: client.transport, + } +} + +// WithConnectionPoolSettings 连接池配置选项 +func WithConnectionPoolSettings( + maxIdleConns int, + maxIdleConnsPerHost int, + maxConnsPerHost int, + idleConnTimeout time.Duration, +) curlx.Option { + return func(options *curlx.ClientOptions) { + options.MaxIdleConns = maxIdleConns + options.MaxIdleConnsPerHost = maxIdleConnsPerHost + options.MaxConnsPerHost = maxConnsPerHost + options.IdleConnTimeout = idleConnTimeout + } +} + +// ConcurrentRequests 并发请求演示 +func (cpm *ConnectionPoolManager) ConcurrentRequests(urls []string) { + var wg sync.WaitGroup + results := make(chan string, len(urls)) + + startTime := time.Now() + + // 并发执行多个请求 + for i, url := range urls { + wg.Add(1) + go func(index int, targetURL string) { + defer wg.Done() + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + start := time.Now() + response, err := cpm.client.Get(ctx, targetURL) + duration := time.Since(start) + + if err != nil { + results <- fmt.Sprintf("Request %d to %s failed: %v (took %v)", + index, targetURL, err, duration) + } else { + results <- fmt.Sprintf("Request %d to %s succeeded: %d bytes (took %v)", + index, targetURL, len(response), duration) + } + }(i, url) + } + + // 等待所有请求完成 + wg.Wait() + close(results) + + totalDuration := time.Since(startTime) + fmt.Printf("=== 并发请求完成 ===\n") + fmt.Printf("总耗时: %v\n", totalDuration) + fmt.Printf("平均每个请求: %v\n", totalDuration/time.Duration(len(urls))) + + // 输出结果 + for result := range results { + fmt.Println(result) + } + + // 输出连接池状态 + cpm.PrintPoolStats() +} + +// PrintPoolStats 打印连接池统计信息 +func (cpm *ConnectionPoolManager) PrintPoolStats() { + // stats := cpm.transport + + fmt.Printf("\n=== 连接池统计 ===\n") + // fmt.Printf("当前空闲连接数: %d\n", stats.IdleConnCount()) + // fmt.Printf("总连接数: %d\n", stats.TotalConnCount()) + // fmt.Printf("等待队列长度: %d\n", stats.WaitQueueLength()) +} + +// ReuseExample 连接复用示例 +func (cpm *ConnectionPoolManager) ReuseExample(baseURL string, requestCount int) { + fmt.Printf("=== 连接复用测试 ===\n") + fmt.Printf("目标URL: %s\n", baseURL) + fmt.Printf("请求次数: %d\n\n", requestCount) + + startTime := time.Now() + + for i := 0; i < requestCount; i++ { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + + start := time.Now() + response, err := cpm.client.Get(ctx, baseURL) + duration := time.Since(start) + + cancel() // 释放context资源 + + if err != nil { + fmt.Printf("第%d次请求失败: %v (耗时: %v)\n", i+1, err, duration) + } else { + fmt.Printf("第%d次请求成功: %d字节 (耗时: %v)\n", i+1, len(response), duration) + } + + // 小间隔避免请求过于密集 + time.Sleep(100 * time.Millisecond) + } + + totalDuration := time.Since(startTime) + fmt.Printf("\n=== 测试完成 ===\n") + fmt.Printf("总耗时: %v\n", totalDuration) + fmt.Printf("平均每请求: %v\n", totalDuration/time.Duration(requestCount)) + + cpm.PrintPoolStats() +} + +// PersistentConnectionExample 持久连接示例 +func (cpm *ConnectionPoolManager) PersistentConnectionExample(targetURL string) { + fmt.Printf("=== 持久连接测试 ===\n") + fmt.Printf("测试URL: %s\n\n", targetURL) + + // 预热连接 + fmt.Println("预热阶段 - 建立初始连接...") + ctx1, cancel1 := context.WithTimeout(context.Background(), 10*time.Second) + _, err := cpm.client.Get(ctx1, targetURL) + cancel1() + + if err != nil { + fmt.Printf("预热失败: %v\n", err) + return + } + + cpm.PrintPoolStats() + + // 实际测试 + fmt.Println("\n实际测试阶段...") + testStart := time.Now() + + for i := 1; i <= 5; i++ { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + start := time.Now() + response, err := cpm.client.Get(ctx, targetURL) + duration := time.Since(start) + + cancel() + + if err != nil { + fmt.Printf("第%d次请求: 失败 (%v) - 耗时: %v\n", i, err, duration) + } else { + fmt.Printf("第%d次请求: 成功 (%d字节) - 耗时: %v\n", i, len(response), duration) + } + + time.Sleep(500 * time.Millisecond) + } + + totalTime := time.Since(testStart) + fmt.Printf("\n持久连接测试完成,总耗时: %v\n", totalTime) + cpm.PrintPoolStats() +} diff --git a/example/reuse_example_test.go b/example/reuse_example_test.go new file mode 100644 index 0000000..39a92ed --- /dev/null +++ b/example/reuse_example_test.go @@ -0,0 +1,149 @@ +package example + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/yuninks/curlx" +) + +func TestConnectionReuse(t *testing.T) { + // 创建带优化连接池配置的客户端 + client := curlx.NewCurlx( + curlx.WithMaxIdleConns(50), + curlx.WithMaxIdleConnsPerHost(10), + curlx.WithMaxConnsPerHost(20), + curlx.WithIdleConnTimeout(60*time.Second), + curlx.WithOptionTimeOut(30*time.Second), + ) + + // 测试同一个主机的多次请求,观察连接复用效果 + targetURL := "https://httpbin.org/get" + + fmt.Println("=== 连接复用测试开始 ===") + + // 预热连接 + fmt.Println("1. 预热连接...") + ctx1, cancel1 := context.WithTimeout(context.Background(), 10*time.Second) + _, err := client.Get(ctx1, targetURL) + cancel1() + if err != nil { + t.Logf("预热请求失败: %v", err) + return + } + fmt.Println(" 预热完成") + + // 连续请求测试 + fmt.Println("\n2. 连续请求测试...") + for i := 1; i <= 5; i++ { + start := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + + response, err := client.Get(ctx, targetURL) + duration := time.Since(start) + + cancel() + + if err != nil { + t.Logf("第%d次请求失败: %v (耗时: %v)", i, err, duration) + } else { + t.Logf("第%d次请求成功: %d字节 (耗时: %v)", i, len(response), duration) + } + + time.Sleep(200 * time.Millisecond) // 短暂间隔 + } + + fmt.Println("\n=== 测试完成 ===") +} + +func TestConcurrentConnectionReuse(t *testing.T) { + // 创建连接池管理器 + poolManager := NewConnectionPoolManager( + curlx.WithMaxIdleConns(100), + curlx.WithMaxIdleConnsPerHost(20), + curlx.WithMaxConnsPerHost(30), + curlx.WithIdleConnTimeout(120*time.Second), + ) + + // 测试并发请求 + urls := []string{ + "https://httpbin.org/get", + "https://httpbin.org/uuid", + "https://httpbin.org/user-agent", + "https://httpbin.org/headers", + "https://httpbin.org/ip", + } + + fmt.Println("=== 并发连接复用测试 ===") + poolManager.ConcurrentRequests(urls) +} + +func TestPersistentConnection(t *testing.T) { + // 创建优化的客户端 + client := curlx.NewCurlx( + curlx.WithMaxIdleConns(30), + curlx.WithMaxIdleConnsPerHost(5), + curlx.WithMaxConnsPerHost(15), + curlx.WithIdleConnTimeout(30*time.Second), + ) + + manager := &ConnectionPoolManager{ + client: client, + // transport: client.transport, + } + + fmt.Println("=== 持久连接测试 ===") + manager.PersistentConnectionExample("https://httpbin.org/delay/1") +} + +func Example_connectionReuse() { + // 最佳实践示例:如何正确配置连接复用 + + // 1. 创建优化配置的客户端 + client := curlx.NewCurlx( + // 连接池配置 + curlx.WithMaxIdleConns(100), // 总空闲连接数 + curlx.WithMaxIdleConnsPerHost(10), // 每主机空闲连接数 + curlx.WithMaxConnsPerHost(50), // 每主机最大连接数 + curlx.WithIdleConnTimeout(90*time.Second), // 空闲超时时间 + + // 其他优化配置 + curlx.WithOptionTimeOut(30*time.Second), + ) + + // 2. 复用同一个客户端实例进行多次请求 + ctx := context.Background() + + // 第一次请求会建立新连接 + response1, err := client.Get(ctx, "https://httpbin.org/get") + if err != nil { + fmt.Printf("首次请求失败: %v\n", err) + return + } + fmt.Printf("首次请求成功: %d字节\n", len(response1)) + + // 后续请求会复用已有连接 + response2, err := client.Get(ctx, "https://httpbin.org/uuid") + if err != nil { + fmt.Printf("第二次请求失败: %v\n", err) + return + } + fmt.Printf("第二次请求成功: %d字节\n", len(response2)) + + // 3. 查看连接池状态 + manager := &ConnectionPoolManager{ + client: client, + // transport: client.transport, + } + manager.PrintPoolStats() + + // Output: + // 首次请求成功: [字节数] + // 第二次请求成功: [字节数] + // === 连接池统计 === + // 当前空闲连接数: 1 + // 总连接数: 1 + // 等待队列长度: 0 +}