NAT穿透解決方案介紹

原文鏈接: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通信解決方法:

(1)如果通信雙方在同一個局域網內,這種情況下可以不借助任何外力直接通過內網地址通信即可;
 
(2)如果通信雙方都在有獨立的公網地址,這種情況下當然可以不借助任何外力直接通信即可;

(3)如果通信雙方一方擁有獨立的公網地址另一方在NAT後面,那麼可以由位於NAT後面的一方主動發起通信請求;

(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服務器

/** 
* 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();
          }

     }

}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章