【问题标题】:Fastest approach to using raw sockets in .NET在 .NET 中使用原始套接字的最快方法
【发布时间】:2021-04-17 23:17:57
【问题描述】:

我正在尝试提高基于 .NET 5 的网络代码的性能以提高吞吐量。我一直在使用标准的 .NET Sockets 实现——但是当涉及到大约 200Mbps(1kb 数据包)的吞吐量时,我遇到了障碍。我已经尝试了异步模式的两种变体(Begin... 和 SendToAsync...),并使用了使用同步操作的专用线程 - 但无法进一步提高性能。发送方和接收方的整体 CPU 负载不到 50% - 机器以 1Gbps 的速度连接。

我还尝试使用 Pcap.Net 甚至是 Windows 数据包过滤器驱动程序 (http://ntkernel.com) 来插入套接字层下方的 NDIS。

阅读表明注册 I/O 可能是一个好方法,但在我的情况下,我需要使用 SocketType.Raw(因为我需要处理 GRE、SCTP 和除 UDP 和 TCP 之外的各种其他协议)。注册 I/O API 似乎不支持原始(IP 级)套接字(请参阅https://docs.microsoft.com/en-us/windows/win32/api/mswsock/nc-mswsock-lpfn_riocreaterequestqueue

分析器建议将时间花在 WSASendTo 中 - 并且 .NET Socket 类几乎没有引入额外的开销。这表明即使直接使用 Winsock 从 C# 迁移到 C++ 也不太可能有太大帮助。我宁愿避开内核开发。

有什么类似于注册 I/O 的东西可以与原始套接字一起使用吗?注册 I/O 应该支持 UDP 但不直接支持 IP 似乎很奇怪。还有什么其他想法可以解决这个瓶颈吗?与原始套接字(相对于 UDP 和 TCP)相关的 Winsock 是否存在任何固有的低效率?

根据要求 - 添加了一个简单示例,以尽可能快地发送 1Kb 虚拟 GRE 数据包。这导致大约 270Mbps 的吞吐量和大约 30% 的 CPU 均匀分布在所有内核上。使用 localhost 的性能大致相同 - 我认为排除网卡是瓶颈。

using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;

namespace RawSender
{
    class Program
    {
        private static readonly ConcurrentBag<SocketAsyncEventArgs> ArgsPool = new ConcurrentBag<SocketAsyncEventArgs>();
        static void Main(string[] args)
        {
            var Destination = IPAddress.Parse(args[0]);

            // Create pool of SocketAsyncEventArgs for the async operations

            for (int Index=0;Index<10_000;Index++)
            {
                var Args = new SocketAsyncEventArgs();
                Args.SetBuffer(new byte[1000]);
                Args.Completed += OnComplete;
                Args.RemoteEndPoint = new IPEndPoint(Destination, 0);
                ArgsPool.Add(Args);
            }

            // Send 1 million dummy 1kb packets using IP protocol 47 (GRE)
            
            var Socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, (ProtocolType)47);
            for(int Scan=0;Scan<1_000_000;Scan++)
            {
                if (!ArgsPool.TryTake(out SocketAsyncEventArgs Args)) throw new Exception("Args pool empty");
                if (!Socket.SendToAsync(Args))
                {
                    OnComplete(null, Args);
                }
            }
        }

        private static void OnComplete(object sender, SocketAsyncEventArgs e)
        {
            ArgsPool.Add(e);
        }
    }
}

【问题讨论】:

  • 早在 RIO 出现之前,人们就已经在编写高性能套接字代码了。使用 RIO 只是通过减少在用户模式和内核模式之间转换的一些开销来提高效率。
  • 感谢您的回复。不确定我可以提供什么示例 - 因为我在询问类似于 RIO 的支持原始套接字的 API 的可用性。但是,我添加了一个非常简单的示例,该示例演示了使用标准异步套接字的有限吞吐量。
  • TPL DataFlow 在这里有用吗?
  • 我已经在平台内使用了 TPL,但除了通过标准 Sockets 实现之外,我不知道有任何方法可以发送原始数据包 - 这不能提供所需的性能。跨度>

标签: c# .net sockets winsock


【解决方案1】:

玩弄您的代码,您似乎受到发送的数据包开销数量的限制。使用您编写的代码,我将获得 246MB/s 的吞吐量,当我将缓冲区大小增加到 10000(即比您的大 10 倍)时,我将获得 2346MB/s(即再次大 10 倍)。

就像任何其他类型的套接字编程一样,更好地将您的套接字消息组合在一起,不要说如果每秒发送 数百万个这样的东西就不能。 p>

【讨论】:

  • 不幸的是,我受限于 (i) 网络路径的 MTU(1500 字节,包括标头),以及 (ii) 正在使用的协议规范 - 所以我不能将字节捆绑到任意大的数据包。正如您所说,我怀疑问题在于每个数据包的处理开销(可能是用户/内核转换) - 这就是为什么我对注册 I/O 感兴趣的原因,它确实支持每次发送数百万个数据包(请参阅video.ch9.ms/build/2011/slides/SAC-593T_Briggs.pptx)但是不是原始套接字。
  • 不确定我是否理解第二点。您是说不能增加缓冲区大小并将多个应用层消息放入同一个缓冲区?
  • 这些是原始 IP 数据包(不是 UDP 或 TCP)。如果我创建一个大缓冲区(并且不设置 DF 标志),那么消息将被分段并且帧将丢失。如果我确实设置了 DF 标志,那么如果我提供了超过 MTU 值的数据,它将失败并出现 MessageSize 错误。
  • @MikeBrom,取决于您的网络设置,特别是如果您控制沿途的所有路由器,则可以增加 MTU。 1500 MTU 不是最大值,它只是一个旧值,现在“足够好”了。
  • 不幸的是,所涉及的网络是 Internet - 我更改 MTU 的机会不大
猜你喜欢
  • 1970-01-01
  • 2021-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-10
相关资源
最近更新 更多