PS: 此次是UDP打洞简单实现的讲解以及进一步的尝试,仅在Cone NAT下可打洞成功
UDP打洞能干啥?
- 通信
- 跨过防火墙(开放端口)
- 跨过局域网(Cone NAT)
- 互相聊天(传递数据)
Server S1 Server S2
18.181.0.31:1235 138.76.29.7:1235
| |
| |
+----------------------+----------------------+
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 155.99.25.11:62000 v | v 155.99.25.11:62000 v
|
Cone NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
|
Client A
10.0.0.1:1234
说明:客户端A建立了两个连续的对外会话,从相同的内部端点(10.0.0.1:1234)到两个不同的外部服务端S1和S2。 Cone NAT只为两个会话映射了一个公网端点(155.99.25.11:62000), 确保客户端端口的“身份”在地址转换的时候保持不变。 由于基本NAT和防火墙都不改变数据包的端口号,因此这些类型的中间件也可以看作是退化的Cone NAT。
Server S1 Server S2
18.181.0.31:1235 138.76.29.7:1235
| |
| |
+----------------------+----------------------+
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 155.99.25.11:62000 v | v 155.99.25.11:62001 v
|
Symmetric NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
|
Client A
10.0.0.1:1234
Server S
|
|
+----------------------+----------------------+
| |
NAT A NAT B
| |
| |
Client A Client B
Server S
18.181.0.31:1235
|
|
+----------------------+----------------------+
| |
NAT A |
155.99.25.11:62000 |
| |
| |
Client A Client B
10.0.0.1:1234 138.76.29.7:1234
Server S
18.181.0.31:1234
|
|
NAT
A-S 155.99.25.11:62000
B-S 155.99.25.11:62001
|
+----------------------+----------------------+
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
Server S
18.181.0.31:1234
|
|
+----------------------+----------------------+
| |
NAT A NAT B
155.99.25.11:62000 138.76.29.7:31000
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
Server S
18.181.0.31:1234
|
|
NAT X
A-S 155.99.25.11:62000
B-S 155.99.25.11:62001
|
|
+----------------------+----------------------+
| |
NAT A NAT B
192.168.1.1:30000 192.168.1.2:31000
| |
| |
Client A Client B
10.0.0.1:1234 10.1.1.3:1234
不同NAT工作过程及举例: 点我直通车
NAT类型及对应穿透讲解: 点我直通车
介绍STUN穿透NAT的方法: 点我直通车
一个UDP打洞例子(内含测试NAT类型的代码): 点我直通车
需要打开相应端口[以下Windows演示]
PS:路由器和防火墙的UDP打洞的端口有个时间限制的,在一定时间内如果没有数据通讯会自动关闭
三种实现方法,此篇文章讨论实现第一种
这个过程中,需要具有公网地址的serverS在中间。一旦clientA和clientB能够直接通信后,双方就能为其它client端充当server的角色了。
回环传输(loopback translation)
。NAT设备要支持这个功能才行。说明:使用UDP打洞技术的限制在于NAT设备必须能够保持端口绑定。即——[私有IP,私有UDP端口]对和[公网IP,公网UDP端口]对的一一对应。
分别为serverS、client1、client2的终端界面
说明:
设定奇数作为clientA,偶数作为clientB 数据只能一条一条的发,同样也只能一条一条的接收。
改进思路:将输入的内容单独存入缓冲区然后开线程来进行发送,并且开线程不断接收数据也存入缓冲区,这样应该显示的效果会更好。
public static void main(String[] args) throws IOException
{
System.out.println("server start");
#绑定端口
DatagramSocket server = new DatagramSocket(8002);
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
int count=0;
String count_S;
String clientA_IP = "",clientB_IP="";
String ClientA="",ClientB="";
int clientA_port = 0,clientB_port=0;
InetAddress clientA_address = null,clientB_address=null;
while(true)#循环监听
{
server.receive(packet);
String requestMessage = new String(packet.getData(), 0, packet.getLength());
System.out.println(requestMessage);
if(requestMessage.contains("Request ID")){
count++;#记录客户端数量
count_S=String.valueOf(count);
if(count%2!=0){
clientA_port = packet.getPort();
clientA_address = packet.getAddress();
ClientA=count_S+":" + clientA_address.getHostAddress() + ":" + clientA_port;
System.out.println("client "+ClientA);
clientA_IP = clientA_address.getHostAddress() + ":" + clientA_port;
sendID(count_S,clientA_port,clientA_address,server);#分配ID
}
else{
clientB_port = packet.getPort();
clientB_address = packet.getAddress();
ClientB=count_S+":" + clientB_address.getHostAddress() + ":" + clientB_port;
System.out.println("client "+ClientB);
clientB_IP = clientB_address.getHostAddress() + ":" + clientB_port;
sendID(count_S,clientB_port,clientB_address,server);#分配ID
try { #延时预防丢包问题
Thread.sleep(1000);#延时1秒
} catch (Exception e) {
System.out.println("Got an exception!");
}
#异步给新节点与当前已读节点发送对方的的节点信息
sendIP(ClientA, clientB_port, clientB_address, server);
sendIP(ClientB, clientA_port, clientA_address, server);
}
}
}
}
private static void sendID(String id, int port, InetAddress address, DatagramSocket server)
{#发送ID
byte[] sendBuf = id.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, address, port);
try {
server.send(sendPacket);
System.out.println("ID assignment successful!");
} catch (IOException e) {
e.printStackTrace();
}
}
private static void sendIP(String Client, int port, InetAddress address, DatagramSocket server)
{#发送节点信息
byte[] sendBuf = Client.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, address, port);
try {
server.send(sendPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
#发送请求
SocketAddress target = new InetSocketAddress("192.168.0.101", 8002);
DatagramSocket client = new DatagramSocket();
String message = "Request ID";
byte[] sendBuf = message.getBytes();
DatagramPacket pack = new DatagramPacket(sendBuf, sendBuf.length, target);
client.send(pack);
#获取分配ID
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
client.receive(packet);
String ID = new String(packet.getData(), 0, packet.getLength());
Name = ID;
System.out.println("本机被分配ID为:"+Name);
#与客户端进行UDP打洞
while(true){
client.receive(packet);
content = new String(packet.getData(), 0, packet.getLength());
if(content.contains(":"))break;
}
String[] str = content.split(":");#字符串转行为字符串数组
String Client_name = str[0];
String Client_address = str[1];
int Client_port = Integer.parseInt(str[2]);
String requestClient = "Hello, client"+Client_name+". I'm client"+Name;
sendBuf = requestClient.getBytes();
SocketAddress ToClient_address = new InetSocketAddress(Client_address, Client_port);
DatagramPacket sendPacket0 = new DatagramPacket(sendBuf, sendBuf.length, ToClient_address);
client.send(sendPacket0);
client.receive(packet);
String Client_Message = new String(packet.getData(), 0, packet.getLength());
System.out.println("接收消息:"+Client_Message);
client.send(sendPacket0);
System.out.println("成功打洞!");
client.receive(packet);#清空上一个包
#创建Scanner对象
#System.in表示标准化输出,也就是键盘输出
Scanner sc = new Scanner(System.in);
sc.useDelimiter("\n");#获取包含空格的字符串
while(true){
if(sc.hasNext()){#利用hasNextXXX()判断是否还有下一输入项
String Message = sc.next();#利用nextXXX()方法输出内容
sendBuf = Message.getBytes();
SocketAddress To_address = new InetSocketAddress(Client_address, Client_port);
DatagramPacket sendMessage = new DatagramPacket(sendBuf, sendBuf.length, To_address);
client.send(sendMessage);
}
client.receive(packet);
String R_Message = new String(packet.getData(), 0, packet.getLength());
System.out.println("接收消息:"+R_Message);
}
分别为client1、client2、client3的终端界面
说明:
与多客户端间的打洞成功,但是本人能力有限多人聊天却没有成功,有兴趣的尝试下呀~
public static void main(String[] args) throws IOException
{
System.out.println("server start");
#云服务器对应存储节点信息的列表地址
String dir="C:/Users/Administrator/Desktop/ListFolder";
String fileName="List.txt";
#绑定端口
DatagramSocket server = new DatagramSocket(8002);
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
int count=0;
String count_S;
String client_IP = "";
int client_port = 0;
InetAddress client_address = null;
while(true)#循环监听
{
server.receive(packet);
String requestMessage = new String(packet.getData(), 0, packet.getLength());
System.out.println(requestMessage);
if(requestMessage.contains("Request ID")){
#获取数据包内容为对应请求则判断为新节点的加入
File file = new File(dir,fileName);
if(count==0)file.createNewFile();#初始服务器需要构建节点列表
count++;#记录客户端数量
count_S=String.valueOf(count);
client_port = packet.getPort();
client_address = packet.getAddress();
String NewClient=count_S+":" + client_address.getHostAddress() + ":" + client_port;
System.out.println("client "+NewClient);
client_IP = client_address.getHostAddress() + ":" + client_port;
sendID(count_S,client_port,client_address,server);#分配ID给新节点
try { #延时预防丢包问题
Thread.sleep(1000);#延时1秒
} catch (Exception e) {
System.out.println("Got an exception!");
}
FileOutputStream fos = null;
fos = new FileOutputStream(file,true);#构建文件流,追加写
BufferedReader br = new BufferedReader(new FileReader(dir + File.separatorChar + fileName));
String line=null;
while((line=br.readLine())!=null) {#按行读取已存节点信息
String[] str = line.split(":");#根据":"分割节点信息
InetAddress address = InetAddress.getByName(str[1]); #获取已存节点的IP
int port=Integer.parseInt(str[2]);#获取已存节点的Port
String OldClient = line;
#异步给新节点与当前已读节点发送对方的的节点信息
sendIP(OldClient, client_port, client_address, server);
sendIP(NewClient, port, address,server);
try { #延时3秒,保证异步
Thread.sleep(3000);
} catch (Exception e) {
System.out.println("Got an exception!");
}
}
#给新节点发送Over表示新节点已加入P2P网络
sendIP("Over!", client_port, client_address, server);
System.out.println("NewClient shared successful!");
OutputStreamWriter os = new OutputStreamWriter(fos, "UTF-8");
PrintWriter pw=new PrintWriter(os);#文件写入流
pw.println(NewClient);#将新节点更新到节点列表中
pw.close();
}
}
}
private static void sendID(String id, int port, InetAddress address, DatagramSocket server)
{#发送ID
byte[] sendBuf = id.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, address, port);
try {
server.send(sendPacket);
System.out.println("ID assignment successful!");
} catch (IOException e) {
e.printStackTrace();
}
}
private static void sendIP(String NewClient, int port, InetAddress address, DatagramSocket server)
{#发送节点信息
byte[] sendBuf = NewClient.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendBuf, sendBuf.length, address, port);
try {
server.send(sendPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
String content ="";
String NewClient ="";
String Name = null;
String dir = "/Users/ling/Desktop";/** 要改*/
String FileName = "List.txt";
File file = new File(dir,FileName);
public void run(){
try {
#发送请求
SocketAddress target = new InetSocketAddress("120.53.16.130", 8002);/** 要改*/
DatagramSocket client = new DatagramSocket();
String message = "Request ID";
byte[] sendBuf = message.getBytes();
DatagramPacket pack = new DatagramPacket(sendBuf, sendBuf.length, target);
client.send(pack);
#获取分配ID
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
client.receive(packet);
String ID = new String(packet.getData(), 0, packet.getLength());
Name = ID;
System.out.println("本机被分配ID为:"+Name);
#与全部客户端进行UDP打洞
String flag="0";
while(true){
while(true){
client.receive(packet);
content = new String(packet.getData(), 0, packet.getLength());
if(content.contains(":"))break;
else if(content.equals("Over!")) {
flag="1";
break;
}
}
if(flag.equals("1"))break;
else{
String[] str = content.split(":");#字符串转行为字符串数组
String Client_name = str[0];
String Client_address = str[1];
int Client_port = Integer.parseInt(str[2]);
String requestClient = "Hello, client"+Client_name+". I'm client"+Name;
sendBuf = requestClient.getBytes();
SocketAddress ToClient_address = new InetSocketAddress(Client_address, Client_port);
DatagramPacket sendPacket0 = new DatagramPacket(sendBuf, sendBuf.length, ToClient_address);
client.send(sendPacket0);
client.receive(packet);
String Client_Message = new String(packet.getData(), 0, packet.getLength());
System.out.println("接收消息:"+Client_Message);
client.send(sendPacket0);
try {
FileOutputStream fos = null;
if(!file.exists()){
file.createNewFile();#如果文件不存在,就创建该文件
fos = new FileOutputStream(file);#首次写入获取
}else{
#如果文件已存在,那么就在文件末尾追加写入
fos = new FileOutputStream(file,true);#这里构造方法多了一个参数true,表示在文件末尾追加写入
}
OutputStreamWriter os = new OutputStreamWriter(fos, "UTF-8");#指定以UTF-8格式写入文件
PrintWriter pw=new PrintWriter(os);
pw.println(content);#每输入一个数据,自动换行,便于我们每一行每一行地进行读取
pw.close();
os.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("成功接入P2P网络!");
#循环监听是否有新节点加入并进行UDP打洞
while (true) {
try {
Thread.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
byte[] buf0 = new byte[1024];
DatagramPacket packet0 = new DatagramPacket(buf0, buf0.length);
while(true){
client.receive(packet0);
NewClient = new String(packet0.getData(), 0, packet0.getLength());
if(NewClient.contains(":"))break;
}
String[] NewInfo = NewClient.split(":");
String NewClient_name = NewInfo[0];
String NewClient_address = NewInfo[1];
int NewClient_port = Integer.parseInt(NewInfo[2]);
#应答
String requestNewClient = "Welcome, client"+NewClient_name+". I'm client"+Name;
sendBuf = requestNewClient.getBytes();
SocketAddress ToNewClient_address = new InetSocketAddress(NewClient_address, NewClient_port);
DatagramPacket sendPacket1 = new DatagramPacket(sendBuf, sendBuf.length, ToNewClient_address);
client.send(sendPacket1);#丢失
#接收回复
client.receive(packet);
String NewClient_Message = new String(packet.getData(), 0, packet.getLength());
System.out.println("接收消息:"+NewClient_Message);
client.send(sendPacket1);
System.out.println("与Client"+NewClient_name+"NAT穿透成功");
try {
FileOutputStream fos = null;
fos = new FileOutputStream(file,true);#这里构造方法多了一个参数true,表示在文件末尾追加写入
OutputStreamWriter os = new OutputStreamWriter(fos, "UTF-8");#指定以UTF-8格式写入文件
PrintWriter pw=new PrintWriter(os);
pw.println(NewClient);#每输入一个数据,自动换行,便于我们每一行每一行地进行读取
pw.close();
os.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
System.out.println(e);
}
}
网上查到的两种说法,不知道对错
偷摸放个资源: 仅供学习