NAT穿透解决

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

     }

}

5.参考资料

  ICE:https://tools.ietf.org/html/rfc5245

  SDP:http://tools.ietf.org/html/rfc4566

  SIP:http://tools.ietf.org/html/rfc3261

  NAT:http://en.wikipedia.org/wiki/Network_address_translation

 STUN:http://tools.ietf.org/html/rfc5389

 TURN:http://tools.ietf.org/html/rfc5766

ICE4J:http://code.google.com/p/ice4j/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券