NAT后面的UDP打孔

时间:2013-09-30 05:31:06

标签: java networking udp p2p nat

我试图在Java中实现UDP-Holepunching的简单草图来测试它的概念,并在以后的C / C ++应用程序中使用它。

概念:

从维基百科开始,我将这个概念理解为: 让A和B成为未定义网络结构的客户端,C是一个众所周知的公共可达服务器。

  1. A向服务器C发送数据包,服务器保存其IP地址和端口。 C将获得A的NAT的公共IP地址。这样做,A前面的NAT将创建一个路由,将该端口上的所有数据包传递给A.
  2. B与A相同,向服务器C发送数据包,然后保存地址和端口,B的NAT创建路由等等。
  3. 此时,C知道每个客户端的地址和端口。 C将B的地址和端口发送给A,并从A发送给B.
  4. A向B发送一个数据包,该数据包将被B的NAT拒绝,但这样做会在A的NAT中打开一个“漏洞”,让B的其他数据包通过。
  5. B向A发送一个数据包,它将到达A,因为“洞”之前是“打孔”。这样做也会在B的NAT中打开一个“洞”,让A的更多数据包通过。
  6. 现在已经完成了打孔,A和B应该能够相互通信P2P
  7. 这一切都优于localhost(这并不是一个大惊喜),但在现实世界中,这种情况失败了。

    问题:

    A和B都能够连接到服务器C,服务器C获取其数据包,存储其地址和端口并将其传输到另一个客户端。 但此时它失败了。 A和B无法相互通信。 所以我问自己哪里做错了。我花了几天时间在google和stackoverflow中搜索工作示例,但我偶然发现的是使用STUN的建议,这不是我想要的。

    实现:

    下面我将用Java发布我的草图,因为我不知道我的概念或实现是否有问题。

    这是服务器的代码:

    public class Server
    {
        public static void main(String[] args)
        {
            int port1 = 0, port2 = 0;
            String address1 = null, address2;
            byte[] bytes = new byte[1024];
            try
            {
                System.out.println("Server waiting");
                DatagramSocket ds = new DatagramSocket(789);
                while(!Thread.interrupted())
                {
                    DatagramPacket p = new DatagramPacket(bytes, bytes.length);
                    ds.receive(p);
                    if(port1 == 0)
                    {
                        port1 = p.getPort();
                        address1 = p.getAddress().getHostAddress();
                        System.out.println("(1st) Server received:" + new String(bytes) + " from " + address1 + " on port " + port1);
                    }
                    else
                    {
                        port2 = p.getPort();
                        address2 = p.getAddress().getHostAddress();
                        System.out.println("(2nd) Server received:" + new String(bytes) + " from " + address1 + " on port " + port1);
                        sendConnDataTo(address1, port1, address2, port2, ds);
                        sendConnDataTo(address2, port2, address1, port1, ds);
                    }
                }
                ds.close();
            }
            catch(Exception e)
            {
                e.printStackTrace();
            }
        }
    
        public static void sendConnDataTo(String a1, int p1, String a2, int p2, DatagramSocket ds)
        {
            byte[] bA, bP;
            bA = a1.getBytes();
            bP = Integer.toString(p1).getBytes();
            DatagramPacket pck;
            try
            {
                pck = new DatagramPacket(bA, bA.length, InetAddress.getByName(a2), p2);
                ds.send(pck);
                pck = new DatagramPacket(bP, bP.length, InetAddress.getByName(a2), p2);
                ds.send(pck);
            }
            catch(Exception e)
            {
                e.printStackTrace();
            }
        }
    }
    

    请注意,这只是一些草图,没有真正的应用。服务器应该只接收来自两个客户端的数据包,保存它们的地址和端口并将其传递给另一个客户端。

    这是客户端的代码:

    public class Client
    {
        private DatagramSocket socket;
        private int init = 0;
        private String target;
        private int port;
    
        public Client()
        {
            try
            {
                socket = new DatagramSocket();
            }
            catch(SocketException e)
            {
                e.printStackTrace();
            }
            Thread in = new Thread()
            {
                public void run()
                {
                    while(true)
                    {
                        byte[] bytes = new byte[1024];
                        DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
                        try
                        {
                            socket.receive(packet);
                            bytes = Arrays.copyOfRange(bytes, 0, packet.getLength());
                            String s = new String(bytes);
                            System.out.println("Received: " + s);
                            if(init == 0)
                            {
                                target = s;
                                System.out.println("Target: " + target);
                                init++;
                            }
                            else if(init == 1)
                            {
                                port = Integer.parseInt(s);
                                System.out.println("Port: " + port);
                                init++;
                            }
                            else System.out.println(new String(bytes));
                        }
                        catch(IOException e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
            };
            in.start();
            connectToSupervisor();
        }
    
        private void connectToSupervisor()
        {
            byte[] bytes = new byte[1024];
            System.out.println("Greeting server");
            System.arraycopy("EHLO".getBytes(), 0, bytes, 0, 4);
            try
            {
                DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("localhost"), 789);
                socket.send(packet);
                System.out.println("Greetings sent...");
            }
            catch(IOException e)
            {
                e.printStackTrace();
            }
            send();
        }
    
        private void send()
        {
            while(init != 2)
            {
                try
                {
                    Thread.sleep(20L);
                }
                catch(InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
            System.out.println("Init completed!");
            while(true)
            {
                byte[] b2 = "Hello".getBytes();
                byte[] b1 = new byte[6];
                System.arraycopy(b2, 0, b1, 0, b2.length);
                try
                {
                    DatagramPacket packet = new DatagramPacket(b1, b1.length, InetAddress.getByName(target), port);
                    socket.send(packet);
                }
                catch(Exception e)
                {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args)
        {
            new Client();
        }
    }
    

    客户端只会向服务器发送数据包,从中侦听数据包,从其他客户端获取连接数据,然后不断向其发送包含“Hello”的数据包。

    我很抱歉长代码,但我想保持完整。

    如果你们中的任何人能够指出我正在做的错误,我会很高兴,解释一下为什么这样做不起作用,给我一个有效的例子,或者至少指出一个替代方案。

3 个答案:

答案 0 :(得分:3)

您的代码似乎是正确的。我测试了你的代码,它工作正常。这个概念也是正确的。但请检查您运行的客户端是否在同一NAT设备或不同的NAT设备中。如果您在同一NAT设备下运行两个客户端,那么它可能无法工作,因为并非所有NAT设备都支持发夹,即两个客户端都将数据包发送到需要传递给自身的NAT外部IP。有关更多信息,请参阅此链接: http://tools.ietf.org/html/rfc4787#section-6

答案 1 :(得分:2)

鉴于你的概念轮廓,我认为在第4点有一个问题。虽然A通过自己的NAT打了一个洞,当B试图到达这个洞时,它不知道A的NAT端口(或者更正确/通常 - NAPT)因此当B尝试通信时,A的NAT会丢弃数据包。

答案 2 :(得分:2)

对于那篇伟大帖子后面的人,请注意,在服务器端,收到的第二个UDP数据包被宣布为: System.out.println(“(第二个)服务器收到:”+ new String( bytes)+“from”+ address1 +“on port”+ port1 ); 它应该是 System.out.println(“(2nd) )服务器收到:“+新字符串(字节)+”来自“+ 地址2 +”端口“+ port2 ”; 它没什么大不了的只是一条信息性的消息,但它让我失去了一些时间,只是想知道路由器在地球上如何为两个不同的通信提供相同的端口:P