VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之连接池与复用(TCP连接池、HTTP连接池)

第14章 连接池与复用
14.1 连接池与复用(TCP连接池、HTTP连接池)
一、为什么要搞连接池?我踩过的“握手坑”
刚写C#爬虫时,每次爬网页都新建一个HttpClient,结果爬了1000个网页后,程序报“端口耗尽”——后来才发现,每次新建HttpClient都会新建一个TCP连接,TCP连接关闭后会进入TIME_WAIT状态(默认2分钟),1000个连接就要占用1000个端口,端口耗尽就无法新建连接了。这节我把自己从“每次新建连接”到“连接池复用”的经验揉进去,用大白话讲透TCP连接池、HTTP连接池的原理,结合C#代码逐行讲解,让你的程序能轻松爬10万+网页,还不报错。
二、先搞懂TCP连接的“握手开销”
TCP连接的建立需要三次握手,关闭需要四次挥手——每次新建连接都要花10-100ms的握手时间,关闭后还要等2分钟的TIME_WAIT状态,这是巨大的性能开销。
类比:TCP连接就像你去银行办事——每次去都要重vb.net教程C#教程python教程SQL教程access 2010教程新取号、排队(三次握手),办完事后还要等2分钟才能离开(TIME_WAIT),如果每天去1000次,光排队就要花1000*10ms=100秒,这太浪费时间了。如果办一张VIP卡(连接池),每次去直接用VIP通道(复用连接),不用重新排队,效率能提高10倍以上。
三、TCP连接池:复用TCP连接,减少握手开销
TCP连接池是复用TCP连接的技术——程序先创建一批TCP连接,放到连接池里,每次需要连接时从池里取一个,用完后放回池里,不需要新建连接。
核心原理:
1.初始化连接池:程序启动时,创建一批TCP连接,放到连接池里;
2.获取连接:需要连接时,从连接池里取一个可用的连接;
3.使用连接:用连接发送数据、接收数据;
4.归还连接:用完后把连接放回连接池,供下次使用。

  1. C#实战:手动实现TCP连接池
    csharp
	using System;
	using System.Collections.Generic;
	using System.Net.Sockets;
	using System.Threading;
	
	namespace TcpConnectionPool;
	
	// TCP连接池
	public class TcpConnectionPool
	{
	private readonly string _serverIp;
	private readonly int _serverPort;
	private readonly int _maxConnections; // 最大连接数
	private readonly Stack<TcpClient> _pool; // 连接池
	private readonly object _lockObj = new object(); // 锁,保证线程安全
	
	public TcpConnectionPool(string serverIp, int serverPort, int maxConnections)
	{
	_serverIp = serverIp;
	_serverPort = serverPort;
	_maxConnections = maxConnections;
	_pool = new Stack<TcpClient>(maxConnections);
	
	// 初始化连接池:创建maxConnections个连接
	for (int i = 0; i < maxConnections; i++)
	{
	var client = CreateConnection();
	_pool.Push(client);
	}
	Console.WriteLine($"TCP连接池已初始化,最大连接数:{maxConnections}");
	}
	
	// 创建新的TCP连接
	private TcpClient CreateConnection()
	{
	var client = new TcpClient();
	client.Connect(_serverIp, _serverPort);
	Console.WriteLine($"新建TCP连接:{client.Client.RemoteEndPoint}");
	return client;
	}
	
	// 从连接池获取连接
	public TcpClient GetConnection()
	{
	lock (_lockObj)
	{
	// 如果池里有可用连接,直接返回
	if (_pool.Count > 0)
	{
	var client = _pool.Pop();
	// 检查连接是否可用,如果不可用,新建一个
	if (!client.Connected)
	{
	Console.WriteLine("连接已断开,新建连接");
	client.Dispose();
	return CreateConnection();
	}
	return client;
	}
	// 如果池里没有可用连接,等待一段时间再尝试
	else
	{
	Console.WriteLine("连接池已满,等待可用连接");
	Thread.Sleep(100);
	return GetConnection(); // 递归尝试,直到获取到连接
	}
	}
	}
	
	// 归还连接到连接池
	public void ReturnConnection(TcpClient client)
	{
	lock (_lockObj)
	{
	// 如果连接池未满,归还连接
	if (_pool.Count < _maxConnections)
	{
	if (client.Connected)
	{
	_pool.Push(client);
	Console.WriteLine($"连接已归还,当前可用连接数:{_pool.Count}");
	}
	else
	{
	Console.WriteLine("连接已断开,不归还");
	client.Dispose();
	}
	}
	// 如果连接池已满,关闭连接
	else
	{
	Console.WriteLine("连接池已满,关闭连接");
	client.Dispose();
	}
	}
	}
	
	// 释放连接池
	public void Dispose()
	{
	lock (_lockObj)
	{
	foreach (var client in _pool)
	{
	client.Dispose();
	}
	_pool.Clear();
	}
	}
	}
	
	// 测试TCP连接池
	class Program
	{
	static void Main(string[] args)
	{
	// 初始化TCP连接池:连接到localhost:8080,最大连接数10
	var pool = new TcpConnectionPool("localhost", 8080, 10);
	
	// 模拟100个线程同时获取连接
	for (int i = 0; i < 100; i++)
	{
	var thread = new Thread(UseConnection);
	thread.Start(pool);
	}
	
	Console.ReadLine();
	pool.Dispose();
	}
	
	static void UseConnection(object obj)
	{
	var pool = (TcpConnectionPool)obj;
	var client = pool.GetConnection();
	
	// 模拟使用连接:发送数据
	var stream = client.GetStream();
	var buffer = System.Text.Encoding.UTF8.GetBytes("Hello Server");
	stream.Write(buffer, 0, buffer.Length);
	
	// 模拟处理数据:接收响应
	buffer = new byte[1024];
	int bytesRead = stream.Read(buffer, 0, buffer.Length);
	string response = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
	Console.WriteLine($"收到响应:{response}");
	
	// 归还连接
	pool.ReturnConnection(client);
	}
	}

代码逐行讲:
1.TcpConnectionPool类:TCP连接池的核心,负责初始化连接、获取连接、归还连接、释放连接;
2.CreateConnection方法:创建新的TCP连接,调用TcpClient.Connect连接到服务器;
3.GetConnection方法:从连接池获取连接,如果池里有可用连接直接返回,没有的话等待一段时间再尝试;
4.ReturnConnection方法:归还连接到连接池,如果连接池未满且连接可用,就放回池里,否则关闭连接;
5.线程安全:用lock保证连接池的线程安全,避免多个线程同时操作连接池导致的错误。
性能对比:用连接池爬10万网页,只需要10个TCP连接,耗时10分钟;不用连接池爬10万网页,需要10万个TCP连接,耗时2小时,还会报“端口耗尽”。
三、HTTP连接池:复用HTTP连接,减少握手开销
HTTP连接池是复用HTTP连接的技术——HTTP/1.1默认支持持久连接(Keep-Alive),可以在一个TCP连接上发送多个HTTP请求,HTTP连接池就是基于持久连接实现的。
核心原理:
1.初始化连接池:程序启动时,创建一批HTTP连接,放到连接池里;
2.获取连接:需要发送HTTP请求时,从连接池里取一个可用的连接;
3.发送请求:用连接发送多个HTTP请求;
4.归还连接:用完后把连接放回连接池,供下次使用。

  1. C#实战:用HttpClientFactory实现HTTP连接池
    在C#中,HttpClientFactory是官方推荐的HTTP连接池实现——它会自动复用HTTP连接,管理连接池的大小,处理连接的超时、断开等问题。
    示例:用HttpClientFactory实现HTTP连接池
    csharp
	using System;
	using System.Net.Http;
	using System.Threading.Tasks;
	using Microsoft.Extensions.DependencyInjection;
	
	namespace HttpClientPoolDemo;
	
	class Program
	{
	static async Task Main(string[] args)
	{
	// 初始化服务容器,注册HttpClientFactory
	var services = new ServiceCollection();
	services.AddHttpClient("MyHttpClient", client =>
	{
	// 设置基础地址
	client.BaseAddress = new Uri("https://example.com/");
	// 设置超时时间
	client.Timeout = TimeSpan.FromSeconds(30);
	// 设置默认请求头
	client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
	})
	.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
	{
	// 设置最大连接数:每个服务器的最大连接数
	MaxConnectionsPerServer = 100,
	// 启用持久连接(默认启用)
	UseCookies = true,
	UseDefaultCredentials = false
	});
	
	// 构建服务提供器
	var serviceProvider = services.BuildServiceProvider();
	var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
	
	// 模拟发送1000个HTTP请求
	for (int i = 0; i < 1000; i++)
	{
	await SendHttpRequest(httpClientFactory, i);
	}
	
	Console.WriteLine("所有请求已完成");
	}
	
	static async Task SendHttpRequest(IHttpClientFactory httpClientFactory, int index)
	{
	// 从连接池获取HttpClient
	var client = httpClientFactory.CreateClient("MyHttpClient");
	
	try
	{
	// 发送HTTP GET请求
	var response = await client.GetAsync("api/data");
	response.EnsureSuccessStatusCode(); // 确保请求成功
	
	// 读取响应内容
	var content = await response.Content.ReadAsStringAsync();
	Console.WriteLine($"请求{index}已完成,响应长度:{content.Length}");
	}
	catch (Exception ex)
	{
	Console.WriteLine($"请求{index}失败:{ex.Message}");
	}
	}
	}

代码逐行讲:
1.AddHttpClient方法:注册HttpClient,设置基础地址、超时时间、默认请求头;
2.ConfigurePrimaryHttpMessageHandler方法:配置HTTP消息处理器,设置MaxConnectionsPerServer(每个服务器的最大连接数),启用持久连接;
3.CreateClient方法:从连接池获取HttpClient,HttpClient会自动复用HTTP连接;
4.SendHttpRequest方法:用HttpClient发送HTTP请求,用完后不需要手动归还连接——HttpClientFactory会自动管理连接的复用和释放。
关键:HttpClientFactory会自动管理HTTP连接池,不需要手动创建和释放连接,还会处理连接的超时、断开等问题,这是官方推荐的做法。
2. 为什么不要每次新建HttpClient?
端口耗尽:每次新建HttpClient都会新建一个TCP连接,TCP连接关闭后会进入TIME_WAIT状态,端口耗尽就无法新建连接;
内存泄漏:HttpClient会创建一个底层的HttpMessageHandler,HttpMessageHandler会占用内存和端口,每次新建HttpClient会导致内存泄漏;
性能差:每次新建连接都要花握手时间,性能差。
正确做法:用HttpClientFactory或单例HttpClient——单例HttpClient也能复用连接,但无法处理连接的超时、断开等问题,HttpClientFactory更强大。
四、连接池的最佳实践
1.设置合理的连接池大小:连接池大小不是越大越好——连接数越多,上下文切换的开销越大,反而会降低性能。一般来说,IO密集型程序的连接池大小设置为CPU核心数的10-20倍就够了;
2.处理连接的超时、断开:连接池要定期检查连接的可用性,如果连接断开或超时,要及时替换;
3.设置连接的最大空闲时间:如果连接空闲时间过长,要关闭连接,释放资源;
4.线程安全:连接池必须是线程安全的,避免多个线程同时操作连接池导致的错误;
5.监控连接池:监控连接池的可用连接数、使用连接数、等待连接数,根据监控结果调优连接池大小。
五、基础知识拓展

  1. TCP的TIME_WAIT状态
    TCP连接关闭后,会进入TIME_WAIT状态(默认2分钟)——这是为了保证最后一个ACK包能到达对方,避免对方重发FIN包。
    如何减少TIME_WAIT的影响:
    复用连接:用连接池复用TCP连接,减少新建连接的次数;
    调整系统参数:比如Windows的TcpTimedWaitDelay(默认120秒),Linux的net.ipv4.tcp_tw_reuse(允许复用TIME_WAIT状态的连接)、net.ipv4.tcp_tw_recycle(快速回收TIME_WAIT状态的连接)。
    注意:调整系统参数要谨慎,可能会导致网络问题。
  2. HTTP/2的多路复用
    HTTP/2支持多路复用——在一个TCP连接上可以同时发送多个HTTP请求,不需要等待前一个请求完成,这比HTTP/1.1的持久连接更高效。
    C#实战:用HttpClientFactory支持HTTP/2
    csharp
	services.AddHttpClient("MyHttpClient", client =>
	{
	client.BaseAddress = new Uri("https://example.com/");
	client.DefaultRequestVersion = new Version(2, 0); // 设置HTTP/2
	client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
	})
	.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
	{
	EnableMultipleHttp2Connections = true,
	MaxConnectionsPerServer = 100
	});
  1. 连接池的监控工具
    Windows:Performance Monitor(性能监视器),可以监控TCP连接数、HTTP连接数;
    Linux:netstat -an | grep TIME_WAIT(查看TIME_WAIT状态的连接数)、ss -s(查看TCP连接统计);
    C#代码:用HttpClientFactory的监控API,监控连接池的使用情况。
    示例:监控HttpClientFactory的连接池
    csharp
	// 注册监控服务
	services.AddHttpClient("MyHttpClient")
	.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)))
	.AddHttpMessageHandler(() => new MonitoringHandler());
	
	// 自定义监控处理器
	public class MonitoringHandler : DelegatingHandler
	{
	protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
	{
	var startTime = DateTime.Now;
	var response = await base.SendAsync(request, cancellationToken);
	var duration = DateTime.Now - startTime;
	
	// 记录监控数据:请求时间、响应状态码等
	Console.WriteLine($"请求{request.RequestUri}耗时{duration.TotalMilliseconds}ms,状态码{response.StatusCode}");
	
	return response;
	}
	}

六、总结:连接池是性能优化的“利器”
TCP连接池:复用TCP连接,减少握手开销,避免端口耗尽;
HTTP连接池:复用HTTP连接,减少握手开销,提高性能;
C#实战:手动实现TCP连接池,用HttpClientFactory实现HTTP连接池;
最佳实践:设置合理的连接池大小,处理连接的超时、断开,监控连接池。

 本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49529.html


相关教程