.NET中并行发送Ping请求速度极慢的原因及优化方案
解决批量Ping/TCP连接请求提交缓慢的问题
问题根源
不管是Ping.SendPingAsync还是TcpClient.ConnectAsync,底层都会为每个请求初始化套接字资源,即使预创建实例,每个请求的同步初始化步骤(比如套接字绑定、协议初始化)的开销累积起来,在批量处理100个节点时就会导致提交耗时剧增。
针对Ping的优化方案
1. 用原始套接字批量发送ICMP请求(需管理员权限)
绕过Ping类的封装,直接使用Socket发送ICMP包,能大幅减少每个请求的初始化开销。ICMP包需手动构造,但可以批量发起请求:
// 示例:批量构造ICMP请求并发送 using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp); socket.ReceiveTimeout = 100; socket.SendTimeout = 100; var tasks = new List<Task<(IPAddress Address, long Delay)>>(); foreach (var addr in targetAddresses) { var buffer = CreateIcmpBuffer(); // 自行实现ICMP echo请求包构造逻辑 var task = Task.Run(async () => { var stopwatch = Stopwatch.StartNew(); try { await socket.SendToAsync(new ArraySegment<byte>(buffer), SocketFlags.None, new IPEndPoint(addr, 0)); var recvBuffer = new byte[1024]; var recvResult = await socket.ReceiveFromAsync(new ArraySegment<byte>(recvBuffer), SocketFlags.None, new IPEndPoint(IPAddress.Any, 0)); stopwatch.Stop(); if (IsValidIcmpResponse(recvBuffer, recvResult.ReceivedBytes)) // 自行实现响应包验证逻辑 { return (addr, stopwatch.ElapsedMilliseconds); } } catch { // 处理超时或错误 } stopwatch.Stop(); return (addr, long.MaxValue); // 标记节点不可达 }); tasks.Add(task); } // 等待结果并筛选延迟最低的前5个 var results = await Task.WhenAll(tasks); var top5Nodes = results.Where(r => r.Delay != long.MaxValue).OrderBy(r => r.Delay).Take(5).ToList();
注意:使用原始套接字需要管理员权限,Windows和Linux的权限配置规则不同,需提前确认。
2. 调整系统ICMP速率限制(可选)
部分系统默认限制ICMP请求的发送速率,比如Windows可通过修改注册表调整:
- 注册表路径:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters - 添加
DWORD值ICMPRateLimit,设为0表示无限制(需重启系统生效)
针对TCP连接的优化方案
1. 用SocketAsyncEventArgs池批量发起连接
SocketAsyncEventArgs可复用资源,避免每个连接请求重复初始化套接字相关对象,大幅降低提交开销:
// 初始化SocketAsyncEventArgs对象池(需引用Microsoft.Extensions.ObjectPool) var argsPool = new DefaultObjectPool<SocketAsyncEventArgs>(new DefaultPooledPolicy<SocketAsyncEventArgs> { Create = () => { var args = new SocketAsyncEventArgs(); args.Completed += (s, e) => { var tcs = e.UserToken as TaskCompletionSource<(IPAddress Address, long Delay)>; var stopwatch = e.Properties["Stopwatch"] as Stopwatch; stopwatch.Stop(); if (e.SocketError == SocketError.Success) { tcs.SetResult(((IPEndPoint)e.RemoteEndPoint).Address, stopwatch.ElapsedMilliseconds); } else { tcs.SetResult(((IPEndPoint)e.RemoteEndPoint).Address, long.MaxValue); } // 将对象归还到池 argsPool.Return(e); }; return args; } }); var tasks = new List<Task<(IPAddress Address, long Delay)>>(); foreach (var addr in targetAddresses) { var args = argsPool.Get(); var tcs = new TaskCompletionSource<(IPAddress Address, long Delay)>(); args.UserToken = tcs; args.RemoteEndPoint = new IPEndPoint(addr, targetTcpPort); // 目标服务的TCP端口 var stopwatch = Stopwatch.StartNew(); args.Properties["Stopwatch"] = stopwatch; // 发起异步连接,若同步完成则直接触发Completed事件 if (!args.AcceptSocket.ConnectAsync(args)) { args.Completed?.Invoke(null, args); } tasks.Add(tcs.Task); } // 等待结果并筛选延迟最低的前5个 var results = await Task.WhenAll(tasks); var top5Nodes = results.Where(r => r.Delay != long.MaxValue).OrderBy(r => r.Delay).Take(5).ToList();
2. 直接使用Socket.ConnectAsync替代TcpClient
TcpClient的封装会带来额外开销,直接使用Socket的异步方法能减少初始化步骤,配合套接字资源池使用效果更优。
通用优化建议
- 设置合理超时:不管是Ping还是TCP连接,超时设为100-200ms即可,无需等待过久,避免无效占用资源。
- 控制并发数:系统套接字资源有限,并发数控制在50-100之间即可,过多并发反而会加剧资源竞争,拖慢提交速度。
- 预初始化资源:提前创建好套接字池、
SocketAsyncEventArgs池,避免在请求提交阶段才进行资源初始化。
内容的提问来源于stack exchange,提问作者LOST
相关产品推荐
相关产品推荐

