原文連結: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();
          }

     }

}