p2p技术之tcp内网穿透 java实现版

NAT打洞原理(详细+经典)

https://blog.csdn.net/max126313/article/details/19165913

 

内网ip打洞-----p2p实现原理

https://blog.csdn.net/eydwyz/article/details/52449537

 

转:https://blog.csdn.net/kiss1987f5/article/details/57084303

如今p2p技术已经十分成熟,然而很多人停留在理论层面,在实现过程中遇到这样那样的问题,甚至有的人说tcp打洞无法实现,本文简单阐述tcp穿透的过程,然后附上完整的java代码。

由于32位Ip地址的稀少,我们身边的设备,大部分运行在nat后面,无论是家庭还是单位,都会由一个路由器统一接入互联网,很多设备连上路由器组成一个内网。同一内网里的所有设备,拥有相同的外网ip地址,内网设备对外网进行访问,每次会使用不同的端口进行通信,不同内网里面的设备不能直接进行连接 ,因为不知道对方的公网地址和端口,这个时候就需要借助一台公网的设备进行牵线搭桥,也就是大家常说的穿透打洞。穿透的原理和NAT的运行原理,就不在此讨论,网上已有大量理论文章。

假设现在有以下3台机器:

外网机器,IP:121.56.21.85 , 以下简称“主机A”

处在内网1下的机器,外网IP:106.116.5.45 ,内网IP:192.168.1.10, 以下简称“主机1”

处在内网2下的机器,外网IP:104.128.52.6 ,内网IP:192.168.0.11,以下简称“主机2”

很显然内网的两台机器不能直接连接,我们现在要实现的是借助外网机器,让两台内网机器进行tcp直连通讯。

实现过程如下:

1、主机A启动服务端程序,监听端口8888,接受TCP请求。

2、启动主机1的客户端程序,连接主机A的8888端口,建立TCP连接。

3、启动主机2的客户端程序,连接主机A的8888端口,建立TCP连接。

4、主机2发送一个命令告诉主机A,我要求与其他设备进行连接,请求协助进行穿透。

5、主机A接收到主机2的命令之后,会返回主机1的外网地址和端口给主机2,同时把主机2的外网地址和端口发送给主机1。

6、主机1和主机2在收到主机A的信息之后,同时异步发起对对方的连接。

7、在与对方发起连接之后,监听本地与主机A连接的端口(也可以在发起连接之前),(由于不同的操作系统对tcp的实现不尽相同,有的操作系统会在连接发送之后,把对方的连接当作是回应,即发出SYN之后,把对方发来的SYN当作是本次SYN的ACK,这种情况就不需要监听也可建立连接,本文的代码所在测试环境就不需要监听,测试环境为:服务器centos 7.3, 内网1 win10,内网2 win10和centos7.2都测试过)。

8、主机1和主机2成功连上,可以关闭主机A的服务,主机1和主机2的连接依然会持续生效,不关闭就形成了一个3方直连的拓扑网状结构网络。

服务器端代码:

package org.inchain.p2p;
 
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
 
/**
 * 外网端服务,穿透中继
 * 
 * @author ln
 *
 */
public class Server {
 
    public static List<ServerThread> connections = new ArrayList<ServerThread>();
 
    public static void main(String[] args) {
        try {
            // 1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
            ServerSocket serverSocket = new ServerSocket(8888);
            Socket socket = null;
            // 记录客户端的数量
            int count = 0;
            System.out.println("***服务器即将启动,等待客户端的连接***");
            // 循环监听等待客户端的连接
            while (true) {
                // 调用accept()方法开始监听,等待客户端的连接
                socket = serverSocket.accept();
                // 创建一个新的线程
                ServerThread serverThread = new ServerThread(socket);
                // 启动线程
                serverThread.start();
 
                connections.add(serverThread);
 
                count++;// 统计客户端的数量
                System.out.println("客户端的数量:" + count);
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


package org.inchain.p2p;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
 
/**
 * 外网端服务多线程处理内网端连接
 * 
 * @author ln
 *
 */
public class ServerThread extends Thread {
    // 和本线程相关的Socket
    private Socket socket = null;
    private BufferedReader br = null;
    private PrintWriter pw = null;
    
 
    public ServerThread(Socket socket) throws IOException {
        this.socket = socket;
        this.br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        this.pw = new PrintWriter(socket.getOutputStream());
    }
 
    // 线程执行的操作,响应客户端的请求
    public void run() {
 
        InetAddress address = socket.getInetAddress();
        System.out.println("新连接,客户端的IP:" + address.getHostAddress() + " ,端口:" + socket.getPort());
 
        try {
            pw.write("已有客户端列表:" + Server.connections + "\n");
 
            // 获取输入流,并读取客户端信息
            String info = null;
            
            while ((info = br.readLine()) != null) {
                // 循环读取客户端的信息
                System.out.println("我是服务器,客户端说:" + info);
 
                if (info.startsWith("newConn_")) {
                    //接收到穿透消息,通知目标节点
                    String[] infos = info.split("_");
                    //目标节点的外网ip地址
                    String ip = infos[1];
                    //目标节点的外网端口
                    String port = infos[2];
                    
                    System.out.println("打洞到 " + ip + ":" + port);
                    
                    for (ServerThread server : Server.connections) {
                        if (server.socket.getInetAddress().getHostAddress().equals(ip)
                                && server.socket.getPort() == Integer.parseInt(port)) {
                            
                            //发送命令通知目标节点进行穿透连接
                            server.pw.write("autoConn_" + socket.getInetAddress().getHostAddress() + "_" + socket.getPort()
                                    + "\n");
                            server.pw.flush();
                            
                            break;
                        }
                    }
                } else {
                    // 获取输出流,响应客户端的请求
                    pw.write("欢迎您!" + info + "\n");
                    // 调用flush()方法将缓冲输出
                    pw.flush();
                }
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("客户端关闭:" + address.getHostAddress() + " ,端口:" + socket.getPort());
            Server.connections.remove(this);
            // 关闭资源
            try {
                if (pw != null) {
                    pw.close();
                }
                if (br != null) {
                    br.close();
                }
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    @Override
    public String toString() {
        return "ServerThread [socket=" + socket + "]";
    }
}

客户端代码:

package org.inchain.p2p;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
 
/**
 * 内网客户端,要进行穿透的内网服务
 * 
 * @author ln
 *
 */
public class Client {
    
    //输入scanner
    private Scanner scanner = new Scanner(System.in);
    //是否等待输入
    private boolean isWaitInput = true;
    //首次与外网主机通信的连接
    private Socket socket;
    //首次与外网主机通信的本地端口
    private int localPort;
 
    private PrintWriter pw;
    private BufferedReader br;
    
    public static void main(String[] args) {
        new Client().start();
    }
    
    public void start() {
        try {
            // 新建一个socket通道
            socket = new Socket();
            // 设置reuseAddress为true
            socket.setReuseAddress(true);
 
            //TODO在此输入外网地址和端口
            String ip = "xxx.xxxx.xxxx.xxxx";
            int port = 8888;
            socket.connect(new InetSocketAddress(ip, port));
            
            //首次与外网服务器通信的端口
            //这就意味着我们内网服务要与其他内网主机通信,就可以利用这个通道
            localPort = socket.getLocalPort();
 
            System.out.println("本地端口:" + localPort);
            System.out.println("请输入命令 notwait等待穿透,或者输入conn进行穿透");
 
            pw = new PrintWriter(socket.getOutputStream());
            br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            
            try {
                while (true) {
                    if(process()) {
                        break;
                    }
                }
            } finally {
                // 关闭资源
                try {
                    if(pw != null) {
                        pw.close();
                    }
                    if (br != null) {
                        br.close();
                    }
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /*
     * 处理与服务器连接的交互,返回是否退出
     */
    private boolean process() throws IOException {
        
        String in = null;
        
        if (isWaitInput) {
            //把输入的命令发往服务端
            in = scanner.next();
            pw.write(in + "\n");
            
            //调用flush()方法将缓冲输出
            pw.flush();
            
            if ("notwait".equals(in)) {
                isWaitInput = false;
            }
        }
        //获取服务器的响应信息
        String info = br.readLine();
        if(info != null) {
            System.out.println("我是客户端,服务器说:" + info);
        }
        //处理本地命令
        processLocalCommand(in);
        
        //处理服务器命令
        processRemoteCommand(info);
        
        return "exit".equals(in);
    }
 
    private void processRemoteCommand(String info) throws IOException {
        if (info != null && info.startsWith("autoConn_")) {
            
            System.out.println("服务器端返回的打洞命令,自动连接目标");
            
            String[] infos = info.split("_");
            //目标外网地址
            String ip = infos[1];
            //目标外网端口
            String port = infos[2];
            
            doPenetration(ip, Integer.parseInt(port));
        }
    }
 
    private void processLocalCommand(String in) throws IOException {
        if ("conn".equals(in)) {
            System.out.println("请输入要连接的目标外网ip:");
            String ip = scanner.next();
            System.out.println("请输入要连接的目标外网端口:");
            int port = scanner.nextInt();
 
            pw.write("newConn_" + ip + "_" + port + "\n");
            pw.flush();
 
            doPenetration(ip, port);
            
            isWaitInput = false;
        }
    }
 
    /*
     * 对目标服务器进行穿透
     */
    private void doPenetration(String ip, int port) {
        try {
            //异步对目标发起连接
            new Thread() {
                public void run() {
                    try {
 
                        Socket newsocket = new Socket();
 
                        newsocket.setReuseAddress(true);
                        newsocket.bind(new InetSocketAddress(
                                InetAddress.getLocalHost().getHostAddress(), localPort));
 
                        System.out.println("connect to " + new InetSocketAddress(ip, port));
                        
                        newsocket.connect(new InetSocketAddress(ip, port));
                        
                        System.out.println("connect success");
 
                        BufferedReader b = new BufferedReader(
                                new InputStreamReader(newsocket.getInputStream()));
                        PrintWriter p = new PrintWriter(newsocket.getOutputStream());
                        
                        while (true) {
                            
                            p.write("hello " + System.currentTimeMillis() + "\n");
                            p.flush();
                            
                            String message = b.readLine();
                            
                            System.out.println(message);
                            
                            pw.write(message + "\n");
                            pw.flush();
                            
                            if("exit".equals(message)) {
                                break;
                            }
                            
                            Thread.sleep(1000l);
                        }
                        
                        b.close();
                        p.close();
                        newsocket.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }.start();
            
//            //监听本地端口
//            ServerSocket serverSocket = new ServerSocket();
//            serverSocket.setReuseAddress(true);
//            serverSocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), localPort));
//
//            // 记录客户端的数量
//            System.out.println("******开始监听端口:" + localPort);
//            // 循环监听等待客户端的连接
//            // 调用accept()方法开始监听,等待客户端的连接
//            Socket st = serverSocket.accept();
//            
//            System.out.println("成功了,哈哈,新的连接:" + st.getInetAddress().getHostAddress() + ":" + st.getPort());
//            
//            serverSocket.close();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("监听端口 " + socket.getLocalPort() + " 出错");
        }
    }
}

服务端就是一个普通的socket服务,没什么特别的地方。
客户端需要注意的是:

1、最关键的地方,设置SO_REUSEADDR参数,41行的socket.setReuseAddress(true)和161行的newsocket.setReuseAddress(true)。

2、内网主机穿透时一定要异步发起连接。
3、在穿透时,新建的连接,需要先设置SO_REUSEADDR,再绑定端口,最后进行连接,顺序不能错。


                                                Socket newsocket = new Socket(ip, port);
 
                        newsocket.setReuseAddress(true);
                        newsocket.bind(new InetSocketAddress(
                                InetAddress.getLocalHost().getHostAddress(), localPort));
 
                        System.out.println("connect to " + new InetSocketAddress(ip, port));
                        
                        System.out.println("connect success");


如果改成上面这样就会连接超时。

最后附上测试方法和运行效果:


使用方法:

1、在服务器启动Server。
2、在客户端1启动Client,输入notwait命令,等待服务器通知打洞。
3、在客户端2启动Client,输入conn命令,然后输入服务器返回的客户端1的外网ip和端口,接下来就会自动完成连接。
运行效果:

客户端1运行结果 (穿透成功之后,客户端会把穿透对方返回的内容发送给服务器,服务器再返回):

客户端1使用netstat查看的网络连接

客户端2的运行结果

客户端2使用netstat查看的网络连接


可以看到客户端2对应的端口不同,那是因为电信NAT的问题,本地获取的Ip是电信10开头的内网地址,相当于在客户端2的上层还进行了一次中继。

最后附上完整的代码 http://download.csdn.net/detail/kiss1987f5/9763947

ps:由于没有对称型的NAT设备,无法做深入研究,对称型设备的端口太难猜测,穿透成功概率很小。
--------------------- 
作者:印链-杨霖 
来源:CSDN 
原文:https://blog.csdn.net/kiss1987f5/article/details/57084303 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值