【问题标题】:Good UDP relay server design良好的 UDP 中继服务器设计
【发布时间】:2012-06-04 04:03:27
【问题描述】:

我正在创建一个 UDP 服务器,它需要从各种客户端接收 UDP 数据包,然后将它们转发给其他客户端。我正在使用 C#,所以每个 UDP 套接字对我来说都是一个 UdpClient。我想我想要 2 个 UdpClient 对象,一个用于接收,一个用于发送。接收套接字将绑定到一个已知端口,而发送者根本不会绑定。

服务器会获取每个数据包,在数据包数据中查找用户名,然后根据服务器维护的路由列表,将数据包转发给1个或多个其他客户端。

我开始监听 UdpClient:

   UdpClient udpListener = new UdpClient(new IPEndPoint(ListenerIP, UdpListenerPort));
   udpListener.BeginReceive(new AsyncCallback(OnUDPRead), udpListener);

我的回调看起来像:

    void OnUDPRead(IAsyncResult ar)
    {
        UdpClient udpListener = (UdpClient)ar.AsyncState;

        try
        {
            IPEndPoint remoteEndPoint = null;
            byte[] packet = udpListener.EndReceive(ar, ref remoteEndPoint);

            // Get connection based on data in packet

            // Get connections routing table (from memory)

            // ***Send to each client in routing table***
        }
        catch (Exception ex)
        {
        }

        udpListener.BeginReceive(new AsyncCallback(OnUDPRead), udpListener);
    }

服务器需要每秒处理 100 个数据包(这是针对 VOIP)。我不确定的主要部分是在上面的“发送给每个客户”部分要做什么。我应该使用UdpClient.Send 还是UdpClient.BeginSend?由于这是针对实时协议的,因此为任何给定客户端设置多个待处理/缓冲的发送是没有意义的。

由于我在任何给定时间只发布了一个 BeginReceive(),我假设我的 OnUDPRead 函数在已经执行时永远不会被调用。除非 UdpClient 在这方面的工作方式与 TcpClient 不同?由于我不给它一个缓冲区(我不给它任何缓冲区),我想它可以在只发布一个 BeginReceive 的情况下多次触发 OnUDPRead?

【问题讨论】:

    标签: c# sockets udp rtp


    【解决方案1】:

    我看不出有什么理由不能直接使用 one 套接字。在我看来,异步 I/O 在这种情况下并没有提供任何惊人的好处。只需使用非阻塞模式。收到一个数据包,发送它。如果 send() 导致 EAGAIN 或 EWOULDBLOCK 或任何 C# 表现形式,只需将其删除。

    在您的计划中,发送套接字将在您第一次调用 send() 时自动绑定。客户端将回复该端口。所以它必须是你正在阅读的端口。而且您已经拥有其中之一。

    【讨论】:

    • 谢谢,看来在服务器上使用 2 个套接字很可能导致我的 NAT 穿越问题。由于服务器需要在它收到数据包的同一端口上发送出去,因此基本的“UDP打孔”工作。当我第一次尝试使用单个套接字时,我遇到了一些问题,一旦我发送了 1 个数据包,接收将不断出错,并显示“连接被远程主机强制关闭”,所以我在 SO 上发现了一些说 2 是必需的——我认为那一定是错的。
    • @eselk 那一定是 TCP。 UDP 中没有要关闭的连接,无论是强制还是其他方式。
    【解决方案2】:

    EJP 所说的。

    大多数关于使用异步 IO 扩展套接字服务器的文献都是在考虑 TCP 场景的情况下编写的。去年我对如何扩展 UDP 套接字服务器进行了大量研究,发现 IOCP/epoll 并不像 TCP 那样适合 UDP。看到这个答案here

    因此,仅使用多个线程来扩展 UDP 套接字服务器实际上比尝试执行任何类型的异步 I/O 事情要简单。很多时候,一个线程抽出 recvfrom/sendto 调用就足够了。

    我建议服务器为每个客户端设置一个同步套接字。然后让几个线程在客户端套接字的不同子集上执行 Socket.Select 调用。我想您可以为所有客户端使用一个套接字,但是如果您尝试从多个线程处理该套接字上的音频数据包,则会遇到一些乱序问题。

    还可以考虑使用 UdpClient 的较低级别的套接字类。

    但如果您真的想向外扩展 - 您可能希望在 C/C++ 中完成核心工作。使用 C#,winsock 之上的更多抽象层意味着更多的缓冲区副本和更高的延迟。您可以通过一些技术使用 winsock 将数据包从一个套接字转发到另一个套接字,而不会产生缓冲区副本。

    【讨论】:

      【解决方案3】:

      这在很大程度上取决于规模 - 即您想要中继多少并发 RTP 流。我已经开发了这个解决方案,作为我们 SIP 交换机的媒体网关的一部分。尽管最初有保留,但我使用 C# 非常高效地工作(单个服务器上 1000 个并发流,每秒 50K 数据报,使用 20ms PTime)。

      通过反复试验,我确定了以下几点:

      1) 使用 Socket.ReceiveFromAsync 比使用同步方法要好得多,因为您不需要为每个流使用单独的线程。收到数据包后,它们会通过线程池进行调度。

      2) 创建/处置 SocketAsyncEventArgs 结构需要时间 - 最好在启动时分配结构池并从池中“借用”它们。

      3) 对于处理过程中所需的任何其他对象(例如 RTPPacket 类)也是如此。以如此高的速率动态分配这些会在任何类型的实际负载下很快导致 GC 问题。如果您避免所有动态分配,而是使用您自己管理的对象池,这个问题就会消失。

      4) 如果您需要做任何时钟来协调输入和输出流、转码、混合或文件播放,请不要尝试使用标准的 .NET 计时器。我从 winmm.dll 包装了 Win32 多媒体计时器 - 这些更精确。

      我最终得到了一个架构,其中组件可以公开 IRTPSource 和 IRTPSink 接口,然后可以将它们连接起来以创建所需的图形。我创建了 RTP 输入/输出组件、混音器、播放器、录音机、(简单)转码器、播放缓冲区等 - 然后使用这两个接口将它们连接起来。然后使用 MM 计时器对 RTP 数据包进行计时。

      事实证明,对于通常使用 C 或 C++ 处理的问题,C# 是一个不错的选择。

      迈克

      【讨论】:

        【解决方案4】:

        一个好的设计不是重新发明轮子:)

        已经有很多 RTP 媒体代理解决方案:Kamailio、FreeSWITCH、Asterisk 和其他一些开源项目。我在 FreeSWITCH 方面拥有丰富的经验,强烈推荐它作为 SIP 呼叫的媒体代理。

        如果您的信令协议不是 SIP,也有一些针对其他协议的解决方案。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-08-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多