目录
1.内网穿透概论
本人不喜欢长篇大论的讲一些学术用语,最后看的云里雾里的。就用最简单的白话文大概讲一下内网穿透的原理和作用。
1.1 公网IP端口,NAT转发,内网IP端口之间的关系
要了解和使用内网穿透,首先要明白他们之间是如何交换消息的。
举个例子:在网页上输入www.baidu.com 就可以打开百度,这是因为知道了域名,域名在DNS服务器上映射到了对应的公网IP,域名是和ip地址绑定的,访问www.xxx.com 等于访问 公网IP,可以使用cmd的ping命令去看返回ip地址。
当我们访问了www.baidu.com ,百度的服务器需要知道你的公网ip和端口才能把信息返回给你(最终结果就是浏览器上显示百度页面)
浏览器输入www.baidu.com到返回百度页面 整个过程的路径
内网 IP:端口 -> nat(转发)网线的公网 IP:端口 -> 百度服务器公网 IP:端口 ->nat(转发) 网线的公网 IP:端口 -> 内网 IP:端口
其中 内网 和 nat(转发) 每一次请求都会刷新端口 , 内网IP(在cmd 中输入 ipconfig /all 可以查看的ipv4地址 ) 如下图:
了解网络传输协议的应该都知道 互联网上进行信息传输 需要目标地址,端口和响应地址,端口
这里涉及到一个知识就是nat,因为ipv4地址是有限的,现在运营商的网线大部分是一个公网对应一个区域的用户,简单来说就是你和其他人一起使用一个公网ip。
nat起到一个转发作用,所以你在本地电脑上的地址和端口是不能被外网用户访问的,但是可以被同一个公网ip下的用户访问。可以这样理解一个公网ip对应一个nat。
那么为什么要进行udp打洞呢?因为需要和nat外用户进行信息交换。
前面的内容只是介绍一下大概,下面是重点(仔细看)
1.2 NAT转发和UDP打洞原理
当两个内网A,B想要互相通信怎么办呢?首先需要知道A,B的nat地址和端口
每次发送信息到百度服务器返回的地址其实是nat的地址和端口,然后由nat转发给你的内网地址和端口并生成记录,记录类似于: (nat地址:端口——内网地址:端口)。
nat地址端口和内网地址端口 1对1绑定了(可以画上等于号,每次调用端口都会刷新)
因为A,B都是内网,无法直接通信,所以需要一个公网服务器 来记录A,B的nat地址和端口。
然后让A向公网服务器的地址发UDP消息,这样A的NAT上就会留下一条记录 A IP:端口 ——natA 公网IP:端口。
让B向公网服务器的地址发UDP消息,这样B的NAT上就会留下一条记录 B IP:端口 ——natB公网IP:端口。
这样A,B的nat地址和端口都知道了,然后在让A,B互相发送UDP请求 在nat中留下记录 这样 A,B之间就可以通过UDP进行通信了。
重点!!!使用java new DatagramSocket() 一定要使用单例模式,千万不要关闭不然每次nat映射的端口都不一样 ,nat在变化就无法获取正确的地址了 之前踩坑过
1.3 核心代码
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static spike.controller.CheckSyncStateController.doGet;
/**
* @PACKAGE_NAME: spike.schedule
* @NAME: NatSchedule
* @USER: spike
* @DATE: 2023/5/12 10:27
* @PROJECT_NAME: Springcolud_Spike
*/
@Slf4j
@Component
public class NatSchedule {
@Value("${tarurl}")
private String TAR_URL;
@Value("${isserver}")
private boolean IS_SERVER;
@Value("${key}")
private String KEY;
@Value("${sendport}")
private Integer SEND_PORT;
@Value("${receiveport}")
private Integer RECEIVE_PORT;
public static DatagramSocket RECEIVE_SOCKET = null;
public static DatagramSocket SEND_SOCKET = null;
public static Map<String, Object> NAT_MAP = new HashMap<>();
//每1分钟执行一次
@PostConstruct
@Scheduled(cron = "0/60 * * * * ?")
private void SendUdp() throws IOException {
sendUpdMsg();
}
//每1秒钟执行一次
@Scheduled(cron = "*/1 * * * * ?")
private void receiveUpd() throws IOException {
receiveUpdMsg();
}
public void sendUpdMsg() throws IOException {
if (!IS_SERVER) {
if (SEND_SOCKET == null) {
//清除key值
doGet("http://" + TAR_URL + "/Nat/clearNatMap?key=" + KEY);
SEND_SOCKET = new DatagramSocket(SEND_PORT);
Thread thread = new Thread() {
@SneakyThrows
@Override
public void run() {
log.info("开始接收upd请求");
responseSend();
}
};
thread.start();
}
sendUpdToServer();
}
}
private void responseSend() throws IOException {
byte[] bytes = new byte[1024];
DatagramPacket packet1 = new DatagramPacket(bytes, bytes.length);
while (true) {
SEND_SOCKET.receive(packet1);
String receive = new String(bytes, 0, packet1.getLength(), "utf-8");
if (receive.contains("client")) {
String msg = "****************************打洞成功****************************";
log.info(msg);
}else {
log.info("*************************** upd信息开始 ***************************\n {}", receive);
log.info("*************************** upd信息结束 ***************************");
}
}
}
private void sendUpdToServer() throws IOException {
String ipV4 = InetAddress.getLocalHost().getHostAddress();
String[] ipArr = TAR_URL.split(":");
// 客户段发送upd请求到服务端(服务端记录 客户端内网ip/外网ip/外网端口)
String text = KEY + ":" + ipV4 + ":" + SEND_SOCKET.getLocalPort();
byte[] buf = text.getBytes();
sendUdp(buf, ipArr[0], RECEIVE_PORT, SEND_SOCKET);
//查询 相同key的两个客户段nat信息
String result = doGet("http://" + TAR_URL + "/Nat/getNatMap?key=" + KEY);
if (result.isEmpty()) {
return;
}
List<String> list = JSON.parseObject(result, List.class);
// 相同key 需要nat穿透的ip数组
log.info("键值:{} 下穿透ip数组 {}", KEY, JSON.toJSONString(list));
String client = "";
for (String str : list) {
if (str == null || str.isEmpty()) {
continue;
}
String[] strArr = str.split(":");
if (strArr[0].equals(ipV4)) {
client = str;
}
if (!strArr[0].equals(ipV4)) {
//upd打洞
String[] udpArr = str.split("/")[1].split(":");
// 2,明确要发送的具体数据。
String clientMsg = "来自 client " + client + " upd 信息";
byte[] data = clientMsg.getBytes();
sendUdp(data, udpArr[0], Integer.valueOf(udpArr[1]), SEND_SOCKET);
}
}
}
public static void sendUdp(byte[] data, String ip, int port, DatagramSocket datagramSocket) throws IOException {
// 3,将数据封装成了数据包。
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName(ip), port);
log.info("向服务端" + ip + ":" + port + "发送upd 信息");
datagramSocket.send(packet);
}
/**
* 服务端接收upd请求
*
* @throws IOException
*/
public void receiveUpdMsg() throws IOException {
if (IS_SERVER) {
//创建一个数据包,用于接收数据
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
//DatagramSocket(int port)构造数据报套接字并将其绑定到本地主机上的指定端口
if (RECEIVE_SOCKET == null) {
RECEIVE_SOCKET = new DatagramSocket(RECEIVE_PORT);
}
DatagramSocket ds = RECEIVE_SOCKET;
//调用DatagramSocket对象的方法接收数据
ds.receive(dp);
InetAddress address = dp.getAddress();
int port = dp.getPort();
String url = address.getHostAddress() + ":" + port;
//解析数据包,并把数据在控制台显示
byte[] data = dp.getData();
//int getLength()返回要发送的数据的长度或接收到的数据的长度
int length = dp.getLength();
String dataString = new String(data, 0, length);
log.info("\n客户端Nat地址: **** {}**** \n客户端内网地址: **** {} **** ", url, dataString);
String[] clientArr = dataString.split(":");
if (IS_SERVER) {
String key = clientArr[0];
putNatMap(key, clientArr[1] + ":" + clientArr[2] + "/" + url);
log.info("key :{} 映射数组更新完成", KEY);
}
}
}
private void putNatMap(String key, String value) {
if (NAT_MAP.get(KEY) == null) {
List<String> tempArr = new ArrayList<>();
tempArr.add(value);
NAT_MAP.put(key, tempArr);
} else {
List<String> tempArr = (List<String>) NAT_MAP.get(key);
String refresh = null;
for (String str : tempArr) {
String uuid = StringUtils.substringBeforeLast(str, ":");
String uuidValue = StringUtils.substringBeforeLast(value, ":");
if (uuid.equals(uuidValue)) {
refresh = str;
break;
}
}
if (refresh != null) {
tempArr.remove(refresh);
}
tempArr.add(value);
NAT_MAP.put(key, tempArr);
}
}
}
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import spike.schedule.NatSchedule;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import static spike.schedule.NatSchedule.RECEIVE_SOCKET;
import static spike.schedule.NatSchedule.SEND_SOCKET;
import static spike.schedule.NatSchedule.sendUdp;
/**
* @PACKAGE_NAME: com.example.controller
* @NAME: CheckSyncState
* @USER: spike
* @DATE: 2023/5/8 8:57
* @PROJECT_NAME: SendAlertMsg
*/
@Slf4j
@RestController
@RequestMapping("Nat")
public class CheckSyncStateController {
@Value("${isserver}")
private boolean IS_SERVER;
/**
* 查询NAT_MAP
*/
@ApiOperation(value = "查询NAT_MAP", tags = "Nat Upd打洞")
@RequestMapping(value = "/getNatMap", method = RequestMethod.GET)
public Object getSyncState(@RequestParam(required = false, value = "key") String key) {
if (!key.isEmpty()) {
return NatSchedule.NAT_MAP.get(key);
}
return NatSchedule.NAT_MAP;
}
/**
* 清除指定NAT_MAP key
*/
@ApiOperation(value = "清除指定NAT_MAP key", tags = "Nat Upd打洞")
@RequestMapping(value = "/clearNatMap", method = RequestMethod.GET)
public Object clearNatMap(@RequestParam(required = false, value = "key") String key) {
if (NatSchedule.NAT_MAP.get(key) != null) {
NatSchedule.NAT_MAP.put(key, null);
return NatSchedule.NAT_MAP.get(key);
}
return NatSchedule.NAT_MAP.get(key);
}
/**
* 请求
*/
@ApiOperation(value = "udp打洞", tags = "CheckSyncStateController 检查同步状态")
@RequestMapping(value = "/sendToUdp", method = RequestMethod.GET)
public String sendToUdp(@RequestParam(value = "key") String key, @RequestParam(value = "client_ip") String client_ip, @RequestParam(value = "client_port") Integer client_port) throws IOException {
DatagramSocket datagramSocket;
if (IS_SERVER) {
datagramSocket = RECEIVE_SOCKET;
} else {
datagramSocket = SEND_SOCKET;
}
sendUdp(key.getBytes(), client_ip, client_port, datagramSocket);
log.info("GET接口向服务端 {}:{} 发送upd 信息:{}", client_ip, client_port, key);
return "打洞完成";
}
public static String doGet(String httpurl) {
HttpURLConnection connection = null;
InputStream is = null;
BufferedReader br = null;
String result = null;// 返回结果字符串
try {
// 创建远程url连接对象
URL url = new URL(httpurl);
// 通过远程url连接对象打开一个连接,强转成httpURLConnection类
connection = (HttpURLConnection) url.openConnection();
// 设置连接方式:get
connection.setRequestMethod("GET");
// 设置连接主机服务器的超时时间:15000毫秒
connection.setConnectTimeout(15000);
// 设置读取远程返回的数据时间:60000毫秒
connection.setReadTimeout(60000);
// 发送请求
connection.connect();
// 通过connection连接,获取输入流
if (connection.getResponseCode() == 200) {
is = connection.getInputStream();
// 封装输入流is,并指定字符集
br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
// 存放数据
StringBuffer sbf = new StringBuffer();
String temp = null;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != is) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
connection.disconnect();// 关闭远程连接
}
return result;
}
}
server:
port: 5001
#服务端公网地址
tarurl: xx.xxx.xx.xxx:5001
#是否服务器
isserver: false
#客户端连接唯一键值
key: 自己定义一个uuid
sendport: 5001
receiveport : 7091
nat内网穿透获取连接服务器的map
http://服务器地址:5001/Nat/getNatMap?key=定义的唯一uuid
返回示例:
[
"192.168.0.117:50979/x11.xx2.89.xx2:15623"
]
upd发送消息接口
http://内网IPV4:5001/Nat/sendToUdp?client_ip=目标公网ip&client_port=目标的nat端口&key=发送内容
返回示例:
打洞完成
结果展示: