NAT穿透解决方案介绍(转)--Java ICE实现
转:http://www.cnblogs.com/javaminer/p/3575282.html
最近公司要实现在各种网络环境下面的多屏互动(机顶盒、android phone、iphone及PC端)的需求;由于IP地址资源有限的原因,目前我们使用的各种终端设备都位于局域网后面也就是多台设备共享同一个公网IP;例如:如果位于局域网里面的一个终端Agent A要与互联网上的另一个终端Agent B通信,当A发送的data packet经过局域网出口处的NAT设备时,NAT会将data packet里面的source address字段替换成相应的公网IP和Port,然后再发送data packet到Agent B。Agent B看到的source address就是经过转换后的IP和Port并不知道Agent A的局域网地址;当Agent B的响应到达Agent A的NAT设备后,NAT设备查找内存中保存的和这个外网地址相对应的内网地址,如果找到后就将这个data packet转发到这个地址,这样就实现了通信。
然而由于目前存在着各种不同类型的NAT设备对NAT有着不同的实现方式(将内外地址映射成外网地址的时候有着不同的行为方式),这就给NAT的穿透带来了麻烦;目前主要的NAT类型有如下几种:
1)Full-cone NAT, also known as one-to-one NAT
- 一旦一个内网地址 (iAddr:iPort) 被映射到一个外部地址 (eAddr:ePort), 来自 iAddr:iPort 的任何数据包将通过 eAddr:ePort 发送.
- 任何外部主机能够通过eAddr:ePort这个地址发送数据包到iAddr:iPort.
2)Address-restricted-cone NAT
- 一旦一个内网地址 (iAddr:iPort) 被映射到一个外部地址 (eAddr:ePort), 来自 iAddr:iPort 的任何数据包将通过 eAddr:ePort 发送.
- 仅只有接收到主机(iAddr:iPort)通过eAddr:ePort发送的数据包的外部主机通过该主机的任何端口发送到eAddr:ePort的数据包才能够被正确的转发到iAddr:iPort.也就是说主机有关端口无关.
3)Port-restricted cone NAT
类似于address restricted cone NAT, 但是端口号有限制.
- 一旦一个内网地址 (iAddr:iPort) 被映射到一个外部地址 (eAddr:ePort), 来自 iAddr:iPort 的任何数据包将通过 eAddr:ePort 发送.
- 仅只有接收到主机(iAddr:iPort)通过eAddr:ePort发送的数据包的外部主机通过该主机的相同端口发送到eAddr:ePort的数据包才能够被正确的转发到iAddr:iPort.
4)Symmetric NAT
- 来自相同内部ip和port发送到相同目的地ip和port的请求被映射到唯一的外部ip和port地址;如果相同的内部主机采用相同的ip和port地址发送到不同的目的地,那么重新分配映射地址。
- 只有先前收到内部主机发送的包的外部主机才能够发送返回包到内部主机。
针对前面三种NAT类型(即cone NAT)只要通信双方彼此知道对方的内部地址和外部地址的映射关系,然后通过UDP打洞的方式就可以建立相互连接的通信;但是第四种也就是Symmetric NAT的话由于每次向不同目的地发送数据包时采用不同的外部地址,也就没办法通过直接的方式建立P2P连接。
1.各种网络环境下的P2P通信解决方法:
(3)如果通信双方一方拥有独立的公网地址另一方在NAT后面,那么可以由位于NAT后面的一方主动发起通信请求;
(4)如果通信双方都位于NAT后面,且双方的NAT类型都是cone NAT,那么可以通过一个STUN服务器发现自己的NAT类型以及内网和外网传输地址映射信息,然后通过Signaling(信令服务器,实现了SIP协议的主机)交换彼此的NAT类型及内网和外网传输地址映射信息,然后通过UDP打洞的方式建立通信连接;
2.协议及用到的相关技术介绍:
v=0
o=ice4j.org 0 0 IN IP4 192.168.106.215
s=-
t=0 0
a=ice-options:trickle
a=ice-ufrag:bc01a
a=ice-pwd:1boove7ehnpo1lqho7unefni36
m=audio 3030 RTP/AVP 0
c=IN 192.168.106.215 IP4
a=mid:audio
a=candidate:1 1 udp 2130706431 192.168.106.215 3030 typ host
a=candidate:2 1 udp 1694498815 121.15.130.xxx 64923 typ srflx raddr 192.168.106.215 rport 3030
STUN(Session Traversal Utilities for NAT)
NAT会话穿透工具;STUN提供了一种方式使一个端点能够确定NAT分配的和本地私有IP地址和端口相对应的公网IP地址和端口以及NAT的类型信息。它也为端点提供了一种方式保持一个NAT绑定不过期。NAT绑定过期则表示为相同的内网地址重新分配外网地址也就是端口号。
TURN(Traversal Using Relay NAT)
TURN是STUN协议的扩展,在实际应用中他也可以充当STUN的角色;如果一个位于NAT后面的设备想要和另外一个位于NAT后面的设备建立通信,当采用UDP打洞技术不能改实现的时候就必须要一台中间服务器扮演数据包转发的角色,这台TURN服务器需要拥有公网的IP地址;
ICE(Interactive Connectivity Establishment)
是实现NAT穿透的一种技术方案;ICE是一种NAT穿透技术,通过offer/answer模型建立基于UDP的媒介流。ICE是offer/answer模型的扩展,通过在offer和answer的SDP里面包含多种IP地址和端口,然后对本地SDP和远程SDP里面的IP地址进行配对,然后通过P2P连通性检查进行连通性测试工作,如果测试通过即表明该传输地址对可以建立连接。其中IP地址和端口(也就是地址)有以下几种:本机地址、通过STUN服务器反射后获取的server-reflexive地址(内网地址被NAT映射后的地址)、relayed地址(和TURN转发服务器相对应的地址)及Peer reflexive地址等。
/**
* Copyright (c) 2014 All Rights Reserved.
* TODO
*/ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.List; import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.ice4j.Transport;
import org.ice4j.TransportAddress;
import org.ice4j.ice.Agent;
import org.ice4j.ice.Component;
import org.ice4j.ice.IceMediaStream;
import org.ice4j.ice.IceProcessingState;
import org.ice4j.ice.LocalCandidate;
import org.ice4j.ice.NominationStrategy;
import org.ice4j.ice.RemoteCandidate;
import org.ice4j.ice.harvest.StunCandidateHarvester;
import org.ice4j.ice.harvest.TurnCandidateHarvester;
import org.ice4j.security.LongTermCredential; import test.SdpUtils; public class IceClient { private int port; private String streamName; private Agent agent; private String localSdp; private String remoteSdp; private String[] turnServers = new String[] { "stun.jitsi.net:3478" }; private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" }; private String username = "guest"; private String password = "anonymouspower!!"; private IceProcessingListener listener; static Logger log = Logger.getLogger(IceClient.class); public IceClient(int port, String streamName) {
this.port = port;
this.streamName = streamName;
this.listener = new IceProcessingListener();
} public void init() throws Throwable { agent = createAgent(port, streamName); agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO); agent.addStateChangeListener(listener); agent.setControlling(false); agent.setTa(10000); localSdp = SdpUtils.createSDPDescription(agent); log.info("=================== feed the following"
+ " to the remote agent ==================="); System.out.println(localSdp); log.info("======================================"
+ "========================================\n");
} public DatagramSocket getDatagramSocket() throws Throwable { LocalCandidate localCandidate = agent
.getSelectedLocalCandidate(streamName); IceMediaStream stream = agent.getStream(streamName);
List<Component> components = stream.getComponents();
for (Component c : components) {
log.info(c);
}
log.info(localCandidate.toString());
LocalCandidate candidate = (LocalCandidate) localCandidate;
return candidate.getDatagramSocket(); } public SocketAddress getRemotePeerSocketAddress() {
RemoteCandidate remoteCandidate = agent
.getSelectedRemoteCandidate(streamName);
log.info("Remote candinate transport address:"
+ remoteCandidate.getTransportAddress());
log.info("Remote candinate host address:"
+ remoteCandidate.getHostAddress());
log.info("Remote candinate mapped address:"
+ remoteCandidate.getMappedAddress());
log.info("Remote candinate relayed address:"
+ remoteCandidate.getRelayedAddress());
log.info("Remote candinate reflexive address:"
+ remoteCandidate.getReflexiveAddress());
return remoteCandidate.getTransportAddress();
} /**
* Reads an SDP description from the standard input.In production
* environment that we can exchange SDP with peer through signaling
* server(SIP server)
*/
public void exchangeSdpWithPeer() throws Throwable {
log.info("Paste remote SDP here. Enter an empty line to proceed:");
BufferedReader reader = new BufferedReader(new InputStreamReader(
System.in)); StringBuilder buff = new StringBuilder();
String line = new String(); while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.length() == 0) {
break;
}
buff.append(line);
buff.append("\r\n");
} remoteSdp = buff.toString(); SdpUtils.parseSDP(agent, remoteSdp);
} public void startConnect() throws InterruptedException { if (StringUtils.isBlank(remoteSdp)) {
throw new NullPointerException(
"Please exchange sdp information with peer before start connect! ");
} agent.startConnectivityEstablishment(); // agent.runInStunKeepAliveThread(); synchronized (listener) {
listener.wait();
} } private Agent createAgent(int rtpPort, String streamName) throws Throwable {
return createAgent(rtpPort, streamName, false);
} private Agent createAgent(int rtpPort, String streamName,
boolean isTrickling) throws Throwable { long startTime = System.currentTimeMillis(); Agent agent = new Agent(); agent.setTrickling(isTrickling); // STUN
for (String server : stunServers){
String[] pair = server.split(":");
agent.addCandidateHarvester(new StunCandidateHarvester(
new TransportAddress(pair[0], Integer.parseInt(pair[1]),
Transport.UDP)));
} // TURN
LongTermCredential longTermCredential = new LongTermCredential(username,
password); for (String server : turnServers){
String[] pair = server.split(":");
agent.addCandidateHarvester(new TurnCandidateHarvester(
new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP),
longTermCredential));
}
// STREAMS
createStream(rtpPort, streamName, agent); long endTime = System.currentTimeMillis();
long total = endTime - startTime; log.info("Total harvesting time: " + total + "ms."); return agent;
} private IceMediaStream createStream(int rtpPort, String streamName,
Agent agent) throws Throwable {
long startTime = System.currentTimeMillis();
IceMediaStream stream = agent.createMediaStream(streamName);
// rtp
Component component = agent.createComponent(stream, Transport.UDP,
rtpPort, rtpPort, rtpPort + 100); long endTime = System.currentTimeMillis();
log.info("Component Name:" + component.getName());
log.info("RTP Component created in " + (endTime - startTime) + " ms"); return stream;
} /**
* Receive notify event when ice processing state has changed.
*/
public static final class IceProcessingListener implements
PropertyChangeListener { private long startTime = System.currentTimeMillis(); public void propertyChange(PropertyChangeEvent event) { Object state = event.getNewValue(); log.info("Agent entered the " + state + " state.");
if (state == IceProcessingState.COMPLETED) {
long processingEndTime = System.currentTimeMillis();
log.info("Total ICE processing time: "
+ (processingEndTime - startTime) + "ms");
Agent agent = (Agent) event.getSource();
List<IceMediaStream> streams = agent.getStreams(); for (IceMediaStream stream : streams) {
log.info("Stream name: " + stream.getName());
List<Component> components = stream.getComponents();
for (Component c : components) {
log.info("------------------------------------------");
log.info("Component of stream:" + c.getName()
+ ",selected of pair:" + c.getSelectedPair());
log.info("------------------------------------------");
}
} log.info("Printing the completed check lists:");
for (IceMediaStream stream : streams) { log.info("Check list for stream: " + stream.getName()); log.info("nominated check list:" + stream.getCheckList());
}
synchronized (this) {
this.notifyAll();
}
} else if (state == IceProcessingState.TERMINATED) {
log.info("ice processing TERMINATED");
} else if (state == IceProcessingState.FAILED) {
log.info("ice processing FAILED");
((Agent) event.getSource()).free();
}
}
}
} import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.util.concurrent.TimeUnit; public class PeerA { public static void main(String[] args) throws Throwable {
try {
IceClient client = new IceClient(2020, "audio");
client.init();
client.exchangeSdpWithPeer();
client.startConnect();
final DatagramSocket socket = client.getDatagramSocket();
final SocketAddress remoteAddress = client
.getRemotePeerSocketAddress();
System.out.println(socket.toString());
new Thread(new Runnable() { public void run() {
while (true) {
try {
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf,
buf.length);
socket.receive(packet);
System.out.println("receive:"
+ new String(packet.getData(), 0, packet
.getLength()));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
}
}).start(); new Thread(new Runnable() { public void run() {
int count = 1;
while (true) {
try {
byte[] buf = ("send msg " + count++ + "").getBytes();
DatagramPacket packet = new DatagramPacket(buf,
buf.length); packet.setSocketAddress(remoteAddress);
socket.send(packet);
System.out.println("send msg");
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
}
}).start();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} } }
最新文章
- [转]用C/C++扩展PHP详解
- C# WinForm应用程序降低系统内存占用方法
- 添加或修改ssh服务的端口
- Yahoo!网站性能最佳体验的34条黄金守则(转载)
- 一个webpack,react,less,es6的DEMO
- 应用JConsole学习Java GC
- poj2443(简单的状态压缩)
- FIR数字滤波器的设计要点
- spring简单笔记
- iOS的阴影绘制及性能优化
- 设计模式之 观察者模式详解(包含观察者模式JDK的漏洞以及事件驱动模型)
- 海外仓系统 COD货到付款到付功能
- 史上最全TensorFlow学习资源汇总
- [TJOI2015] 棋盘
- SXWIN7X64EN_20181104_NET_msu_LITE英文精简版
- docker简单搭建gitlab
- Atitit s2018.6 s6 doc list on com pc.docx Atitit s2018.6 s6 doc list on com pc.docx Aitit algo fix 算法系列补充.docx Atiitt 兼容性提示的艺术 attilax总结.docx Atitit 应用程序容器化总结 v2 s66.docx Atitit file cms api
- 详解vue-cli脚手架项目-package.json
- MFC中获取命令行参数的几种方法
- spring-boot 打包成 war包发布