如今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;
-
-
-
-
-
-
-
- public class Server {
-
- public static List connections = new ArrayList();
-
- public static void main(String[] args) {
- try {
-
- ServerSocket serverSocket = new ServerSocket(8888);
- Socket socket = null;
-
- int count = 0;
- System.out.println("***服务器即将启动,等待客户端的连接***");
-
- while (true) {
-
- socket = serverSocket.accept();
-
- ServerThread serverThread = new ServerThread(socket);
-
- serverThread.start();
-
- connections.add(serverThread);
-
- count++;
- System.out.println("客户端的数量:" + count);
- }
-
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
客户端代码:
服务端就是一个普通的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设备,无法做深入研究,对称型设备的端口太难猜测,穿透成功概率很小。