【问题标题】:How can you maximize the throughput on a single TCP socket?如何最大化单个 TCP 套接字的吞吐量?
【发布时间】:2013-01-03 13:26:51
【问题描述】:

我试图让标准示例“echo”客户端/服务器应用程序尽快运行,我确信网络是一个限制因素。我有一个 1 Gb 的网卡,当我使用资源监视器时,我只能从客户端获得 7 mb。

我了解套接字、消息帧和长度指示器的基础知识,接收长度指示器指示的所有字节。保持活动数据包、半开连接等。

我开始使用标准套接字操作,然后切换到使用异步。 (我没有将发送更改为异步,因为有人[似乎知识渊博的人说它不应该有任何影响])我有完全相同的性能,我只能认为所有材料都假设我有一些其他工作可以在同一个线程上完成。但是在我的快速测试中,我有一个线程专门用于循环发送和另一个完全不同的线程来接收。

我已经尝试了所有方法,但完全不知道在哪里可以获得更高的性能。我使用了 IPerf,它报告的返回速度为每秒 1 吉比特,资源监视器也显示这会占用带宽。

即使有人可以为我指出一个更完整的例子。我遇到的大多数都是微不足道的或不完整的。

这是通用代码。

class Program
{
private static Socket sock;
private static BlockingCollection<string> queue;
private static int bytesReceived;
private static byte[] dataBuffer;
private static readonly byte[] lengthBuffer = new byte[4];

private static byte[] PrependLengthIndicator(byte[] data)
{
    return BitConverter.GetBytes(data.Length).Concat(data).ToArray();
}

private static void Receive()
{
    if (dataBuffer == null)
    {
        sock.BeginReceive(lengthBuffer, 0, 4, SocketFlags.None, ReceiveCallback, null);
    }
    else
    {
        sock.BeginReceive(dataBuffer, 0, bytesReceived, SocketFlags.None, ReceiveCallback, null);
    }
}

private static void ReceiveCallback(IAsyncResult ar)
{
    bytesReceived += sock.EndReceive(ar);
    if (dataBuffer == null)
    {
        // Currently receiving length indicator
        if (bytesReceived >= 4)
        {
            var length = BitConverter.ToInt32(lengthBuffer, 0);
            dataBuffer = new byte[length];
            bytesReceived = 0;
        }
    }
    else
    {
        if (bytesReceived == dataBuffer.Length)
        {
            // Finished reading
            var request = Encoding.ASCII.GetString(dataBuffer);
            dataBuffer = null;
            bytesReceived = 0;
            queue.Add(request);
        }
    }
    ContinueReading();
}

private static void ContinueReading()
{
    // Read into the appropriate buffer: length or data
    if (dataBuffer != null)
    {
        sock.BeginReceive(dataBuffer, bytesReceived, dataBuffer.Length - bytesReceived, SocketFlags.None, ReceiveCallback, null);
    }
    else
    {
        sock.BeginReceive(lengthBuffer, bytesReceived, lengthBuffer.Length - bytesReceived, SocketFlags.None, ReceiveCallback, null);
    }
}

}

这是服务器部分:

static void Main(string[] args)
{
        var listenSock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        listenSock.Bind(new IPEndPoint(IPAddress.Parse(ConfigurationManager.AppSettings["LocalIp"]), 3333));
        listenSock.Listen(10);
        Console.WriteLine("Server started...");
        sock = listenSock.Accept();
        Console.WriteLine("Connection accepted.");

        queue = new BlockingCollection<string>();
        Receive();
        var count = 0;
        var sender = new Thread(() =>
            {
                while (true)
                {
                    var bar = queue.Take() + "Resp";
                    count++;
                    var resp = Encoding.ASCII.GetBytes(bar);
                    var toSend = PrependLengthIndicator(resp);
                    if (count % 10000 == 0)
                    {
                        Console.WriteLine(bar);
                    }
                    sock.Send(toSend);
                }
            });
        sender.Start();
    }

这是客户端部分:

static void Main(string[] args)
{

    sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        Console.WriteLine("Connecting...");
        sock.Connect(IPAddress.Parse(ConfigurationManager.AppSettings["EndPointIp"]), 3333);
        Console.WriteLine("Connected.");
        Receive();

        var count = 0;
        while(true)
        {
            count++;
            var foo = "Echo-" + count;
            var data = Encoding.ASCII.GetBytes(foo);
            var toSend = PrependLengthIndicator(data);
            sock.Send(toSend);
        }
    }

【问题讨论】:

    标签: c# .net performance sockets


    【解决方案1】:

    您正在发送小消息。想想你需要多少百万个才能使 1 Gbit/sec 链路饱和。对套接字的每次调用都会消耗 CPU。

    发送更大的消息。另外,尽量不要一直分配新的缓冲区。不要使用Enumerable.Concat 连接字节缓冲区,因为这会以极其低效的方式逐字节操作。将Array.Copy预分配数组一起使用。

    如果您使用的线程很少,请切换到 同步 IO,因为它会更快(真的!它的开销更少)。

    您可以通过运行一个愚蠢的无限发送循环来确认这个答案是正确的,该循环只是一直同步发送 64KB 的缓冲区。它会使链接饱和。

    【讨论】:

    • 我使用的是 linq 和 concat,因为我认为网络肯定是瓶颈,而不是 CPU。当我运行测试时,CPU 非常低。我在帖子中确实提到过,我最初只是使用普通的同步 IO,其中一个专用于发送,一个用于接收。我将尝试制作 64K 的缓冲区并不断发送这些缓冲区。顺便说一句,在我的实际应用程序中,大多数请求将是大约 50 个字节,响应可能是 70-80 个字节。考虑到这一点,我如何才能最大限度地提高吞吐量。
    • @uriDium 对于小消息,您只能通过流水线处理它们(而不是等待响应)来提高吞吐量。在 1 毫秒的往返时间中,您每秒只能等待 1000 次。这让你无处可去。
    • 我发现了一些 WCF 实现的比较,他们每秒获得 40 K 1 路操作。每秒 25K 次双向操作。我的最大速度约为每秒 10K。这就是促使我进行整个调查的原因。你知道我怎样才能获得类似的性能吗?
    • 我上面的代码确实会等待响应。他们是管道内衬还是完全错过了什么?
    • @uriDium 1. 更彻底地查看您的代码,看起来您所说的应该是真的。流水线应该工作。 2. 你是通过物理链接发送的,对吗? (不是环回)。; 3. 客户端和服务器到底消耗多少CPU?它们是否使单个 CPU 内核饱和? 4. 您的客户端发送不受限制,它收到的响应堆积在队列中。也许这会产生“窒息”效应,因为 GC 运行过于频繁并且必须扫描一个巨大的堆。尽量不要写入客户端上的队列。 5.尝试将nagling设置设置为true或false,看看是否有区别。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-07
    • 1970-01-01
    • 2011-11-24
    • 2016-08-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多