发送 UDP 数据包的长时间延迟

Posted

技术标签:

【中文标题】发送 UDP 数据包的长时间延迟【英文标题】:Long delays in sending UDP packets 【发布时间】:2015-11-23 15:55:30 【问题描述】:

我有一个接收、处理和传输 UDP 数据包的应用程序。

如果接收和发送的端口号不同,一切正常。

如果端口号相同而 IP 地址不同,则通常可以正常工作,除非 IP 地址与运行应用程序的计算机位于同一子网中。在最后一种情况下,send_to 函数需要几秒钟才能完成,而不是通常的几毫秒。

Rx Port  Tx IP          Tx Port    Result

5001     Same           5002       OK  Delay ~ 0.001 secs
         subnet     

5001     Different      5001       OK  Delay ~ 0.001 secs
         subnet

5001     Same           5001       Fails  Delay > 2 secs
         subnet

这是一个演示问题的简短程序。

#include <ctime>
#include <iostream>
#include <string>
#include <boost/array.hpp>
#include <boost/asio.hpp>

using boost::asio::ip::udp;
using std::cout;
using std::endl;

int test( const std::string& output_IP)

    try
    
        unsigned short prev_seq_no;

        boost::asio::io_service io_service;

        // build the input socket

        /* This is connected to a UDP client that is running continuously
        sending messages that include an incrementing sequence number
        */

        const int input_port = 5001;
        udp::socket input_socket(io_service, udp::endpoint(udp::v4(), input_port ));

        // build the output socket

        const std::string output_Port = "5001";
        udp::resolver resolver(io_service);
        udp::resolver::query query(udp::v4(), output_IP, output_Port );
        udp::endpoint output_endpoint = *resolver.resolve(query);
        udp::socket output_socket( io_service );
        output_socket.open(udp::v4());

       // double output buffer size
       boost::asio::socket_base::send_buffer_size option( 8192 * 2 );
       output_socket.set_option(option);

        cout  << "TX to " << output_endpoint.address() << ":"  << output_endpoint.port() << endl;



        int count = 0;
        for (;;)
        
            // receive packet
            unsigned short recv_buf[ 20000 ];
            udp::endpoint remote_endpoint;
            boost::system::error_code error;
            int bytes_received = input_socket.receive_from(boost::asio::buffer(recv_buf,20000),
                                 remote_endpoint, 0, error);

            if (error && error != boost::asio::error::message_size)
                throw boost::system::system_error(error);

            // start timer
            __int64 TimeStart;
            QueryPerformanceCounter( (LARGE_INTEGER *)&TimeStart );

            // send onwards
            boost::system::error_code ignored_error;
            output_socket.send_to(
                boost::asio::buffer(recv_buf,bytes_received),
                output_endpoint, 0, ignored_error);

            // stop time and display tx time
            __int64 TimeEnd;
            QueryPerformanceCounter( (LARGE_INTEGER *)&TimeEnd );
            __int64 f;
            QueryPerformanceFrequency( (LARGE_INTEGER *)&f );
            cout << "Send time secs " << (double) ( TimeEnd - TimeStart ) / (double) f << endl;

            // stop after loops
            if( count++ > 10 )
                break;
        
    
    catch (std::exception& e)
    
        std::cerr << e.what() << std::endl;
    


int main(  )


    test( "193.168.1.200" );

    test( "192.168.1.200" );

    return 0;

该程序在地址为 192.168.1.101 的机器上运行时的输出

TX to 193.168.1.200:5001
Send time secs 0.0232749
Send time secs 0.00541566
Send time secs 0.00924535
Send time secs 0.00449014
Send time secs 0.00616714
Send time secs 0.0199299
Send time secs 0.00746081
Send time secs 0.000157972
Send time secs 0.000246775
Send time secs 0.00775578
Send time secs 0.00477618
Send time secs 0.0187321
TX to 192.168.1.200:5001
Send time secs 1.39485
Send time secs 3.00026
Send time secs 3.00104
Send time secs 0.00025927
Send time secs 3.00163
Send time secs 2.99895
Send time secs 6.64908e-005
Send time secs 2.99864
Send time secs 2.98798
Send time secs 3.00001
Send time secs 3.00124
Send time secs 9.86207e-005

为什么会这样?有什么办法可以减少延迟?

注意事项:

使用 code::blocks 构建,在各种风格的 Windows 下运行

数据包长 10000 字节

如果我将运行应用程序的计算机连接到第二个网络,问题就会消失。例如 WWLAN(蜂窝网络“火箭棒”)

据我所知,我们的情况是这样的:

这可行(不同的端口,相同的局域网):

这也有效(相同的端口,不同的 LAN):

这不起作用(相同的端口,相同的 LAN)

这似乎可行(相同的端口,相同的 LAN,双宿主 Module2 主机)

【问题讨论】:

我真的很难相信。我建议删除所有引用以使用原始 BSD 套接字来增强和构建相同的功能。 但是您应该首先检查所有可能性,对吗?在简单示例中检查普通 BSD。如果它行为不端,那么...我不知道是什么。与网络工程师交谈。如果没有(如我所料!),您可以深入研究 boost 实现。它可能坏了。例如 ACE。 对。这就是为什么我说网络堆栈是最后的罪魁祸首。先做普通的套接字,然后我们再谈。 ;) 也许 ARP 有问题。目标 IP 是否已连接/可访问?你能在 ARP 表中找到 IP(命令“arp -a”适用于所有操作系统)吗? 我观察到这种行为的大部分时间是因为网络配置。考虑使用iperf 独立测量网络,然后使用网络分析工具,例如wireshark 来更深入地了解给定节点上发生的情况。调试时,有时更改为 TCP 协议或不同的端口可能会显示网络特性(节流、整形等)。 【参考方案1】:

鉴于在 Windows 上观察到大型数据报的目标地址是与发送者在同一子网内不存在的对等方,问题可能是 send() 阻塞等待 Address Resolution Protocol (ARP) 响应的结果,所以可以填充第 2 层以太网帧:

发送数据时,第 2 层以太网帧将填充路由中下一跳的媒体访问控制 (MAC) 地址。如果发送方不知道下一跳的 MAC 地址,它将广播 ARP 请求并缓存响应。使用发送方的子网掩码和目标地址,发送方可以确定下一跳是否与发送方位于同一子网上,或者数据是否必须通过默认网关进行路由。根据问题中的结果,发送大数据报时:

发往不同子网的数据报没有延迟,因为默认网关的 MAC 地址在发送方的 ARP 缓存中 发往发件人子网上不存在的对等方的数据报会导致等待 ARP 解析的延迟

套接字的send buffer size (SO_SNDBUF) 被设置为16384 字节,但正在发送的数据报大小为10000。未指定send() 在缓冲区饱和时的行为行为,但某些系统会观察到send() 阻塞。在这种情况下,如果任何数据报发生延迟(例如等待 ARP 响应),饱和将很快发生。

// Datagrams being sent are 10000 bytes, but the socket buffer is 16384.
boost::asio::socket_base::send_buffer_size option(8192 * 2);
output_socket.set_option(option);

考虑让内核管理套接字缓冲区大小或根据您的预期吞吐量增加它。

当发送一个大小超过 Window 注册表 FastSendDatagramThreshold‌ 参数的数据报时,send() 调用会阻塞,直到数据报发送完毕。更多详情请见Microsoft TCP/IP Implementation Details:

小于此参数值的数据报通过快速 I/O 路径或在发送时被缓冲。在实际发送数据报之前,将保留较大的。通过测试发现默认值是性能的最佳整体值。快速 I/O 意味着复制数据并绕过 I/O 子系统,而不是映射内存并通过 I/O 子系统。这对于少量数据是有利的。 一般不建议更改此值

如果观察到每个send() 到发送者子网上的现有对等点的延迟,则分析和分析网络:

使用iperf 衡量网络潜在吞吐量 使用wireshark 可以更深入地了解给定节点上发生的情况。查找 ARP 请求和响应。 从发件人的机器上,ping 对方,然后检查 APR 缓存。验证对等方是否存在缓存条目并且它是正确的。 尝试不同的端口和/或 TCP。这有助于确定网络策略是否在限制或调整特定端口或协议的流量。

另请注意,在等待 ARP 解析时快速连续发送低于 FastSendDatagramThreshold 值的数据报可能会导致数据报被丢弃:

ARP 仅对指定目标地址的一个出站 IP 数据报进行排队,同时将该 IP 地址解析为媒体访问控制地址。如果基于用户数据报协议 (UDP) 的应用程序将多个 IP 数据报发送到单个目标地址而在它们之间没有任何暂停,则如果不存在 ARP 缓存条目,则可能会丢弃一些数据报。应用程序可以通过在发送数据包流之前调用 iphlpapi.dll 例程 SendArp() 来建立 ARP 缓存条目来弥补这一点。

【讨论】:

我将 HKLM\System\CurrentControlSet\Services\Afd\Parameters\FastSendDatagramThreshold‌​​ 添加到我的注册表中,值为 2048(十进制),重新启动并再次尝试。没有变化。 @ravenspoint 如上所述,一般不建议更改FastSendDatagramThreshold‌‌。我强烈建议解决问题,而不是症状。如果您愿意接受更改 FastSendDatagramThreshold‌ 所涉及的所有风险,请查阅您使用的 Windows 版本的 TCP/IP 实施详细信息以确定 AFD 注册表参数的位置。 “我强烈建议解决这个问题” 是的,这就是我过去五天一直在尝试做的事情。我了解您的建议是更改此值。你推荐的是什么? "ARP 用于将网络地址(例如 IPv4 地址)映射到物理地址,如以太网地址(也称为 MAC 地址)。在我看来,ARP 与端口号无关,它应该关心的只是 IP 地址。在我的情况下 ipa:5001, ipb:5001 失败但 ipa:5001,ipb:5002 成功。当我更改端口号时,为什么 ARP 会做出不同的响应? @ravenspoint 我的建议是使用网络分析工具进一步调查并确定问题。在没有明确确定根本问题的情况下更改更高级别的变量只会引入噪音。【参考方案2】:

好的,整理一些代码(如下)。很明显,大多数时间发送时间不到一毫秒。这证明问题出在提升上。

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdexcept>
#include <poll.h>
#include <string>
#include <memory.h>
#include <chrono>
#include <stdio.h>

void test( const std::string& remote, const std::string& hello_string, bool first)

    try
    
        const short unsigned input_port = htons(5001);
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock == -1) 
            perror("Socket creation error: ");
            throw std::runtime_error("Could not create socket!");
        

        sockaddr_in local_addr;
        local_addr.sin_port = input_port;
        local_addr.sin_addr.s_addr = INADDR_ANY;
        if (bind(sock, (const sockaddr*)&local_addr, sizeof(local_addr))) 
            perror("Error: ");
            throw std::runtime_error("Can't bind to port!");
        

        sockaddr_in remote_addr;
        remote_addr.sin_port = input_port;
        if (!inet_aton(remote.c_str(), &remote_addr.sin_addr))
            throw std::runtime_error("Can't parse remote IP address!");

        std::cout  << "TX to " << remote << "\n";

        unsigned char recv_buf[40000];

        if (first) 
            std::cout << "First launched, waiting for hello.\n";
            int bytes = recv(sock, &recv_buf, sizeof(recv_buf), 0);
            std::cout << "Seen hello from my friend here: " << recv_buf << ".\n";
        

        int count = 0;
        for (;;)
        

            std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
            if (sendto(sock, hello_string.c_str(), hello_string.size() + 1, 0, (const sockaddr*)&remote_addr, sizeof(remote_addr)) != hello_string.size() + 1) 
                perror("Sendto error: ");
                throw std::runtime_error("Error sending data");
            
            std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();

            std::cout << "Send time nanosecs " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << "\n";

            int bytes = recv(sock, &recv_buf, sizeof(recv_buf), 0);
            std::cout << "Seen hello from my friend here: " << recv_buf << ".\n";

            // stop after loops
            if (count++ > 10)
                break;
        
    
    catch (std::exception& e)
    
        std::cerr << e.what() << std::endl;
    


int main(int argc, char* argv[])

    test(argv[1], argv[2], *argv[3] == 'f');

    return 0;

正如预期的那样,没有延迟。这是其中一对的输出(我在同一网络中的两台机器上成对运行代码):

./socktest x.x.x.x 'ThingTwo' f
TX to x.x.x.x
First launched, waiting for hello.
Seen hello from my friend here: ThingOne.
Send time nanosecs 17726
Seen hello from my friend here: ThingOne.
Send time nanosecs 6479
Seen hello from my friend here: ThingOne.
Send time nanosecs 6362
Seen hello from my friend here: ThingOne.
Send time nanosecs 6048
Seen hello from my friend here: ThingOne.
Send time nanosecs 6246
Seen hello from my friend here: ThingOne.
Send time nanosecs 5691
Seen hello from my friend here: ThingOne.
Send time nanosecs 5665
Seen hello from my friend here: ThingOne.
Send time nanosecs 5930
Seen hello from my friend here: ThingOne.
Send time nanosecs 6082
Seen hello from my friend here: ThingOne.
Send time nanosecs 5493
Seen hello from my friend here: ThingOne.
Send time nanosecs 5893
Seen hello from my friend here: ThingOne.
Send time nanosecs 5597

【讨论】:

不幸的是,这不能在 windows 上的 code::blocks 下编译。我尝试用 winsock.h 替换 sockets.h 但现在我都陷入了 POSIX 问题 @ravenspoint,这应该可以在 Windows 上运行,前提是你初始化了 winsock。 AFAIR,所有这些功能也是为 Windows 定义的吗?如果没有,只需将它们替换为 WSA 对应项。 您不需要任何系统标头,只需将它们全部删除即可。 对我来说没问题(20k 数据包):paste.ubuntu.com/13479999(与this code 相同)。房间里的大象是:地址是什么。您是否跟踪了两个 IP 的路由? @sehe 我怀疑原始代码中设置的小套接字发送缓冲区(SO_SNDBUF)设置为正在发送的数据报大小的 1.6 倍。使用 Linux 上的 Asio 和 BSD 套接字,我可以通过将 UDP 消息发送到同一子网上的地址但目的地没有机器的 UDP 消息使套接字发送缓冲区饱和来重现 3s~ 延迟。分析网络堆栈显示消息正在等待 ARP 超时。【参考方案3】:

最好将 Tx 和 Rx 端口分开。我从 CAsynchSocket 派生了我自己的套接字类,因为它有一个消息泵,当你的套接字上接收到数据时发送系统消息并拉出 OnReceive 函数(如果你覆盖底层虚函数,则为你的,如果不覆盖则为默认函数

【讨论】:

感谢您的回复。我相信 CAsynchSocket 是 MFC 的一部分。这个问题是关于 boost::asio 的。记得检查问题标签。

以上是关于发送 UDP 数据包的长时间延迟的主要内容,如果未能解决你的问题,请参考以下文章

使用UDP和TCP测量网络延迟

哪个是按顺序发送大型 UDP 数据包的最佳方法

无法接收使用 GCDAsyncSocket 发送的 UDP 数据包的响应

如果我在 C# 中发送 0 有效载荷数据,udp 数据包的大小是多少?

获取接收到的UDP数据包的目的地址

PING命令就是向主机发UDP数据包,但是啥端口