You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TCP通信随机延迟排查:ACK包引发的延迟原因咨询

高响应TCP通信延迟问题排查

我正在开发一款要求两台直连以太网PC间实现高响应TCP通信的应用,使用以下代码对TCP Socket响应性进行基准测试:

TCP Server代码

class Program
{
    static void Main(string[] args)
    {
        string serverAddress = "192.168.23.88"; // Localhost
        int serverPort = 6012;

        TcpListener server = new TcpListener(IPAddress.Parse(serverAddress), serverPort);
        server.Server.NoDelay = true;
        server.Start();
        Console.WriteLine($"Server started {serverAddress}:{serverPort}...");

        while (true)
        {
            var client = server.AcceptTcpClient();
            client.NoDelay = true;
            Console.WriteLine("Client connected...");
            HandleClientAsync(client); // Fire and forget
        }
    }


    private static void HandleClientAsync(TcpClient client)
    {
        using (client)
        {
            var buffer = new byte[20];
            var stream = client.GetStream();

            var t1RP = Encoding.UTF8.GetBytes("<TEST,1>");
            var t2RP = Encoding.UTF8.GetBytes("<TEST,2>");
            var t3RP = Encoding.UTF8.GetBytes("<TEST,3>");

            List<string> consoleList = new List<string>();
            while (true)
            {
                int bytesRead;
                try
                {
                    bytesRead = stream.Read(buffer, 0, buffer.Length);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"Error reading from client: {e.Message}");
                    foreach (var a in consoleList)
                        Console.WriteLine(a);

                    break;
                }

                stream.Write(t1RP, 0, t1RP.Length);
                stream.Write(t2RP, 0, t2RP.Length);
                stream.Write(t3RP, 0, t3RP.Length);
            }
        }
    }
}

TCP Client代码

static async Task Main(string[] args)
{
    string serverAddress = "192.168.23.88"; // Localhost
    int serverPort = 6012;

    using (TcpClient client = new TcpClient())
    {
        try
        {
            int cycle = 5000;
            client.NoDelay = true;
            Console.WriteLine("Connecting to server...");
            await client.ConnectAsync(serverAddress, serverPort);
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Connected to server {serverAddress}:{serverPort}");

            Stopwatch time = new Stopwatch();
            Stopwatch elapsedTime = new Stopwatch();

            var stream = client.GetStream();
            int count = 0;
            {
                byte[] buffer = new byte[20];
                for (int i = 0; i < cycle; ++i)
                {
                    long responseTime = 0;
                    long grabTime = 0;
                    long resultTime = 0;
                    byte[] data = Encoding.UTF8.GetBytes("<SOV,1,4,>");
                    stream.Write(data, 0, data.Length);
                    _ = stream.FlushAsync();

                    elapsedTime.Restart();
                    
                    string combined = "";
                    while (true)
                    {
                        int bytesRead = stream.Read(buffer, 0, buffer.Length);
                        combined += Encoding.UTF8.GetString(buffer, 0, bytesRead);
                        if (combined.Contains("<TEST,1>"))
                        {
                            _ = stream.FlushAsync();
                            responseTime = elapsedTime.ElapsedMilliseconds;
                            combined = combined.Replace("<TEST,1>", "");
                            if (responseTime > 3)
                            {
                                Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} SOV Response Time {responseTime}");
                                responseTime = elapsedTime.ElapsedMilliseconds;
                                count++;
                            }
                            break;
                        }
                    }

                    while (true)
                    {
                        int bytesRead = stream.Read(buffer, 0, buffer.Length);
                        combined += Encoding.UTF8.GetString(buffer, 0, bytesRead);
                        if (combined.Contains("<TEST,2>"))
                        {
                            grabTime = elapsedTime.ElapsedMilliseconds - responseTime;
                            combined = combined.Replace("<TEST,2>", "");
                            if (grabTime > 3)
                            {
                                _ = stream.FlushAsync();
                                Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} SOV Grab Time {grabTime}");
                                grabTime = elapsedTime.ElapsedMilliseconds - responseTime;
                                count++;
                            }
                            break;
                        }
                    }

                    while (true)
                    {
                        int bytesRead = stream.Read(buffer, 0, buffer.Length);
                        combined += Encoding.UTF8.GetString(buffer, 0, bytesRead);
                        if (combined.Contains("<TEST,3>"))
                        {
                            resultTime = elapsedTime.ElapsedMilliseconds - responseTime - grabTime;
                            combined = combined.Replace("<TEST,3>", "");
                            if (resultTime > 10)
                            {
                                _ = stream.FlushAsync();
                                Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} SOV Result Time {resultTime}");
                                resultTime = elapsedTime.ElapsedMilliseconds - responseTime - grabTime;
                                count++;
                            }
                            break;
                        }
                    }

                    time.Restart();
                    while (time.ElapsedMilliseconds < 50)
                    {

                    }

                    if ((i % 50) == 0)
                        Console.WriteLine($"Complete {i} Cycle");
                }
            }
            Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Completed {cycle} Cycle, Exceed Count {count}");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        Console.ReadLine();
    }

    Console.WriteLine("Connection closed");
}

测试现象

  • 5000次循环中随机出现5ms-20ms的响应延迟,WireShark抓包显示Server侧TCP包立即发出,但Client侧存在延迟。

2024年12月10日更新

修改Client代码后,Server发送的3条消息可通过一次Socket.Read()接收,但抓包发现Server侧存在来自Client的[ACK]包引发延迟,该ACK包并非业务消息,咨询此现象原因及TCP配置是否存在遗漏。


问题分析与解决方案

1. Client侧ACK延迟的核心原因

TCP的**延迟ACK(Delayed ACK)**机制是直接诱因。默认情况下,Windows和Linux系统都会启用该机制:接收方收到数据后,不会立即发送ACK,而是等待10-20ms(系统不同略有差异),若这段时间内有要发送给对方的数据,就会把ACK附带在数据里一起发送,以此减少网络包数量。

你的场景中,Client发送业务请求后进入等待状态,这段时间内没有主动向Server发送数据,触发了延迟ACK,导致ACK包延迟发送。Server在未收到ACK时,TCP栈的滑动窗口或拥塞控制逻辑可能间接影响后续数据发送,最终表现为观测到的延迟。

2. 遗漏的TCP配置检查

你已开启NoDelay=true(禁用Nagle算法),但这仅解决了发送方的包合并问题,并未关闭接收方的延迟ACK,需额外配置:

关闭延迟ACK的代码实现

  • Windows平台:通过底层Socket的IOControl设置
    // 在Client和Server的TcpClient初始化后添加
    const int SIO_TCP_SET_ACK_DELAY = -1744830452;
    byte[] optionIn = BitConverter.GetBytes(0); // 0表示关闭延迟ACK
    client.Client.IOControl(SIO_TCP_SET_ACK_DELAY, optionIn, null);
    
  • Linux平台:使用TCP_QUICKACK套接字选项
    const int TCP_QUICKACK = 12;
    client.Client.SetSocketOption(SocketOptionLevel.Tcp, (SocketOptionName)TCP_QUICKACK, 1);
    

其他优化点

  • 调整接收缓冲区大小:确保缓冲区能容纳Server发送的3条消息,避免因缓冲区不足导致分段等待
    client.ReceiveBufferSize = 1024; // 根据实际消息总大小调整
    
  • 替换空循环等待:原代码中while (time.ElapsedMilliseconds < 50)是空循环,会占用CPU导致线程调度延迟,建议替换为异步等待
    await Task.Delay(50);
    
  • 优化消息读取逻辑:当前字符串匹配方式效率低,建议采用固定格式(比如前缀带消息长度),减少字符串拼接和匹配开销。

3. 验证方法

修改配置后重新抓包,检查Client的ACK包是否在收到Server数据后1ms内发送,同时观测业务延迟是否降至预期范围。

内容的提问来源于stack exchange,提问作者Casey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 16:20:22