首页 文章

UDP打孔 . 让服务器与客户交谈

提问于
浏览
16

我一直在阅读很多关于如何实现UDP打孔的内容但是出于某些原因我无法使其工作 .

For those that are not familiar of what udp hole punching is here is my own definition:

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

我的目标是能够做我刚才描述的(udp打孔) . Before doing so I think it will be helpful to be able to connect from the server to the client . 为此,我计划向服务器发送有关客户端的信息 . 一旦服务器收到该信息尝试从头开始连接到客户端 . 一旦我能够执行,我应该拥有开始实现真正的udp打孔所需的一切 .

以下是我设置的方法:

enter image description here

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

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

所以我在伪代码中完成的算法是:

  • 客户端连接到服务器 .

  • 客户端向服务器发送一些UDP包,以便在NAT上打开一些端口

  • 将信息发送到客户端正在侦听的端口上的服务器 .

  • 一旦服务器收到该信息尝试从头开始连接到客户端 .

这是代码中的实现:

Server:

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);
}

Client:

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);
}

For some reason it worked a few times! 当客户端在线路上成功接收数据时,它可以工作: sending_socket.Receive(buffer);

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


编辑:

Here is a different way of looking at the same problem. 当我初始化一个新对象而不是使用同一个对象时,客户端不会收到响应 .

我有一个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 :(

note that the client is behind a router (NAT) otherwise I will not have this problem. 我想复制udpClient的原因是我可以将该变量发送到另一台计算机,使另一台计算机能够将数据发送到客户端 .

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

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

4 回答

  • 12

    嗯,我觉得你在这里混淆了几件事 . 首先,它真的叫做UDP hole punching . 让我试着解释一下这应该如何运作 .

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

    假设您在NAT后面的计算机上创建了UDP套接字,并将数据报发送到某个外部IP /端口 . 当携带该数据报的IP数据包离开发送机器时,其IP报头的源地址字段设置为本地不可全局路由的专用IP地址(如 192.168.1.15 ),UDP报头的源端口字段设置为分配的任何端口到套接字(显式地通过绑定,或由OS从临时端口隐式选择) . 我将这个源端口号称为 P1 .

    然后,当NAT路由器在外部网络上发送该数据包时,它会将源IP地址覆盖到其自己的外部IP地址(否则它将 P2 标记为 P2 )保留在路由器中以匹配返回数据包 . 此映射还可能特定于目标IP地址和目标UDP端口 .

    所以现在你在路由器中有"punched a hole" - 发送回路由器到端口 P2 的UDP数据包被转发到UDP端口 P1 上的内部机器 . 同样,根据NAT实现,这可能仅限于来自原始目标IP地址和目标UDP端口的数据包 .

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

    希望这可以帮助 .

  • 0

    终于找到了答案!这是仅使用客户端和服务器的实现 . 我的下一次尝试是使用3台电脑 . 无论如何希望这有助于:

    Server code:

    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;
        }
    }
    

    client code:

    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!!!!!
    
  • 1

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

  • 3

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

相关问题