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

如今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方直连的拓扑网状结构网络。


服务器端代码:

[java]  view plain  copy
  1. package org.inchain.p2p;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.ServerSocket;  
  5. import java.net.Socket;  
  6. import java.util.ArrayList;  
  7. import java.util.List;  
  8.   
  9. /** 
  10.  * 外网端服务,穿透中继 
  11.  *  
  12.  * @author ln 
  13.  * 
  14.  */  
  15. public class Server {  
  16.   
  17.     public static List<ServerThread> connections = new ArrayList<ServerThread>();  
  18.   
  19.     public static void main(String[] args) {  
  20.         try {  
  21.             // 1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口  
  22.             ServerSocket serverSocket = new ServerSocket(8888);  
  23.             Socket socket = null;  
  24.             // 记录客户端的数量  
  25.             int count = 0;  
  26.             System.out.println("***服务器即将启动,等待客户端的连接***");  
  27.             // 循环监听等待客户端的连接  
  28.             while (true) {  
  29.                 // 调用accept()方法开始监听,等待客户端的连接  
  30.                 socket = serverSocket.accept();  
  31.                 // 创建一个新的线程  
  32.                 ServerThread serverThread = new ServerThread(socket);  
  33.                 // 启动线程  
  34.                 serverThread.start();  
  35.   
  36.                 connections.add(serverThread);  
  37.   
  38.                 count++;// 统计客户端的数量  
  39.                 System.out.println("客户端的数量:" + count);  
  40.             }  
  41.               
  42.         } catch (IOException e) {  
  43.             e.printStackTrace();  
  44.         }  
  45.     }  
  46. }  



[java]  view plain  copy
  1. package org.inchain.p2p;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.IOException;  
  5. import java.io.InputStreamReader;  
  6. import java.io.PrintWriter;  
  7. import java.net.InetAddress;  
  8. import java.net.Socket;  
  9.   
  10. /** 
  11.  * 外网端服务多线程处理内网端连接 
  12.  *  
  13.  * @author ln 
  14.  * 
  15.  */  
  16. public class ServerThread extends Thread {  
  17.     // 和本线程相关的Socket  
  18.     private Socket socket = null;  
  19.     private BufferedReader br = null;  
  20.     private PrintWriter pw = null;  
  21.       
  22.   
  23.     public ServerThread(Socket socket) throws IOException {  
  24.         this.socket = socket;  
  25.         this.br = new BufferedReader(new InputStreamReader(socket.getInputStream()));  
  26.         this.pw = new PrintWriter(socket.getOutputStream());  
  27.     }  
  28.   
  29.     // 线程执行的操作,响应客户端的请求  
  30.     public void run() {  
  31.   
  32.         InetAddress address = socket.getInetAddress();  
  33.         System.out.println("新连接,客户端的IP:" + address.getHostAddress() + " ,端口:" + socket.getPort());  
  34.   
  35.         try {  
  36.             pw.write("已有客户端列表:" + Server.connections + "\n");  
  37.   
  38.             // 获取输入流,并读取客户端信息  
  39.             String info = null;  
  40.               
  41.             while ((info = br.readLine()) != null) {  
  42.                 // 循环读取客户端的信息  
  43.                 System.out.println("我是服务器,客户端说:" + info);  
  44.   
  45.                 if (info.startsWith("newConn_")) {  
  46.                     //接收到穿透消息,通知目标节点  
  47.                     String[] infos = info.split("_");  
  48.                     //目标节点的外网ip地址  
  49.                     String ip = infos[1];  
  50.                     //目标节点的外网端口  
  51.                     String port = infos[2];  
  52.                       
  53.                     System.out.println("打洞到 " + ip + ":" + port);  
  54.                       
  55.                     for (ServerThread server : Server.connections) {  
  56.                         if (server.socket.getInetAddress().getHostAddress().equals(ip)  
  57.                                 && server.socket.getPort() == Integer.parseInt(port)) {  
  58.                               
  59.                             //发送命令通知目标节点进行穿透连接  
  60.                             server.pw.write("autoConn_" + socket.getInetAddress().getHostAddress() + "_" + socket.getPort()  
  61.                                     + "\n");  
  62.                             server.pw.flush();  
  63.                               
  64.                             break;  
  65.                         }  
  66.                     }  
  67.                 } else {  
  68.                     // 获取输出流,响应客户端的请求  
  69.                     pw.write("欢迎您!" + info + "\n");  
  70.                     // 调用flush()方法将缓冲输出  
  71.                     pw.flush();  
  72.                 }  
  73.                   
  74.             }  
  75.         } catch (Exception e) {  
  76.             e.printStackTrace();  
  77.         } finally {  
  78.             System.out.println("客户端关闭:" + address.getHostAddress() + " ,端口:" + socket.getPort());  
  79.             Server.connections.remove(this);  
  80.             // 关闭资源  
  81.             try {  
  82.                 if (pw != null) {  
  83.                     pw.close();  
  84.                 }  
  85.                 if (br != null) {  
  86.                     br.close();  
  87.                 }  
  88.                 if (socket != null) {  
  89.                     socket.close();  
  90.                 }  
  91.             } catch (IOException e) {  
  92.                 e.printStackTrace();  
  93.             }  
  94.         }  
  95.     }  
  96.   
  97.     @Override  
  98.     public String toString() {  
  99.         return "ServerThread [socket=" + socket + "]";  
  100.     }  
  101. }  

客户端代码:

[java]  view plain  copy
  1. package org.inchain.p2p;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.IOException;  
  5. import java.io.InputStreamReader;  
  6. import java.io.PrintWriter;  
  7. import java.net.InetAddress;  
  8. import java.net.InetSocketAddress;  
  9. import java.net.Socket;  
  10. import java.util.Scanner;  
  11.   
  12. /** 
  13.  * 内网客户端,要进行穿透的内网服务 
  14.  *  
  15.  * @author ln 
  16.  * 
  17.  */  
  18. public class Client {  
  19.       
  20.     //输入scanner  
  21.     private Scanner scanner = new Scanner(System.in);  
  22.     //是否等待输入  
  23.     private boolean isWaitInput = true;  
  24.     //首次与外网主机通信的连接  
  25.     private Socket socket;  
  26.     //首次与外网主机通信的本地端口  
  27.     private int localPort;  
  28.   
  29.     private PrintWriter pw;  
  30.     private BufferedReader br;  
  31.       
  32.     public static void main(String[] args) {  
  33.         new Client().start();  
  34.     }  
  35.       
  36.     public void start() {  
  37.         try {  
  38.             // 新建一个socket通道  
  39.             socket = new Socket();  
  40.             // 设置reuseAddress为true  
  41.             socket.setReuseAddress(true);  
  42.   
  43.             //TODO在此输入外网地址和端口  
  44.             String ip = "xxx.xxxx.xxxx.xxxx";  
  45.             int port = 8888;  
  46.             socket.connect(new InetSocketAddress(ip, port));  
  47.               
  48.             //首次与外网服务器通信的端口  
  49.             //这就意味着我们内网服务要与其他内网主机通信,就可以利用这个通道  
  50.             localPort = socket.getLocalPort();  
  51.   
  52.             System.out.println("本地端口:" + localPort);  
  53.             System.out.println("请输入命令 notwait等待穿透,或者输入conn进行穿透");  
  54.   
  55.             pw = new PrintWriter(socket.getOutputStream());  
  56.             br = new BufferedReader(new InputStreamReader(socket.getInputStream()));  
  57.               
  58.             try {  
  59.                 while (true) {  
  60.                     if(process()) {  
  61.                         break;  
  62.                     }  
  63.                 }  
  64.             } finally {  
  65.                 // 关闭资源  
  66.                 try {  
  67.                     if(pw != null) {  
  68.                         pw.close();  
  69.                     }  
  70.                     if (br != null) {  
  71.                         br.close();  
  72.                     }  
  73.                     if (socket != null) {  
  74.                         socket.close();  
  75.                     }  
  76.                 } catch (IOException e) {  
  77.                     e.printStackTrace();  
  78.                 }  
  79.             }  
  80.         } catch (Exception e) {  
  81.             e.printStackTrace();  
  82.         }  
  83.     }  
  84.   
  85.     /* 
  86.      * 处理与服务器连接的交互,返回是否退出 
  87.      */  
  88.     private boolean process() throws IOException {  
  89.           
  90.         String in = null;  
  91.           
  92.         if (isWaitInput) {  
  93.             //把输入的命令发往服务端  
  94.             in = scanner.next();  
  95.             pw.write(in + "\n");  
  96.               
  97.             //调用flush()方法将缓冲输出  
  98.             pw.flush();  
  99.               
  100.             if ("notwait".equals(in)) {  
  101.                 isWaitInput = false;  
  102.             }  
  103.         }  
  104.         //获取服务器的响应信息  
  105.         String info = br.readLine();  
  106.         if(info != null) {  
  107.             System.out.println("我是客户端,服务器说:" + info);  
  108.         }  
  109.         //处理本地命令  
  110.         processLocalCommand(in);  
  111.           
  112.         //处理服务器命令  
  113.         processRemoteCommand(info);  
  114.           
  115.         return "exit".equals(in);  
  116.     }  
  117.   
  118.     private void processRemoteCommand(String info) throws IOException {  
  119.         if (info != null && info.startsWith("autoConn_")) {  
  120.               
  121.             System.out.println("服务器端返回的打洞命令,自动连接目标");  
  122.               
  123.             String[] infos = info.split("_");  
  124.             //目标外网地址  
  125.             String ip = infos[1];  
  126.             //目标外网端口  
  127.             String port = infos[2];  
  128.               
  129.             doPenetration(ip, Integer.parseInt(port));  
  130.         }  
  131.     }  
  132.   
  133.     private void processLocalCommand(String in) throws IOException {  
  134.         if ("conn".equals(in)) {  
  135.             System.out.println("请输入要连接的目标外网ip:");  
  136.             String ip = scanner.next();  
  137.             System.out.println("请输入要连接的目标外网端口:");  
  138.             int port = scanner.nextInt();  
  139.   
  140.             pw.write("newConn_" + ip + "_" + port + "\n");  
  141.             pw.flush();  
  142.   
  143.             doPenetration(ip, port);  
  144.               
  145.             isWaitInput = false;  
  146.         }  
  147.     }  
  148.   
  149.     /* 
  150.      * 对目标服务器进行穿透 
  151.      */  
  152.     private void doPenetration(String ip, int port) {  
  153.         try {  
  154.             //异步对目标发起连接  
  155.             new Thread() {  
  156.                 public void run() {  
  157.                     try {  
  158.   
  159.                         Socket newsocket = new Socket();  
  160.   
  161.                         newsocket.setReuseAddress(true);  
  162.                         newsocket.bind(new InetSocketAddress(  
  163.                                 InetAddress.getLocalHost().getHostAddress(), localPort));  
  164.   
  165.                         System.out.println("connect to " + new InetSocketAddress(ip, port));  
  166.                           
  167.                         newsocket.connect(new InetSocketAddress(ip, port));  
  168.                           
  169.                         System.out.println("connect success");  
  170.   
  171.                         BufferedReader b = new BufferedReader(  
  172.                                 new InputStreamReader(newsocket.getInputStream()));  
  173.                         PrintWriter p = new PrintWriter(newsocket.getOutputStream());  
  174.                           
  175.                         while (true) {  
  176.                               
  177.                             p.write("hello " + System.currentTimeMillis() + "\n");  
  178.                             p.flush();  
  179.                               
  180.                             String message = b.readLine();  
  181.                               
  182.                             System.out.println(message);  
  183.                               
  184.                             pw.write(message + "\n");  
  185.                             pw.flush();  
  186.                               
  187.                             if("exit".equals(message)) {  
  188.                                 break;  
  189.                             }  
  190.                               
  191.                             Thread.sleep(1000l);  
  192.                         }  
  193.                           
  194.                         b.close();  
  195.                         p.close();  
  196.                         newsocket.close();  
  197.                     } catch (Exception e) {  
  198.                         e.printStackTrace();  
  199.                     }  
  200.                 }  
  201.             }.start();  
  202.               
  203. //          //监听本地端口  
  204. //          ServerSocket serverSocket = new ServerSocket();  
  205. //          serverSocket.setReuseAddress(true);  
  206. //          serverSocket.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), localPort));  
  207. //  
  208. //          // 记录客户端的数量  
  209. //          System.out.println("******开始监听端口:" + localPort);  
  210. //          // 循环监听等待客户端的连接  
  211. //          // 调用accept()方法开始监听,等待客户端的连接  
  212. //          Socket st = serverSocket.accept();  
  213. //            
  214. //          System.out.println("成功了,哈哈,新的连接:" + st.getInetAddress().getHostAddress() + ":" + st.getPort());  
  215. //            
  216. //          serverSocket.close();  
  217.         } catch (Exception e) {  
  218.             e.printStackTrace();  
  219.             System.out.println("监听端口 " + socket.getLocalPort() + " 出错");  
  220.         }  
  221.     }  
  222. }  

服务端就是一个普通的socket服务,没什么特别的地方。

客户端需要注意的是:

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

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

[java]  view plain  copy
  1.                                           Socket newsocket = new Socket(ip, port);  
  2.   
  3. newsocket.setReuseAddress(true);  
  4. newsocket.bind(new InetSocketAddress(  
  5.         InetAddress.getLocalHost().getHostAddress(), localPort));  
  6.   
  7. System.out.println("connect to " + new InetSocketAddress(ip, port));  
  8.   
  9. 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的上层还进行了一次中继。


ps:由于没有对称型的NAT设备,无法做深入研究,对称型设备的端口太难猜测,穿透成功概率很小。
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值