良好的 UDP 中继服务器设计

本文关键字:服务器 UDP | 更新日期: 2023-09-27 18:35:45

我正在创建一个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?

良好的 UDP 中继服务器设计

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

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

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

通过反复试验,我确定了以下内容:

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

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

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

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

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

事实证明,C# 对于通常使用 C 或 C++ 的方法来说是一个不错的选择。

话筒

EJP说的。

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

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

我建议服务器每个客户端都有一个同步套接字。 然后让多个线程执行套接字。选择对客户端套接字的不同子集进行调用。 我想你可以为所有客户端提供一个套接字,但是如果你尝试处理来自多个线程的该套接字上的音频数据包,你会遇到一些乱序的问题。

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

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

一个

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

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

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