【问题标题】:Managed Sockets not behaving as (I) expected托管套接字的行为不像 (I) 预期的那样
【发布时间】:2019-09-19 20:53:56
【问题描述】:

所以我一直很难找到关于确切当你有 2 个套接字应该如何表现的文档,它们都绑定到同一个端点,但其中一个也是 已连接 到远程端点。

  • 套接字是 UDP IPv4
  • 在 .net core 2.2/3 on linux x64 上运行

我能够从各种来源收集到的是,连接的套接字应该始终并且只从它所连接的端点接收数据报,而“未连接”的套接字将接收 其他一切

我依稀记得读过内核套接字实现在 dgram 到达时为每个套接字分配“点”,并且得分较高的套接字(最具体的路由)获取数据。如果两个套接字获得相同的分数,则 dgram 在套接字之间是“负载平衡”的。

我做了一个小测试:

class Program
{
    static void Main(string[] args)
    {
        var localEp = new IPEndPoint(IPAddress.Loopback, 1114);
        var remoteEp = new IPEndPoint(IPAddress.Loopback, 1115);

        //Socket bound to local EP, not connected should receive from everyone
        var notConnected = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        notConnected.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        notConnected.Bind(localEp);

        //Socket bound and connected should receive from only it's remote EP
        var connected = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        connected.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        connected.Bind(localEp);
        connected.Connect(remoteEp);

        var notConnectedTask = Task.Run(() => Receive(notConnected, "Not Connected"));
        var connectedTask = Task.Run(() => Receive(connected, "Connected"));

        //Remote socket to send to connected socket
        var remote1 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        remote1.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        remote1.Bind(remoteEp);
        remote1.Connect(localEp);

        //Remote socket to send to notConnected socket
        var remote2 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        remote2.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
        remote2.Bind(new IPEndPoint(IPAddress.Loopback, 1116));
        remote2.Connect(localEp);

        for (int i = 0; i < 10; i++)
        {
            //This should be received by connected socket only
            remote1.Send(Encoding.Default.GetBytes($"message {i} to connected socket"));

            //This should be received by unconnected socket only
            remote2.Send(Encoding.Default.GetBytes($"message {i} to notConnected socket"));
        }

        remote1.Send(Encoding.Default.GetBytes("end"));
        remote2.Send(Encoding.Default.GetBytes("end"));

        Task.WaitAll(notConnectedTask, connectedTask);
    }

    public static void Receive(Socket sock, string name)
    {
        EndPoint ep = new IPEndPoint(IPAddress.Any, 0);
        var buf = new byte[1024];

        Console.WriteLine($"{name} is listening...");

        while (true)
        {
            var rcvd = sock.ReceiveFrom(buf, ref ep);

            var msg = Encoding.Default.GetString(buf.Take(rcvd).ToArray());

            Console.WriteLine($"{name} => {msg}");

            if (msg.SequenceEqual("end"))
                return;
        }
    }
}

令我惊讶和懊恼的是,结果没有接近我的预期:

Connected is listening...
Not Connected is listening...
Connected => message 0 to connected socket
Connected => message 0 to notConnected socket
Connected => message 1 to connected socket
Connected => message 1 to notConnected socket
Connected => message 2 to connected socket
Connected => message 2 to notConnected socket
Connected => message 3 to connected socket
Connected => message 3 to notConnected socket
Connected => message 4 to connected socket
Connected => message 4 to notConnected socket
Connected => message 5 to connected socket
Connected => message 5 to notConnected socket
Connected => message 6 to connected socket
Connected => message 6 to notConnected socket
Connected => message 7 to connected socket
Connected => message 7 to notConnected socket
Connected => message 8 to connected socket
Connected => message 8 to notConnected socket
Connected => message 9 to connected socket
Connected => message 9 to notConnected socket
Connected => end

notConnected 套接字不仅没有收到任何东西,而且连接的套接字也得到了一切...

所以我的期望似乎都不是真的。没有负载均衡,也没有积分系统。

我有posted a comment SO 问了这个问题,得到的答复几乎证实了我的期望:

我认为如果一个连接到远程端点,那么所有数据报 源自该远程端点将最终在连接 插座。未连接的只会捕获来自其他的数据报 远程端点。

我还收到一封来自 OpenSSL 邮件组的电子邮件,其中大部分也确认了这一点......

我想上面的测试应该可以明确回答我的问题,但它似乎太错了

也许我在代码中犯了一个错误,或者我只是遗漏了一些东西。不胜感激。

我对套接字的工作方式完全错误吗?

编辑

所以我只是重新运行了我的测试,和上面一模一样,结果几乎一样,只有接收数据的套接字是“notConnected”套接字。

绑定notConnected socket之后连接socket也没有效果。

【问题讨论】:

  • 如果先绑定已连接的套接字,然后绑定未连接的套接字会怎样?
  • @AndrewWilliamson 令人惊讶的是,完全没有……同样的结果
  • 我刚刚注意到 - 您的问题指出唯一接收数据的套接字是 notConnected 套接字,但日志输出显示 Connected =&gt; message 0 to connected socket。你误读了输出吗?
  • @AndrewWilliamson 是的,我添加了一个编辑。似乎它几乎是随机的。有时是一个,有时是另一个......我必须在写问题时重新运行测试并且没有注意到不同的输出。
  • @Luaan:“除此之外什么都做不了”——差不多。它过滤传入的数据报。您是对的,它不会将无连接套接字转换为面向连接的套接字,但它确实比仅允许调用 Send() 而不是 SendTo() 更有效。

标签: c# sockets


【解决方案1】:

我依稀记得读过内核套接字实现在 dgram 到达时为每个套接字分配“点”,并且得分较高的套接字(最具体的路由)获取数据。如果两个套接字获得相同的分数,则 dgram 在套接字之间是“负载平衡”的。

我很想知道你在哪里读到的。因为它与我读过或听说过的关于重用套接字地址的任何内容都不一致。

相反,我的理解一直是,如果您重用套接字地址,则行为是未定义/不确定的。 For example:

一旦第二个套接字成功绑定,绑定到该端口的所有套接字的行为都是不确定的。

当我运行您的测试代码时,我得到的行为与您报告的相反。特别是“未连接”套接字是接收所有流量的套接字。

当我修改代码以便两个套接字调用Connect(),每个远程端点地址各一个时,只有一个套接字最终获得任何数据报。这也符合我的理解和之前的测试。特别是,无连接协议套接字上的Connect() 在套接字级别运行,在应用程序看到之前过滤掉套接字接收的任何数据报。

因此,在我的计算机上,“未连接”套接字是获取所有流量的套接字,如果我告诉它连接到正在发送数据报的远程端点之一,那么它仍然接收所有流量,我的应用程序只能看到它通过Connect() 调用请求的那些数据报。其他数据报被丢弃。

(顺便说一句:在我看来,“连接”使用无连接协议的套接字应该被认为只是一种方便,而不应被视为实际上 连接套接字. 同一个套接字仍然可以通过SendTo() 向其他远程端点发送数据报,并且该套接字仍在接收来自其他远程端点的流量,您的程序只是看不到该流量。)

对于重用的套接字地址,我过去也看到过,流量是随机传递的。 IE。有时一个套接字获取流量,有时另一个套接字获取流量。至少有一些套接字持续接收流量这一事实是一个改进!

但是,我认为您不应该有任何理由期望SocketOptionName.ReuseAddress 能够可靠地工作。它没有记录这样做,根据我的经验,它没有。您报告的结果,以及我使用相同代码获得的不同结果,都完全符合重用套接字地址的“非确定性”性质。

如果您看到任何声称重用套接字地址可以和/或应该产生一些确定性结果的东西,我会说这种引用是不正确的。

【讨论】:

  • 我看到您链接到专门讨论 Winsock 的 ms 文档。我说的是linux上的套接字。虽然有趣的是,在 Windows 上,行为被 记录 为未定义。 dotnet 运行时是否可能提供与底层套接字引擎不同的行为?
  • “dotnet 运行时是否可能提供与底层套接字引擎不同的行为?” -- 是的,有可能。但如果真的发生了,我会感到惊讶。最初的 BSD 套接字规定了所需的行为,然后在 Winsock 中进行了复制。在 Linux 中,有可能在基本套接字 API 上添加了额外的保证,但我想说,鉴于您和我的观察,SO_REUSEADDRESS 方面并非如此。
  • 嗨,彼得,如果您有兴趣,我编辑了我的答案,并附上了我在问题中描述的“评分系统”的链接。
【解决方案2】:

所以阅读linux man pages for SO_REUSEPORT,我遇到了这个:

对于 UDP 套接字,使用该选项可以提供更好的 将传入的数据报分发到多个进程(或 线程)与具有的传统技术相比 多个进程竞争接收同一数据报 插座。

所以看起来我需要的是 SO_REUSEPORT,而不是 SO_REUSEADDRESS。这很不幸,因为 SO_REUSEPORT 在 Windows 上不可用...

另外,为了确认 Peter Duniho 的回答,直接来自the horse's mouth

在 UDP 中使用 so_reuseport 的动机类似于 DNS 服务器。另一种方法是在同一个套接字上从 多个线程。与 TCP 的情况一样,这些负载 线程往往是不成比例的,我们也看到很多 插座锁上的连接。请注意, SO_REUSEADDR 已经允许 多个 UDP 套接字绑定到同一个端口,但是没有 防止劫持和不分发数据包的规定 跨所有共享相同绑定端口的套接字

总而言之,绑定到同一个端点并设置了 SO_REUSEADDRESS 的多个 UDP 套接字将具有未定义的行为。也就是说,没有办法知道数据会在哪里结束。

与 SO_REUSEPORT 绑定的多个 UDP 套接字将看到 dgram 以一种“负载平衡”的方式分布。

由于我仍然不知道一个已连接/绑定的套接字和一个绑定的套接字将如何使用 SO_REUSEPORT,我将使用 SO_REUSEPORT 测试上面的场景并更新此答案。

所以this 对 linux 内核的提交实际上实现了我认为我已经阅读过的“套接字评分”系统。具体来说,static int compute_score 似乎采用“udp 表”并计算给定数据报得分最高的套接字。这应该保证一个连接的套接字将从它的端点接收 dgram,即使另一个套接字也绑定到同一个本地端点。

This 是我为测试此案例而创建的要点。它像我希望的那样工作,连接的套接字总是从它的远程端点接收 dgram。

【讨论】:

    猜你喜欢
    • 2017-04-11
    • 1970-01-01
    • 2021-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-11
    • 2016-01-28
    相关资源
    最近更新 更多