VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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连接池实现:

  1. 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. 代码逐行讲解
    1.ConcurrentQueue:线程安全的队列,用来存储可用的TCP连接——因为连接池是多线程共享的,所以必须用线程安全的集合;
    2.GetConnectionAsync:从池中获取连接——先尝试从队列中获取可用连接,检查连接是否可用(是否已断开),如果不可用则销毁;如果池中没有可用连接,创建新连接(不超过最大连接数);
    3.ReleaseConnection:释放连接到池中——把连接放回队列,供下次使用;
    4.CreateConnectionAsync:创建新的TCP连接——设置连接超时,禁用Nagle算法(减少延迟);
    5.CleanupExpiredConnectionsAsync:后台回收超时连接——定期检查连接的最后活动时间,超过10分钟未使用的连接销毁,避免连接泄漏;
    6.Dispose:释放连接池的所有资源——销毁所有连接,避免资源泄漏。
  2. TCP连接池的常见坑
    连接泄漏:如果忘记释放连接到池,会导致连接池耗尽,无法获取连接——必须用try-finally确保连接被释放;
    连接状态检查:放回池中的连接可能已经断开,获取连接时必须检查连接状态——用client.Connected和client.Client.Poll检查;
    最大连接数设置:最大连接数不能太大,否则会导致服务器的连接数过多,性能下降——一般设置为CPU核心数*2,或者根据服务器的最大连接数调整;
    连接超时回收:长时间未使用的连接可能已经断开,必须定期回收——避免池中充满无效连接。
    四、HTTP连接池:HttpClient内置的连接池
    C#的HttpClient内置了HTTP连接池,适合短连接场景,比如调用REST API、下载文件。HttpClient的连接池是基于ServicePointManager实现的,默认每个域名最多同时打开2个连接,这是很多人踩坑的地方——如果并发超过2,会导致请求排队,性能瓶颈。
  3. 基础用法:用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的生命周期,避免连接泄漏。
五、基础知识拓展

  1. 连接池的优缺点
    优点:减少连接创建和销毁的开销,提高性能;减少服务器的TIME_WAIT连接数,避免端口耗尽;
    缺点:增加代码复杂度,需要管理连接池的状态;如果连接泄漏,会导致连接池耗尽,无法获取连接;
    适用场景:适合频繁创建和销毁连接的场景,比如高并发的TCP服务器、HTTP API调用;不适合长连接场景,比如游戏服务器、IM服务器(长连接可以直接复用,不需要连接池)。
  2. 连接池的监控与调优
    监控连接数:用Performance Monitor监控TCP连接数、HTTP连接数——计数器为“TCPv4Connections Established”、“.NET CLR NetworkingHttp Client Connections Current”;
    监控连接复用率:用Wireshark查看TCP连接的复用情况——如果每个请求都创建新连接,说明连接复用率低;如果多个请求复用同一个连接,说明连接复用率高;
    调优最大连接数:根据并发量调整最大连接数——一般设置为并发量的1.5倍,避免请求排队;
    避免连接泄漏:用try-finally确保连接被释放到池,用HttpClientFactory管理HttpClient的生命周期,避免连接泄漏。
  3. 连接池的替代方案
    长连接:适合游戏服务器、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


相关教程