【问题标题】:UDP hole punching. Have server talk to clientUDP 打孔。让服务器与客户端对话
【发布时间】:2012-08-21 14:09:26
【问题描述】:

我阅读了很多关于如何实现 UDP 打孔的文章,但由于某种原因我无法使其工作。

对于不熟悉 udp 打孔的人,这里是我自己的定义:

目标是能够在两个客户端(客户端 A 和客户端 B) 在服务器的帮助下。所以客户端 A 连接到服务器并发送它的信息。客户 B 也是如此。服务器具有必要的信息,以便客户端 A 能够向客户端 B 发送数据,反之亦然。因此,服务器将该信息提供给两个客户端。一旦两个客户端都获得了关于彼此的信息,就可以在没有服务器帮助的情况下开始在这些客户端之间发送和接收数据。

我的目标是能够做我刚才描述的事情(udp 打孔)。 在这样做之前,我认为能够从服务器连接到客户端会很有帮助。为此,我计划向服务器发送有关客户端的信息。一旦服务器收到该信息,就会尝试从头开始连接到客户端。一旦我能够执行,我应该拥有开始实施真正的 udp 打孔所需的一切。

我是这样设置的:

顶部路由器的服务器和底部路由器连接到 LAN 端口。底部路由器 (NAT) 通过其 WAN 端口连接到顶部路由器。客户端计算机连接到底部路由器的 LAN 端口之一。

因此,在该连接中,客户端能够看到服务器,但服务器无法看到客户端。

所以我用伪代码做的算法是:

  • 客户端连接到服务器。
  • 客户端向服务器发送一些 UDP 包,以便在 NAT 上打开一些端口
  • 向服务器发送有关客户端正在侦听的端口的信息。
  • 一旦服务器收到该信息,尝试从头开始连接到客户端。

下面是代码中的实现:

服务器:

static void Main()
{     
    /* Part 1 receive data from client */
    UdpClient listener = new UdpClient(11000);
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);
    string received_data;
    byte[] receive_byte_array = listener.Receive(ref groupEP);       
    received_data = Encoding.ASCII.GetString(receive_byte_array, 0, receive_byte_array.Length);

    // get info
    var ip = groupEP.Address.ToString();
    var port = groupEP.Port;

    /* Part 2 atempt to connect to client from scratch */
    // now atempt to send data to client from scratch once we have the info       
    Socket sendSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    IPEndPoint endPointClient = new IPEndPoint(IPAddress.Parse(ip), port);
    sendSocket.SendTo(Encoding.ASCII.GetBytes("Hello"), endPointClient);
}

客户:

static void Main(string[] args)
{
    /* Part 1 send info to server */
    Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram,  ProtocolType.Udp);
    IPAddress send_to_address = IPAddress.Parse("192.168.0.132");
    IPEndPoint sending_end_point = new IPEndPoint(send_to_address, 11000);
    sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);

    // get info
    var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];

    /* Part 2 receive data from server */
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
    byte[] buffer = new byte[1024];
    sending_socket.Receive(buffer);
} 

由于某种原因成功了几次!当客户端成功在线接收数据时就可以了:sending_socket.Receive(buffer);

注意事项: 如果在第二部分的服务器上,我使用实例变量 listner 而不是创建新变量 sendSocket 并通过该变量发送字节,客户端能够接收正在发送的数据。请记住,服务器的第二部分将由第二个客户端 B 实现,这就是我再次从头开始初始化变量的原因......


编辑:

这是查看同一问题的不同方式。当我初始化一个新对象而不是使用同一个对象时,客户端不会收到响应。

我有一个 UdpClient 类型的对象。我能够将该对象的数据发送给其他对等方。如果我创建另一个具有相同属性的相同类型的对象并尝试发送数据它不起作用!我可能缺少初始化一些变量。我可以通过反射设置私有变量,所以我应该没有问题。反正这里是服务器代码:

public static void Main()
{
    // wait for client to send data
    UdpClient listener = new UdpClient(11000);
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);        
    byte[] receive_byte_array = listener.Receive(ref groupEP);

    // connect so that we are able to send data back
    listener.Connect(groupEP);

    byte[] dataToSend = new byte[] { 1, 2, 3, 4, 5 };

    // now let's atempt to reply back

    // this part does not work!
    UdpClient newClient = CopyUdpClient(listener, groupEP);
    newClient.Send(dataToSend, dataToSend.Length);

    // this part works!
    listener.Send(dataToSend, dataToSend.Length);
}

static UdpClient CopyUdpClient(UdpClient client, IPEndPoint groupEP)
{
    var ip = groupEP.Address.ToString();
    var port = groupEP.Port;
    var newUdpClient = new UdpClient(ip, port);
    return newUdpClient;
}

客户端代码基本上是向服务器发送数据,然后等待响应:

    string ipOfServer = "192.168.0.132";
    int portServerIsListeningOn = 11000;

    // send data to server
    Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    IPAddress send_to_address = IPAddress.Parse(ipOfServer);
    IPEndPoint sending_end_point = new IPEndPoint(send_to_address, portServerIsListeningOn);
    sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);

    // get info
    var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];

    // now wait for server to send data back
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
    byte[] buffer = new byte[1024];
    sending_socket.Receive(buffer); // <----- keeps waiting in here :(

请注意,客户端位于路由器 (NAT) 后面,否则我不会遇到此问题。 我想复制 udpClient 的原因是我可以将该变量发送到另一台计算机启用另一台计算机向客户端发送数据。

所以我的问题是为什么原始对象listener 能够发送数据而newClient 不能?即使在服务器执行了newClient.Send(dataToSend, dataToSend.Length); 行之后,客户端仍然在行sending_socket.Receive(buffer); 处等待。当侦听器发送数据而不是newClient时,客户端成功接收数据。如果两个变量具有相同的目标 IP 和端口,为什么会这样?变量有何不同?

注意: 如果服务器和客户端在同一网络上,则复制工作并且变量newClient 能够将数据发送到客户端。要模拟此问题,客户端必须位于 NAT(路由器)之后。这种网络的一个例子可能包括两个路由器。我们称它们为路由器 X 和路由器 Y。您还需要一个称为 S 的服务器和一个客户端 C。因此 S 可以连接到 X 的 LAN 端口之一。C 可以连接到 Y 的 LAN 端口之一。最后将 Y 的 WAN 口连接到 X 的 LAN 口之一。

【问题讨论】:

  • 你有什么异常吗?
  • 没有例外。客户端只是一直在等待响应。
  • 可能是因为您应该进行“打孔”而不是打孔。

标签: c# sockets networking udp hole-punching


【解决方案1】:

嗯,我认为您在这里混淆了几件事。一方面,它真的叫UDP hole punching。让我试着解释一下这应该如何工作。

在将数据包从内部专用网络转发到外部 Internet 时,NAT 路由器通常执行port mapping。

假设您在 NAT 后面的机器上创建了一个 UDP 套接字,并将数据报发送到某个外部 IP/端口。当携带该数据报的 IP 数据包离开发送机器时,其 IP 报头的 源地址 字段设置为本地不可全局路由的私有 IP 地址(如192.168.1.15),而 UDP 报头具有source port 字段设置为分配给套接字的任何端口(通过绑定显式或由操作系统从 临时端口 隐式选择)。我将此源端口号称为P1。

然后,当 NAT 路由器将该数据包发送到外部网络时,它会将源 IP 地址覆盖到它自己的外部 IP 地址(否则无法将数据包路由回),并且通常会将源 UDP 端口覆盖到其他一些值(可能是因为专用网络上的某些其他主机使用相同的源端口,这会产生歧义)。原始源端口和新端口号(我们将其标记为P2)之间的映射保留在路由器中以匹配返回数据包。此映射也可能特定于目标 IP 地址 和目标 UDP 端口。

所以现在你在路由器上“打了一个洞”——发回路由器到端口P2的UDP数据包被转发到UDP端口P1上的内部机器。同样,根据 NAT 实现,这可能仅限于来自原始目标 IP 地址和目标 UDP 端口的数据包。

对于客户端到客户端的通信,您必须通过服务器将一个的外部 IP/端口告诉另一个,希望 NAT 路由器将相同的内部源端口映射到相同的外部源端口。然后客户端将使用这些数据包相互发送数据包。

希望这会有所帮助。

【讨论】:

  • 尼古拉非常感谢您的帮助。 +1 因为我感谢您的帮助!我在网上找到了很多像你这样的定义。但由于某种原因,我不能用代码来做到这一点。如果我能得到一个带有代码的例子,我将非常感激。首先,我只想对服务器和客户端进行 UDP 打孔,以使事情变得更容易。
  • 问题是您不需要打孔让服务器与客户端对话 - 只需发送回数据报来自的 IP/端口,使用 ReceiveFrom 即可。
  • 这是我读过的关于 UDP 打孔的最好解释。我会向任何需要了解它的工作原理的人推荐这个。被大大低估的答案。
【解决方案2】:

终于找到答案了!这是仅使用客户端和服务器的实现。我的下一次尝试将是使用 3 台计算机。无论如何希望这会有所帮助:

服务器代码:

class Program
{
    static byte[] dataToSend = new byte[] { 1, 2, 3, 4, 5 };

    // get the ip and port number where the client will be listening on
    static IPEndPoint GetClientInfo()
    {
        // wait for client to send data
        using (UdpClient listener = new UdpClient(11000))
        {
            IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);
            byte[] receive_byte_array = listener.Receive(ref groupEP);

            return groupEP;
        }
    }

    static void Main(string[] args)
    {
        var info = GetClientInfo(); // get client info

        /* NOW THAT WE HAVE THE INFO FROM THE CLIENT WE ARE GONG TO SEND
           DATA TO IT FROM SCRATCH!. NOTE THE CLIENT IS BEHIND A NAT AND
           WE WILL STILL BE ABLE TO SEND PACKAGES TO IT
        */

        // create a new client. this client will be created on a 
        // different computer when I do readl udp punch holing
        UdpClient newClient = ConstructUdpClient(info);

        // send data
        newClient.Send(dataToSend, dataToSend.Length);            
    }

    // Construct a socket with the info received from the client
    static UdpClient ConstructUdpClient(IPEndPoint clientInfo)
    {          
        var ip = clientInfo.Address.ToString();
        var port = clientInfo.Port;

        // this is the part I was missing!!!!
        // the local end point must match. this should be the ip this computer is listening on
        // and also the port            
        UdpClient client = new UdpClient(new IPEndPoint( IPAddress.Any, 11000));

        // lastly we are missing to set the end points. (ip and port client is listening on)

        // the connect method sets the remote endpoints
        client.Connect(ip, port);

        return client;
    }
}

客户端代码:

string ipOfServer = "192.168.0.139";
int portServerIsListeningOn = 11000;

// send data to server
Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPAddress send_to_address = IPAddress.Parse(ipOfServer);
IPEndPoint sending_end_point = new IPEndPoint(send_to_address, portServerIsListeningOn);
sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);

// get info
var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];

// now wait for server to send data back
IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
byte[] buffer = new byte[1024];
sending_socket.Receive(buffer); // <----- we can receive data now!!!!!

【讨论】:

  • 您好 Tono,感谢您的示例!您对“三方”沟通有任何更新吗?如果是,请与我们分享:)
  • 不,我停止从事那个项目。我在路由器上配置了端口转发。让它与 udp 一起工作会很好!
  • 谢谢,托诺。也许感兴趣:我正在设计一个设置,其中一些“客户”可以是“主人”,接受来自多个其他客户的连接。我猜想不止一个客户端可以使用相同的端点连接到主服务器,但一个简单的解决方案可能是主服务器为每个客户端使用不同的端口。一旦有一个客户端加入,master 就会选择一个新端口并向服务器发出一个新请求,因此总是有一个“通道”等待下一个想要加入的客户端。也许类似的概念也适用于您的三向沟通。
  • @Tano 如果我将我的外部(公共)IP 作为“ipOfServer”的值,它会从我的电脑外部收到来自客户端的任何消息吗?
【解决方案3】:

您是否考虑过在客户端上使用 UPnP 来配置 NAT 穿越以允许特定端口上的传入数据包?然后,客户端只需将入站 IP 和端口传递给服务器,并等待服务器发送数据包。 http://en.wikipedia.org/wiki/Universal_Plug_and_Play

【讨论】:

    【解决方案4】:

    看来你第一次可以连接到服务器。连接成功后每次都需要关闭和断开连接。请查看此示例代码 http://codeidol.com/csharp/csharp-network/Connectionless-Sockets/A-Simple-UDP-Application/

    【讨论】:

      猜你喜欢
      • 2020-11-13
      • 1970-01-01
      • 1970-01-01
      • 2015-11-30
      • 1970-01-01
      • 2017-12-18
      • 2015-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多