NAT穿透解決方案介紹

最近公司要實如今各類網絡環境下面的多屏互動(機頂盒、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轉發到這個地址,這樣就實現了通訊。html

然而因爲目前存在着各類不一樣類型的NAT設備對NAT有着不一樣的實現方式(將內外地址映射成外網地址的時候有着不一樣的行爲方式),這就給NAT的穿透帶來了麻煩;目前主要的NAT類型有以下幾種:java

1)Full-cone NAT, also known as one-to-one NATandroid

  • 一旦一個內網地址 (iAddr:iPort) 被映射到一個外部地址 (eAddr:ePort), 來自 iAddr:iPort 的任何數據包將經過 eAddr:ePort 發送.
  • 任何外部主機可以經過eAddr:ePort這個地址發送數據包到iAddr:iPort.

2)Address-restricted-cone NATapache

  • 一旦一個內網地址 (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 NATapp

  • 來自相同內部ip和port發送到相同目的地ip和port的請求被映射到惟一的外部ip和port地址;若是相同的內部主機採用相同的ip和port地址發送到不一樣的目的地,那麼從新分配映射地址。
  • 只有先前收到內部主機發送的包的外部主機纔可以發送返回包到內部主機。

針對前面三種NAT類型(即cone NAT)只要通訊雙方彼此知道對方的內部地址和外部地址的映射關係,而後經過UDP打洞的方式就能夠創建相互鏈接的通訊;可是第四種也就是Symmetric NAT的話因爲每次向不一樣目的地發送數據包時採用不一樣的外部地址,也就沒辦法經過直接的方式創建P2P鏈接。iphone

 

1.各類網絡環境下的P2P通訊解決方法:socket

(1)若是通訊雙方在同一個局域網內,這種狀況下能夠不借助任何外力直接經過內網地址通訊便可;
 
(2)若是通訊雙方都在有獨立的公網地址,這種狀況下固然能夠不借助任何外力直接通訊便可;

(3)若是通訊雙方一方擁有獨立的公網地址另外一方在NAT後面,那麼能夠由位於NAT後面的一方主動發起通訊請求;ide

(4)若是通訊雙方都位於NAT後面,且雙方的NAT類型都是cone NAT,那麼能夠經過一個STUN服務器發現本身的NAT類型以及內網和外網傳輸地址映射信息,而後經過Signaling(信令服務器,實現了SIP協議的主機)交換彼此的NAT類型及內網和外網傳輸地址映射信息,而後經過UDP打洞的方式創建通訊鏈接;

(5)若是通訊雙方有一方的NAT類型是Symmetric NAT,則沒法直接創建P2P鏈接,這個時候就須要藉助TURN(Traversal Using Relay NAT)即轉發服務器來實現間接通訊;
 

2.協議及用到的相關技術介紹:

SDP(Session Description Protocol)
當初始化多媒體電視會議、IP電話、視頻流等會話的時候,參與者之間會要求傳送媒介的詳細、傳輸地址和其餘會話描述元數據等信息;SDP爲這些信息提供一種和傳輸方式無關的標準的表現形式。也就是說SDP僅僅只是一種描述會話信息的格式。它主要被各類不一樣的傳輸協議做爲一種信息交換的格式使用列如:HTTP、RTSP、SIP、Email等各類協議。
如ICE裏面的SDP內容爲:
複製代碼
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地址;

SIP(Session Initiation Protocol)
是一種Signaling(信令)通訊協議;有許多互聯網應用須要建立有多個參與者的會話和管理參與者之間相互的數據交換,然而若是這些工做讓應用的參與者來實現是比較複雜的如:用戶也許在端點之間移動、經過多個名稱尋址和也許同時使用幾種不一樣的媒介通訊。有許多協議可以實現各類形式的多媒體會話進行數據傳送例如聲音、視頻或者文本消息。SIP可以和這些協議一同合做,使一個客服端可以發現參與這個會話的其餘客服端並共享同一會話。爲了定位後面加入會話的參與者等功能,SIP可以爲代理服務器建立基礎設施,客服端能夠經過這個代理服務器實現會話註冊、邀請參與會話等功能。SIP是一個建立、修改和終止會話的靈活的多種用途的工具,不依賴於底層的傳輸協議而且不依賴於被建立的會話類型。

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地址等。

 
3.ICE進行NAT穿透的基本過程:
 
在一般的ICE部署環境中,咱們有兩個客服端想要創建通訊鏈接,他們能夠直接經過signaling服務器(如SIP服務器)執行offer/answer過程來交換SDP消息。
在ICE過程開始的時候,客服端忽略他們各自的網絡拓撲結構,不論是不是在NAT設備後面或者多個NAT後面,ICE容許客服端發現他們的所在網絡的拓撲結構的信息,而後找出一個或者更多的能夠創建通訊鏈接的路徑。
下圖顯示了一個典型的ICE部署環境,客服端L和R都在各自的NAT設備後面,下面簡單描述下ICE創建通訊的過程:
(1)L和R先分別經過STUN和TURN服務器獲取本身的host address,server-reflexive address、relayed address(和TURN轉發服務器相對應的地址),其中server-reflexive address和relayed address經過定時刷新保證地址不過時。這些地址一般叫作candinate地址。
(2)給這些candinate地址分配優先級排序並格式化成SDP格式,經過SIP服務器交換彼此的SDP;
(3)交換完成後根據必定的原則把本地的候選和遠程的候選進行配對,每一對都有本身的優先級並根據優先級進行排序後放入Check列表裏面(兩邊都會有相同的Check列表)。
(4)而後進行鏈接性測試,測試前會選擇一個客服端扮演Controlled角色和另外一個扮演Controling角色,連通性檢查完成後扮演Controling角色的客服端負責在有效的Candinate對列表裏面選擇一個做爲一個被選中的傳輸通道並通知Controlled的客服端。
(5)利用被選中的candinate地址對進行通訊。
 
4.ICE JAVA實現代碼
 
我這裏的樣例代碼採用ICE4J來實現,ICE4J的API文檔能夠參考http://bluejimp.com/jitsi/ice4j/javadoc/,在這個實現裏面沒有利用SIP服務器進行SDP信息的交換而是採用手動輸入的方式,在生產環境中能夠部署一臺socket.io或者其餘SIP服務器
 
 1 /** 
 2 * Copyright (c) 2014 All Rights Reserved.  3 * TODO  4 */
 5 
 6 import java.beans.PropertyChangeEvent;  7 import java.beans.PropertyChangeListener;  8 import java.io.BufferedReader;  9 import java.io.InputStreamReader;  10 import java.net.DatagramSocket;  11 import java.net.SocketAddress;  12 import java.util.List;  13 
 14 import org.apache.commons.lang3.StringUtils;  15 import org.apache.log4j.Logger;  16 import org.ice4j.Transport;  17 import org.ice4j.TransportAddress;  18 import org.ice4j.ice.Agent;  19 import org.ice4j.ice.Component;  20 import org.ice4j.ice.IceMediaStream;  21 import org.ice4j.ice.IceProcessingState;  22 import org.ice4j.ice.LocalCandidate;  23 import org.ice4j.ice.NominationStrategy;  24 import org.ice4j.ice.RemoteCandidate;  25 import org.ice4j.ice.harvest.StunCandidateHarvester;  26 import org.ice4j.ice.harvest.TurnCandidateHarvester;  27 import org.ice4j.security.LongTermCredential;  28 
 29 import test.SdpUtils;  30 
 31 public class IceClient {  32 
 33      private int port;  34 
 35      private String streamName;  36 
 37      private Agent agent;  38 
 39      private String localSdp;  40 
 41      private String remoteSdp;  42     
 43      private String[] turnServers = new String[] { "stun.jitsi.net:3478" };  44     
 45      private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" };  46     
 47      private String username = "guest";  48     
 49      private String password = "anonymouspower!!";  50     
 51      private IceProcessingListener listener;  52 
 53      static Logger log = Logger.getLogger(IceClient.class);  54 
 55      public IceClient(int port, String streamName) {  56           this.port = port;  57           this.streamName = streamName;  58           this.listener = new IceProcessingListener();  59  }  60 
 61      public void init() throws Throwable {  62 
 63           agent = createAgent(port, streamName);  64 
 65  agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO);  66          
 67  agent.addStateChangeListener(listener);  68 
 69           agent.setControlling(false);  70 
 71           agent.setTa(10000);  72 
 73           localSdp = SdpUtils.createSDPDescription(agent);  74 
 75           log.info("=================== feed the following"
 76                     + " to the remote agent ===================");  77 
 78  System.out.println(localSdp);  79 
 80           log.info("======================================"
 81                     + "========================================\n");  82  }  83     
 84      public DatagramSocket getDatagramSocket() throws Throwable {  85 
 86           LocalCandidate localCandidate = agent  87  .getSelectedLocalCandidate(streamName);  88 
 89           IceMediaStream stream = agent.getStream(streamName);  90           List<Component> components = stream.getComponents();  91           for (Component c : components) {  92  log.info(c);  93  }  94  log.info(localCandidate.toString());  95           LocalCandidate candidate = (LocalCandidate) localCandidate;  96           return candidate.getDatagramSocket();  97 
 98  }  99 
100      public SocketAddress getRemotePeerSocketAddress() { 101           RemoteCandidate remoteCandidate = agent 102  .getSelectedRemoteCandidate(streamName); 103           log.info("Remote candinate transport address:"
104                     + remoteCandidate.getTransportAddress()); 105           log.info("Remote candinate host address:"
106                     + remoteCandidate.getHostAddress()); 107           log.info("Remote candinate mapped address:"
108                     + remoteCandidate.getMappedAddress()); 109           log.info("Remote candinate relayed address:"
110                     + remoteCandidate.getRelayedAddress()); 111           log.info("Remote candinate reflexive address:"
112                     + remoteCandidate.getReflexiveAddress()); 113           return remoteCandidate.getTransportAddress(); 114  } 115 
116      /**
117  * Reads an SDP description from the standard input.In production 118  * environment that we can exchange SDP with peer through signaling 119  * server(SIP server) 120      */
121      public void exchangeSdpWithPeer() throws Throwable { 122           log.info("Paste remote SDP here. Enter an empty line to proceed:"); 123           BufferedReader reader = new BufferedReader(new InputStreamReader( 124  System.in)); 125 
126           StringBuilder buff = new StringBuilder(); 127           String line = new String(); 128 
129           while ((line = reader.readLine()) != null) { 130                line = line.trim(); 131                if (line.length() == 0) { 132                     break; 133  } 134  buff.append(line); 135                buff.append("\r\n"); 136  } 137 
138           remoteSdp = buff.toString(); 139 
140  SdpUtils.parseSDP(agent, remoteSdp); 141  } 142 
143      public void startConnect() throws InterruptedException { 144 
145           if (StringUtils.isBlank(remoteSdp)) { 146                throw new NullPointerException( 147                          "Please exchange sdp information with peer before start connect! "); 148  } 149 
150  agent.startConnectivityEstablishment(); 151 
152           // agent.runInStunKeepAliveThread();
153 
154           synchronized (listener) { 155  listener.wait(); 156  } 157 
158  } 159 
160      private Agent createAgent(int rtpPort, String streamName) throws Throwable { 161           return createAgent(rtpPort, streamName, false); 162  } 163 
164      private Agent createAgent(int rtpPort, String streamName, 165                boolean isTrickling) throws Throwable { 166          
167           long startTime = System.currentTimeMillis(); 168          
169           Agent agent = new Agent(); 170          
171  agent.setTrickling(isTrickling); 172 
173           // STUN
174           for (String server : stunServers){ 175                String[] pair = server.split(":"); 176                agent.addCandidateHarvester(new StunCandidateHarvester( 177                          new TransportAddress(pair[0], Integer.parseInt(pair[1]), 178  Transport.UDP))); 179  } 180 
181           // TURN
182           LongTermCredential longTermCredential = new LongTermCredential(username, 183  password); 184 
185           for (String server : turnServers){ 186                String[] pair = server.split(":"); 187                agent.addCandidateHarvester(new TurnCandidateHarvester( 188                          new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP), 189  longTermCredential)); 190  } 191           // STREAMS
192  createStream(rtpPort, streamName, agent); 193 
194           long endTime = System.currentTimeMillis(); 195           long total = endTime - startTime; 196 
197           log.info("Total harvesting time: " + total + "ms."); 198 
199           return agent; 200  } 201 
202      private IceMediaStream createStream(int rtpPort, String streamName, 203                Agent agent) throws Throwable { 204           long startTime = System.currentTimeMillis(); 205           IceMediaStream stream = agent.createMediaStream(streamName); 206           // rtp
207           Component component = agent.createComponent(stream, Transport.UDP, 208                     rtpPort, rtpPort, rtpPort + 100); 209 
210           long endTime = System.currentTimeMillis(); 211           log.info("Component Name:" + component.getName()); 212           log.info("RTP Component created in " + (endTime - startTime) + " ms"); 213 
214           return stream; 215  } 216 
217      /**
218  * Receive notify event when ice processing state has changed. 219      */
220      public static final class IceProcessingListener implements
221  PropertyChangeListener { 222 
223           private long startTime = System.currentTimeMillis(); 224 
225           public void propertyChange(PropertyChangeEvent event) { 226 
227                Object state = event.getNewValue(); 228 
229                log.info("Agent entered the " + state + " state."); 230                if (state == IceProcessingState.COMPLETED) { 231                     long processingEndTime = System.currentTimeMillis(); 232                     log.info("Total ICE processing time: "
233                               + (processingEndTime - startTime) + "ms"); 234                     Agent agent = (Agent) event.getSource(); 235                     List<IceMediaStream> streams = agent.getStreams(); 236 
237                     for (IceMediaStream stream : streams) { 238                          log.info("Stream name: " + stream.getName()); 239                          List<Component> components = stream.getComponents(); 240                          for (Component c : components) { 241                               log.info("------------------------------------------"); 242                               log.info("Component of stream:" + c.getName() 243                                         + ",selected of pair:" + c.getSelectedPair()); 244                               log.info("------------------------------------------"); 245  } 246  } 247 
248                     log.info("Printing the completed check lists:"); 249                     for (IceMediaStream stream : streams) { 250 
251                          log.info("Check list for  stream: " + stream.getName()); 252 
253                          log.info("nominated check list:" + stream.getCheckList()); 254  } 255                     synchronized (this) { 256                          this.notifyAll(); 257  } 258                } else if (state == IceProcessingState.TERMINATED) { 259                     log.info("ice processing TERMINATED"); 260                } else if (state == IceProcessingState.FAILED) { 261                     log.info("ice processing FAILED"); 262  ((Agent) event.getSource()).free(); 263  } 264  } 265  } 266 } 267  
268 import java.io.IOException; 269 import java.net.DatagramPacket; 270 import java.net.DatagramSocket; 271 import java.net.SocketAddress; 272 import java.util.concurrent.TimeUnit; 273 
274 
275 public class PeerA { 276 
277      public static void main(String[] args) throws Throwable { 278           try { 279                IceClient client = new IceClient(2020, "audio"); 280  client.init(); 281  client.exchangeSdpWithPeer(); 282  client.startConnect(); 283                final DatagramSocket socket = client.getDatagramSocket(); 284                final SocketAddress remoteAddress = client 285  .getRemotePeerSocketAddress(); 286  System.out.println(socket.toString()); 287                new Thread(new Runnable() { 288 
289                     public void run() { 290                          while (true) { 291                               try { 292                                    byte[] buf = new byte[1024]; 293                                    DatagramPacket packet = new DatagramPacket(buf, 294  buf.length); 295  socket.receive(packet); 296                                    System.out.println("receive:"
297                                              + new String(packet.getData(), 0, packet 298  .getLength())); 299                               } catch (IOException e) { 300                                    // TODO Auto-generated catch block
301  e.printStackTrace(); 302  } 303 
304  } 305  } 306  }).start(); 307 
308                new Thread(new Runnable() { 309 
310                     public void run() { 311                          int count = 1; 312                          while (true) { 313                               try { 314                                    byte[] buf = ("send msg " + count++ + "").getBytes(); 315                                    DatagramPacket packet = new DatagramPacket(buf, 316  buf.length); 317 
318  packet.setSocketAddress(remoteAddress); 319  socket.send(packet); 320                                    System.out.println("send msg"); 321                                    TimeUnit.SECONDS.sleep(10); 322                               } catch (Exception e) { 323                                    // TODO Auto-generated catch block
324  e.printStackTrace(); 325  } 326 
327  } 328  } 329  }).start(); 330           } catch (Exception e) { 331                // TODO Auto-generated catch block
332  e.printStackTrace(); 333  } 334 
335  } 336 
337 }
View Code

 

 
5.參考資料
 

 

出處:http://www.cnblogs.com/javaminer/p/3575282.html

相關文章
相關標籤/搜索