-
C#网络编程之连接池与复用(TCP连接池、HTTP连接池)
第18章 连接池与复用
18.1 连接池与复用(TCP连接池、HTTP连接池)
一、为什么要搞连接池?我踩过的“连接爆炸坑”
刚写了一个TCP客户端,每次请求都创建新的TCP连接——结果并发到1000时,服务器的TIME_WAIT连接数超过了65535,导致新连接无法建立,程序直接崩溃。后来用TCP连接池复用连接,TIME_WAIT连接数降到了100以内,性能提升了10倍以上。这节我把自己从“连接爆炸”到“连接复用”的踩坑经验揉进去,用大白话讲透连接池的原理,结合C#代vb.net教程C#教程python教程SQL教程access 2010教程码逐行讲解TCP连接池和HTTP连接池的实战用法,以及连接池的监控和调优技巧,让你的网络程序性能飙升。
二、先搞懂连接池的核心:“复用连接,减少开销”
连接池的核心是预先创建一批连接,放在池中,需要时从池中获取,用完后放回池中,重复使用——避免频繁创建和销毁连接的开销,提高性能。
类比:连接池就像公司的共享打印机——不用每个人买一台打印机,而是公司买几台打印机放在共享区域,需要时去用,用完后放回,重复使用。这样既节省成本,又提高效率。连接池也是一样,预先创建一批TCP/HTTP连接,需要时从池中获取,用完后放回,重复使用,避免频繁创建和销毁连接的开销。
三、TCP连接池:手动实现长连接复用
C#没有内置的TCP连接池,需要手动实现。TCP连接池适合长连接场景,比如游戏服务器、IM服务器、数据库连接。下面是一个简单的TCP连接池实现:
-
C#实战:实现TCP连接池
csharp
using System;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace TcpConnectionPool;
/// <summary>
/// TCP连接池
/// </summary>
public class TcpConnectionPool : IDisposable
{
private readonly ConcurrentQueue<TcpClient> _pool = new(); // 连接池队列,线程安全
private readonly string _serverIp;
private readonly int _serverPort;
private readonly int _maxConnections; // 最大连接数
private readonly int _connectionTimeout; // 连接超时时间(毫秒)
private int _currentConnections; // 当前连接数,用Interlocked保证线程安全
private bool _disposed; // 是否已释放
/// <summary>
/// 构造函数
/// </summary>
public TcpConnectionPool(string serverIp, int serverPort, int maxConnections = 100, int connectionTimeout = 5000)
{
_serverIp = serverIp;
_serverPort = serverPort;
_maxConnections = maxConnections;
_connectionTimeout = connectionTimeout;
// 预创建一半的连接,减少首次请求的等待时间
for (int i = 0; i < maxConnections / 2; i++)
{
CreateConnectionAsync().Wait();
}
}
/// <summary>
/// 从连接池获取连接
/// </summary>
public async Task<TcpClient> GetConnectionAsync(CancellationToken cancellationToken = default)
{
if (_disposed)
throw new ObjectDisposedException(nameof(TcpConnectionPool));
// 先从池中获取可用连接
while (_pool.TryDequeue(out var client))
{
// 检查连接是否可用(是否已断开)
if (client.Connected && !client.Client.Poll(0, SelectMode.SelectRead))
{
return client;
}
// 连接不可用,销毁
client.Dispose();
Interlocked.Decrement(ref _currentConnections);
}
// 池中没有可用连接,创建新连接(不超过最大连接数)
if (Interlocked.Increment(ref _currentConnections) > _maxConnections)
{
Interlocked.Decrement(ref _currentConnections);
throw new InvalidOperationException("连接池已满,无法创建新连接");
}
try
{
return await CreateConnectionAsync(cancellationToken);
}
catch
{
Interlocked.Decrement(ref _currentConnections);
throw;
}
}
/// <summary>
/// 释放连接到连接池
/// </summary>
public void ReleaseConnection(TcpClient client)
{
if (_disposed || client == null || !client.Connected)
{
client?.Dispose();
return;
}
// 放回池中
_pool.Enqueue(client);
}
/// <summary>
/// 创建新的TCP连接
/// </summary>
private async Task<TcpClient> CreateConnectionAsync(CancellationToken cancellationToken = default)
{
var client = new TcpClient();
try
{
// 设置连接超时
using var timeoutToken = new CancellationTokenSource(_connectionTimeout);
using var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutToken.Token);
await client.ConnectAsync(_serverIp, _serverPort, linkedToken.Token);
// 禁用Nagle算法,减少延迟
client.NoDelay = true;
return client;
}
catch
{
client.Dispose();
throw;
}
}
/// <summary>
/// 回收超时连接(后台线程定期执行)
/// </summary>
public async Task CleanupExpiredConnectionsAsync()
{
while (!_disposed)
{
await Task.Delay(TimeSpan.FromMinutes(1)); // 每分钟检查一次
var count = _pool.Count;
for (int i = 0; i < count; i++)
{
if (_pool.TryDequeue(out var client))
{
// 检查连接是否超时(比如10分钟未使用)
var lastActivity = client.Client.GetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime);
if (lastActivity is int keepAliveTime && DateTime.Now - DateTime.FromFileTime(keepAliveTime) > TimeSpan.FromMinutes(10))
{
client.Dispose();
Interlocked.Decrement(ref _currentConnections);
}
else
{
_pool.Enqueue(client);
}
}
}
}
}
public void Dispose()
{
_disposed = true;
while (_pool.TryDequeue(out var client))
{
client.Dispose();
}
}
}
// 实战用法
class Program
{
static async Task Main(string[] args)
{
var pool = new TcpConnectionPool("127.0.0.1", 8080, maxConnections: 100);
// 启动后台回收超时连接
_ = pool.CleanupExpiredConnectionsAsync();
try
{
// 获取连接
var client = await pool.GetConnectionAsync();
try
{
// 发送数据
var stream = client.GetStream();
var data = System.Text.Encoding.UTF8.GetBytes("Hello TCP Connection Pool!");
await stream.WriteAsync(data);
// 接收数据
var buffer = new byte[1024];
var bytesRead = await stream.ReadAsync(buffer);
var response = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"服务器响应:{response}");
}
finally
{
// 释放连接到池
pool.ReleaseConnection(client);
}
}
catch (Exception ex)
{
Console.WriteLine($"请求失败:{ex.Message}");
}
finally
{
pool.Dispose();
}
}
}
-
代码逐行讲解
1.ConcurrentQueue:线程安全的队列,用来存储可用的TCP连接——因为连接池是多线程共享的,所以必须用线程安全的集合;
2.GetConnectionAsync:从池中获取连接——先尝试从队列中获取可用连接,检查连接是否可用(是否已断开),如果不可用则销毁;如果池中没有可用连接,创建新连接(不超过最大连接数);
3.ReleaseConnection:释放连接到池中——把连接放回队列,供下次使用;
4.CreateConnectionAsync:创建新的TCP连接——设置连接超时,禁用Nagle算法(减少延迟);
5.CleanupExpiredConnectionsAsync:后台回收超时连接——定期检查连接的最后活动时间,超过10分钟未使用的连接销毁,避免连接泄漏;
6.Dispose:释放连接池的所有资源——销毁所有连接,避免资源泄漏。 -
TCP连接池的常见坑
连接泄漏:如果忘记释放连接到池,会导致连接池耗尽,无法获取连接——必须用try-finally确保连接被释放;
连接状态检查:放回池中的连接可能已经断开,获取连接时必须检查连接状态——用client.Connected和client.Client.Poll检查;
最大连接数设置:最大连接数不能太大,否则会导致服务器的连接数过多,性能下降——一般设置为CPU核心数*2,或者根据服务器的最大连接数调整;
连接超时回收:长时间未使用的连接可能已经断开,必须定期回收——避免池中充满无效连接。
四、HTTP连接池:HttpClient内置的连接池
C#的HttpClient内置了HTTP连接池,适合短连接场景,比如调用REST API、下载文件。HttpClient的连接池是基于ServicePointManager实现的,默认每个域名最多同时打开2个连接,这是很多人踩坑的地方——如果并发超过2,会导致请求排队,性能瓶颈。 -
基础用法:用HttpClient复用连接
csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;
// 顶级语句
// 注意:HttpClient应该单例使用,不要每次创建新的
using var httpClient = new HttpClient();
// 并发发送100个请求
var tasks = new Task[100];
for (int i = 0; i < 100; i++)
{
tasks[i] = SendRequestAsync(httpClient, i);
}
await Task.WhenAll(tasks);
Console.WriteLine("所有请求完成");
async Task SendRequestAsync(HttpClient client, int index)
{
var response = await client.GetAsync("https://api.example.com/data");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"请求{index}完成");
}
代码逐行讲解:
HttpClient单例:HttpClient是线程安全的,应该单例使用——每次创建新的HttpClient会创建新的连接池,导致连接无法复用;
并发请求:用Task.WhenAll并发发送100个请求,HttpClient会自动复用连接池中的连接,避免频繁创建和销毁连接。
2. 进阶用法:调整HTTP连接池参数
默认每个域名最多同时打开2个连接,这会导致并发请求排队,性能瓶颈。可以通过ServicePointManager调整最大连接数:
csharp
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
// 调整每个域名的最大连接数为100
ServicePointManager.DefaultConnectionLimit = 100;
// 启用TCP keep-alive,保持连接复用
ServicePointManager.SetTcpKeepAlive(true, 30000, 10000);
using var httpClient = new HttpClient();
// 后续请求和之前一样...
代码逐行讲解:
DefaultConnectionLimit:设置每个域名的最大连接数——一般设置为100以上,根据并发量调整;
SetTcpKeepAlive:启用TCP keep-alive,保持连接复用——避免连接在空闲时被服务器关闭,提高连接复用率。
3. 高级用法:用HttpClientFactory管理HttpClient生命周期
.NET Core 2.1+提供了HttpClientFactory,可以更好地管理HttpClient的生命周期,避免连接泄漏,同时支持配置连接池参数:
csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// 顶级语句
var host = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddHttpClient("MyHttpClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com/");
client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
// 调整每个域名的最大连接数为100
MaxConnectionsPerServer = 100,
// 启用TCP keep-alive
UseTcpKeepAlive = true,
TcpKeepAliveTime = TimeSpan.FromSeconds(30),
TcpKeepAliveInterval = TimeSpan.FromSeconds(10)
});
})
.Build();
var httpClient = host.Services.GetRequiredService<IHttpClientFactory>().CreateClient("MyHttpClient");
// 后续请求和之前一样...
代码逐行讲解:
AddHttpClient:注册HttpClient,配置基础地址、超时时间;
ConfigurePrimaryHttpMessageHandler:配置HttpClientHandler的参数,包括最大连接数、TCP keep-alive;
IHttpClientFactory:创建HttpClient,HttpClientFactory会自动管理HttpClient的生命周期,避免连接泄漏。
五、基础知识拓展
-
连接池的优缺点
优点:减少连接创建和销毁的开销,提高性能;减少服务器的TIME_WAIT连接数,避免端口耗尽;
缺点:增加代码复杂度,需要管理连接池的状态;如果连接泄漏,会导致连接池耗尽,无法获取连接;
适用场景:适合频繁创建和销毁连接的场景,比如高并发的TCP服务器、HTTP API调用;不适合长连接场景,比如游戏服务器、IM服务器(长连接可以直接复用,不需要连接池)。 -
连接池的监控与调优
监控连接数:用Performance Monitor监控TCP连接数、HTTP连接数——计数器为“TCPv4Connections Established”、“.NET CLR NetworkingHttp Client Connections Current”;
监控连接复用率:用Wireshark查看TCP连接的复用情况——如果每个请求都创建新连接,说明连接复用率低;如果多个请求复用同一个连接,说明连接复用率高;
调优最大连接数:根据并发量调整最大连接数——一般设置为并发量的1.5倍,避免请求排队;
避免连接泄漏:用try-finally确保连接被释放到池,用HttpClientFactory管理HttpClient的生命周期,避免连接泄漏。 -
连接池的替代方案
长连接:适合游戏服务器、IM服务器——保持TCP连接长期打开,不需要连接池;
多路复用:HTTP/2支持多路复用,多个请求可以复用同一个TCP连接——性能比HTTP/1.1的连接池更高;
负载均衡:用负载均衡器分发请求到多个服务器,每个服务器的连接数减少,不需要太大的连接池。
六、总结:连接池是性能优化的“利器”
TCP连接池:适合长连接场景,需要手动实现——要注意连接泄漏、连接状态检查、超时回收;
HTTP连接池:HttpClient内置了连接池,适合短连接场景——要注意调整最大连接数,避免默认2个连接的瓶颈;
最佳实践:TCP连接池适合长连接场景,HTTP连接池适合短连接场景;要合理设置最大连接数,监控连接池状态,避免连接泄漏;用HttpClientFactory管理HttpClient的生命周期,避免连接泄漏。
下一节我们会学习网络编程的高级特性:HTTP/2、HTTP/3,让你的网络程序性能再上一个台阶。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49534.html










