原文鏈接: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();
}
}
}