UDP打洞。让服务器与客户端通信。

16

我一直在学习如何实现UDP打洞,但不知为何无法使其生效。

对于不熟悉UDP打洞的人,这是我的定义:

目标是让两个客户端(客户端A和客户端B)能够在服务器的帮助下传输数据。因此,客户端A连接到服务器并发送其信息。客户端B也是如此。服务器有必要的信息,使得客户端A能够向客户端B发送数据,反之亦然。因此,服务器将该信息提供给两个客户端。一旦两个客户端都掌握了关于彼此的信息,就可以开始在两个客户端之间发送和接收数据,而不需要服务器的帮助。

我的目标是能够实现我刚才描述的UDP打洞。 但在此之前,我认为能够从服务器连接到客户端会很有帮助。为了做到这一点,我计划向服务器发送客户端的信息。一旦服务器收到该信息,尝试从头开始连接到客户端。一旦我能够执行此操作,我就应该拥有开始实现真正的UDP打洞所需的一切。

以下是我的设置方式:

enter image description here

顶部路由器连接了服务器和底部路由器到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);处等待。当监听器发送数据时,客户端成功接收数据,但是使用新的客户端却无法实现。如果这两种变量都具有相同的目标IP和端口,则为什么会出现这种情况?这些变量有何不同?

注意: 如果服务器和客户端在同一网络中,则复制有效,并且变量newClient能够向客户端发送数据。要模拟此问题,必须将客户端放在NAT(路由器)后面。这样的网络示例可能由两个路由器组成。我们称其为路由器X和路由器Y。您还需要一个名为S的服务器。客户端C连接到Y的其中一个LAN端口。最后,将Y的WAN端口连接到X的其中一个LAN端口。


你有收到任何异常吗? - Sajith
客户端一直在等待响应,没有任何异常。 - Tono Nam
1
可能是因为你应该进行“打洞”而不是“穿孔”。 - Jimmy D
4个回答

14

嗯,我认为你在混淆几件事情。首先,这实际上被称为UDP打洞。让我尝试解释一下它应该如何工作。

NAT路由器通常在将数据包从内部私有网络转发到外部互联网时执行端口映射

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

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

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

为了进行客户端之间的通信,您需要通过服务器告知其中一方外部IP/端口,并希望NAT路由器将相同的内部源端口映射到相同的外部源端口。然后客户端将使用这些信息互相发送数据包。
希望这可以帮助您。

Nikolai非常感谢您的帮助。我很感激您的帮助!我在互联网上找到了很多像您这样的定义。但出于某种原因,我无法使用代码实现它。如果我能得到一个带有代码示例的例子,我将非常感激。首先,我只想使用服务器和客户端进行UDP打洞,以使事情变得更容易。 - Tono Nam
问题在于,服务器与客户端进行通信时不需要进行“打洞”操作 - 只需将数据报发送回来自的IP/端口,使用ReceiveFrom即可获取。 - Nikolai Fetissov
这是我读过的关于UDP打洞的最好的解释。我会向任何需要了解其工作原理的人推荐它。这个答案被大大低估了。 - Can Poyrazoğlu

3

终于找到答案了!这是一个只需要客户端和服务器的实现。我的下一个尝试将是使用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,感谢你的示例!你有关于“三方”通信的任何更新吗?如果有,请与我们分享:) - Steve Lam
不,我停止在那个项目上的工作了。我在路由器上配置了端口转发。如果能让它与UDP一起工作就太好了! - Tono Nam
谢谢,Tono。也许你会感兴趣:我正在设计一个设置,其中一些“客户端”可以成为“主机”,接受来自多个其他客户端的连接。我想可能会有多个客户端使用相同的主机端点,但简单的解决方案可能是让主机为每个客户端使用不同的端口。一旦一个客户端加入,主机将选择一个新的端口并向服务器发出新的请求,以便始终有一个“通道”等待下一个要加入的客户端。也许类似的概念也适用于您的三方通信。 - Barbarian
@Tano 如果我将我的外部(公共)IP作为“ipOfServer”的值,那么它会接收来自我的计算机外部的客户端消息吗? - mahmoud nezar sarhan

1
你有没有考虑在客户端使用UPnP来配置NAT穿透,以允许特定端口上的传入数据包?然后客户端只需要将传入IP和端口通信给服务器,并等待服务器发送数据包。 http://en.wikipedia.org/wiki/Universal_Plug_and_Play

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接