p2p技术,又称为点对点(peer-to-peer),可以直接跨越NAT实现内网主机直接通讯。实现方式包括:中继(Relaying),逆向链接(Connection reversal), udp打洞(UDP hole punching)。中继方式是比较传统且效率较低的一种,不推荐使用,本人采用的是udp打洞,可以极大的降低服务器的压力,提高作业效率。
因为项目需要,开始研究起了p2p,之前对网络这块基本还是小白状态,首先,实现内网穿透,我们需要三台机器来搭建环境,一台处在内网的主机A,一台处在内网的主机B,一台处在公网的机器C。代码可参考:
代码转载自p2p代码实现,为了方便,我也把人家代码写在下面了。
注意点:
环境部分的影响因素:
1:部署位置。我们需要将服务端部署在公网环境中。
2:客户端与服务端的防火墙必须都处于关闭状态。
3:客户端与服务端的通信协议必须保持一致(ipv4与ipv6)。
4:保证网络稳定。
代码部分的影响因素:
1:内网主机穿透时必须是异步发起连接。
2:在穿透时,新建的连接,需要先设置SO_REUSEADDR,再绑定端口,最后进行连接,顺序不能错。
3:客户端获取本地ip地址时候,不能使用程序提供的封装方法:InetAddress.getLocalHost().getHostAddress(),需要手动设置改成实际的IP地址,因为有的机器可能有多个网卡或虚拟网卡,不该成当前实际的IP会导致使用错误的IP地址。(最重要的一点,深坑,这点会导致打洞失败!!!)
服务器端代码
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(
"192.168.1.88", localPort)); //实际的IP地址
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() + " 出错");
}
}
}
使用方法:
1、在服务器启动Server。
2、在客户端1启动Client,输入notwait命令,等待服务器通知打洞。
3、在客户端2启动Client,输入conn命令,然后输入服务器返回的客户端1的外网ip和端口,接下来就会自动完成连接。
好啦,如果以上各项都能保证,那么穿透应该是没有什么问题了。希望可以帮助到有需要的小伙伴。