网络穿透与音视频技术(4)——NAT映射检测和常见网络穿越方法论(NAT检测实践1)

(接上文《网络穿透与音视频技术(3)——NAT映射检测和常见网络穿越方法论(NAT检测)》)

2.2、检测过程实战——服务器端

要进行NAT映射检测,按照上文提到的检测方式,我们就需要一个服务端检测程序。并将服务端检测程序部署到具有两个外网IP的硬件环境下。

2.2.1、检测要求

服务端程序至少需要做到以下功能:

  • 检测客户端和当前服务器端之间是否至少有一级NAT设备:这是因为在检测准备阶段,如果判定当前客户端和服务检测端之间没有任何NAT设备,则无需进行后续的检测了

  • 辅助客户端完成Symmetric NAT检测:当进行Symmetric NAT检测时,服务器端的两对IP + PORT在分别收到客户端IP + PORT发来的检测请求后,只需要将自己收到的客户端的IP + PORT信息封装成响应内容,返回给客户端即可。注意:实际上这和“检测客户端和当前服务器端之间是否至少有一级NAT设备”的工作原理是一样的,但是为了说明整个过程,这里还是分开进行介绍和代码编写。

  • 辅助客户端完成Full Cone NAT检测:服务器端的一组IP + PORT将接收到客户端发来的检测请求,然后用服务器端的另一组IP + PORT发送给客户端。发送的信息同样是“一组服务器端IP + PORT收到的客户端的IP + PORT信息和其它主要信息”。

  • Address Restricted Cone NAT/Port Restricted Cone NAT 检测:服务器端的一组IP + PORT在收到客户端的检测请求后,服务器端会使用当前IP + 另一个PORT的方式封装数据进行响应返回。返回的内容同样是“一组服务器端IP + PORT收到的客户端的IP + PORT信息和其它主要信息”。

  • 这里,我们只讨论服务器端的情况,客户端发送和接受响应的处理过程在后文“检测过程实战——客户端”在进行说明。另外,通过这里的分析我们可以看到,无论是哪一步检测过程服务器端起的都是辅助作用,都是将收到检测请求时真实获取到的客户端IP + PORT通过自己这组IP + PORT或者服务器上的另一组IP + PORT又或者本IP + 另一个PORT重新发送回客户端即可

2.2.2、检测程序设计思路

那么基于以上所述的检测功能要求,我们得到一种服务器端程序的设计思路如下图所示:

在这里插入图片描述

  • 如上图所示我们一共使用了两组IP + PORT,其中一组是主要的IP + PORT(如上图蓝色部分所示)还带有一个辅助的PORT,这个辅助的PORT是在进行第四步(Address Restricted Cone NAT/Port Restricted Cone NAT 检测)验证时使用。

  • 每组IP + PORT都由两个线程,其中一个线程用于进行检测请求接收,另一个线程用于进行检测结果的发送。两个线程间之间使用阻塞队列(BlockingQueue)进行数据通信。这样一来只要主IP + PORT需要另一组IP + PORT发送响应信息时,只需要向对应的消息队列中推入消息即可。

  • 注意:在代码实现细节方面,由于检测思路我们经常使用一组IP + PORT接收UDP数据报,又要同时使用相同的IP + PORT发送响应数据报。所以这组IP + PORT不能是阻塞工作方式,只能使用多路复用的工作方式

2.2.3、主要代码

  • 以下是检测消息接收线程
/**
   * 检测服务的UDP数据报接收线程,这个线程将启动两个,分别为两个独立的ip + port工作
   * 还要指定当前接收线程所服务的ip + port是否为master,因为在进行Full Cone NAT检测时需要
   * @author Administrator
   */
  private static class CheckServer_Acceptor implements Runnable {
    private Selector selector;
    private BlockingQueue<JSONObject> messagesQueue;
    /**
     * 可能指定的辅助检测线程所使用的消息队列
     * 它将在进行Full Cone NAT时产生作用
     */
    private BlockingQueue<JSONObject> slaveMessagesQueue; 
    private boolean isMaster = false;
    public CheckServer_Acceptor(Selector selector , BlockingQueue<JSONObject> messagesQueue) {
      this.selector = selector;
      this.messagesQueue = messagesQueue;
    }
    
    public CheckServer_Acceptor(Selector selector , BlockingQueue<JSONObject> messagesQueue , boolean isMaster , BlockingQueue<JSONObject> slaveMessagesQueue) {
      this(selector, messagesQueue);
      this.isMaster = isMaster;
      if(isMaster == true && slaveMessagesQueue == null) {
        throw new IllegalArgumentException("设置成master后,一定要设置slaveQueueChannel");
      }
      this.slaveMessagesQueue = slaveMessagesQueue;
    }
    
    @Override
    public void run() {
      /*
       * 处理过程为:
       * 1、首先在指定的IP + 端口位置,进行持续的UDP检测请求监听
       * 2、一旦有UDP数据到达,就进行数据->json的转换,并且进行校验,还要获取当前UDP数据报的来源IP和PORT
       * 3、根据传来的type信息,决定要进行的检测类型
       *    3.1、type=1,辅助检测客户端和服务端之间有无NAT设备,这时通知所属的messagesQueue即可
       *    3.2、type=2,辅助检测SymmetricNat,这时通知所属的messagesQueue即可
       *    3.3、type=3,辅助检测Full Cone NAT,这时只有当前服务线程的isMaster == true,才起作用
       *    且不但要通知所属的messagesQueue,还要通知slaveMessagesQueue
       *    3.4、type=4,最后辅助检测Address Restricted Cone NAT/Port Restricted Cone NAT,
       *    这时需要通知所属的messagesQueue,且消费者一侧会根据type进行特殊处理
       * */
      while(true) {
        try {
          doHandle();
        } catch(Exception e) {
          LOGGER.error(e.getMessage() , e);
        }
      }
    }
    
    private void doHandle() throws IOException {
      // 1、=============
      ByteBuffer bb = ByteBuffer.allocateDirect(2048);
      selector.select();
      Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
      while(keys.hasNext()) {
        SelectionKey sk = keys.next();
        keys.remove();
        if(sk.isReadable()) {
          DatagramChannel curdc = (DatagramChannel) sk.channel();
          InetSocketAddress senderSocketAddress = null;
          try {
            senderSocketAddress = (InetSocketAddress) curdc.receive(bb);
          } catch(Exception e) {
            LOGGER.warn(e.getMessage() , e);
            continue;
          }
          bb.flip();
          byte[] peerbs = new byte[bb.limit()];
          for(int i=0;i<bb.limit();i++){
            peerbs[i]=bb.get(i);
          }
          
          // 2、=============
          String receStr = new String(peerbs);
          JSONObject requestObject = null;
          Integer type = null;
          try {
            requestObject = JSONObject.parseObject(receStr);
            if(requestObject == null) {
              continue;
            }
            type = requestObject.getInteger("type");
            if(type == null) {
              continue;
            }
          } catch(Exception e) {
            LOGGER.error(e.getMessage() , e);
          } finally {
            bb.clear();
          }
          // 获得发送方的ip+port
          Inet4Address resouceIp4 = (Inet4Address)senderSocketAddress.getAddress();
          String resouceIp = resouceIp4.getHostAddress();
          Integer resoucePort = senderSocketAddress.getPort();
          requestObject.put("resouceIp", resouceIp);
          requestObject.put("resoucePort", resoucePort);
          requestObject.put("ack", true);
          LOGGER.info("=========接收到检测请求,来自于[" + resouceIp + ":" + resoucePort + "] " + receStr);
          
          // 3、============
          try {
            switch (type) {
              // 3.1
              case 1:
                messagesQueue.put(requestObject);
                break;
              // 3.2
              case 2:
                messagesQueue.put(requestObject);
                break;
              // 3.3
              case 3:
                if(this.isMaster) {
                  messagesQueue.put(requestObject);
                  slaveMessagesQueue.put(requestObject);
                }
                break;
              // 3.4
              case 4:
                if(this.isMaster) {
                  messagesQueue.put(requestObject);
                }
                break;
              default:
                break;
            }
          } catch(InterruptedException e) {
            LOGGER.info("CheckServer_Acceptor收到终止信号,停止运行!!");
            return;
          }
        }
      }
    }
  }
  • 以下是检测结果消息发送线程
/**
   * 检测后向检测请求发起者回执消息的线程<br>
   * 它和指定的接收线程间,通过messagesQueue进行消息交互
   * @author yinwenjie
   */
  private static class CheckServer_Sender implements Runnable {
    /**
     * 本发送线程主要使用的UDP Channel
     */
    private DatagramChannel udpChannel;
    /**
     * 辅助的UDP发送channel
     * 再进行type==4的处理时会使用到
     */
    private DatagramChannel assistChannel;
    /**
     * 当前channel已经和哪些发送目标建立的连接,这里要进行记录
     * 以避免重复的连接
     */
    private Map<String, String> connectedMap = new ConcurrentHashMap<>(20);
    private Map<String, String> assistConnectedMap = new ConcurrentHashMap<>(20);
    /**
     * 本发送线程使用的主要消息队列
     */
    private LinkedBlockingQueue<JSONObject> messagesQueue;
    
    public CheckServer_Sender(DatagramChannel udpChannel , LinkedBlockingQueue<JSONObject> messagesQueue) {
      this.udpChannel = udpChannel;
      this.messagesQueue = messagesQueue;
    }
    
    public CheckServer_Sender(DatagramChannel udpChannel , LinkedBlockingQueue<JSONObject> messagesQueue , DatagramChannel assistChannel) {
      this(udpChannel, messagesQueue);
      this.assistChannel = assistChannel;
    }
    
    @Override
    public void run() {
      /*
       * 处理过程如下:
       * 1、首先一直监控messagesQueue中的消息信息
       * 2、一旦获取消息,就要根据type的情况,准备发送信息
       *    2.1、type == 1 || type == 2 || type == 3,只需要增加当前发送方的ip + port
       *    (记为targetIp和targetPort,这是从client的角度出发)
       *    2.2、type == 4、这是使用辅助的assistChannel进行发送
       *    如果没有设定assistChannel,就不进行操作
       * */
      
      while(true) {
        try {
          doHandle();
        } catch(Exception e) {
          LOGGER.error(e.getMessage() , e);
        }
      }
    }
    
    private void doHandle() throws IOException {
      // 1、============
      JSONObject jsonObject;
      try {
        jsonObject = messagesQueue.take();
      } catch (InterruptedException e) {
        LOGGER.error(e.getMessage() , e);
        return;
      }
      String resouceIp = jsonObject.getString("resouceIp");
      Integer resoucePort = jsonObject.getInteger("resoucePort");
      if(StringUtils.isBlank(resouceIp) || resoucePort == null) {
        return;
      }
      Integer type = jsonObject.getInteger("type");
      if(type == null) {
        return;
      }
      
      // 2、===========
      DatagramSocket udpSocket = null;
      byte[] jsonBytes = null;
      Integer curentLocalPort = null;
      String currentLocalIp = null;
      if(type == 1 || type == 2 || type == 3) {
        udpSocket = udpChannel.socket();
        curentLocalPort = udpSocket.getLocalPort();
        currentLocalIp = udpSocket.getLocalAddress().getHostAddress();
        jsonObject.put("targetIp", currentLocalIp);
        jsonObject.put("targetPort", curentLocalPort);
      } else if (type == 4 && assistChannel != null) {
        udpSocket = assistChannel.socket();
        curentLocalPort = udpSocket.getLocalPort();
        currentLocalIp = udpSocket.getLocalAddress().getHostAddress();
        jsonObject.put("targetIp", currentLocalIp);
        jsonObject.put("targetPort", curentLocalPort);
      } else {
        return;
      }
      
      // 准备发送,根据不同的type,使用不同的channel进行发送
      String jsonContext = jsonObject.toJSONString();
      jsonBytes = jsonContext.getBytes();
      DatagramChannel currentChannel = null;
      if(type == 1 || type == 2 || type == 3) {
        if(connectedMap.get(resouceIp + ":" + resoucePort) == null) {
          connectedMap.put(resouceIp + ":" + resoucePort , "true");
          try {
            udpChannel.connect(new InetSocketAddress(resouceIp, resoucePort));
          } catch(Exception e) {
            LOGGER.warn(e.getMessage());
            return;
          }
        }
        currentChannel = udpChannel;
      } else {
        if(assistConnectedMap.get(resouceIp + ":" + resoucePort) == null) {
          assistConnectedMap.put(resouceIp + ":" + resoucePort , "true");
          try {
            assistChannel.connect(new InetSocketAddress(resouceIp, resoucePort));
          } catch(Exception e) {
            LOGGER.warn(e.getMessage());
            return;
          }
        }
        currentChannel = assistChannel;
      }
      
      // 发送
      LOGGER.info("服务器基于[" + currentLocalIp + ":" + curentLocalPort + "]"
          + "向检测请求者[" + resouceIp + ":" + resoucePort + "]发送检测结果===:" + jsonContext);
      ByteBuffer conentBytes = ByteBuffer.allocateDirect(jsonBytes.length);
      try {
        conentBytes.put(jsonBytes);
        conentBytes.flip();
        currentChannel.write(conentBytes);
      } finally {
        conentBytes.clear();
      }
    }
  }
  • 以下是检测程序的启动代码片段
/**
 * 这个检测服务覆盖的服务类型包括:
 * 1、辅助检测客户端和服务端之间有无NAT设备
 * 2、辅助检测SymmetricNat
 * 3、辅助检测Full Cone NAT
 * 4、最后辅助检测Address Restricted Cone NAT/Port Restricted Cone NAT
 * @author yinwenjie
 */
public class CheckServer2 {
  /**
   * 日志
   */
  private static Logger LOGGER = LoggerFactory.getLogger(CheckServer2.class);
  
  private static LinkedBlockingQueue<JSONObject> messagesQueue = new LinkedBlockingQueue<>();
  
  private static LinkedBlockingQueue<JSONObject> slaveMessagesQueue = new LinkedBlockingQueue<>();
  
  /**
   * 用于描述本地IP + port和channel的对应关系
   */
  private static Map<String, DatagramChannel> localChannelMaps = new HashMap<>();
  
  static {
    BasicConfigurator.configure();
  }
  
  public static void main(String[] args) throws IOException {
    // 得到两对可用的 ip + port(第一对IP + PORT将作为主要的IP + PORT),还有一个为检查Address Restricted Cone NAT设定的辅助port
    String currentIp1 = args[0];
    String currentPort1Value = args[1];
    String currentIp2 = args[2];
    String currentPort2Value = args[3];
    String assistPortValue = args[4];
    Integer currentPort1 = Integer.parseInt(currentPort1Value);
    Integer currentPort2 = Integer.parseInt(currentPort2Value);
    Integer assistPort = Integer.parseInt(assistPortValue);
    
    // 初始化NIO-UDP Server
    // 主IP + PORT
    Selector selector = Selector.open();
    DatagramChannel udpChannel1 = DatagramChannel.open();
    udpChannel1.configureBlocking(false);
    LOGGER.info("正在进行UDP服务监听绑定:[" + currentIp1 + ":" + currentPort1 + "]");
    udpChannel1.socket().bind(new InetSocketAddress(currentIp1 , currentPort1));
    udpChannel1.register(selector, SelectionKey.OP_READ);
    localChannelMaps.put(currentIp1 + ":" + currentPort1, udpChannel1);
    // 副IP + PORT
    DatagramChannel udpChannel2 = DatagramChannel.open();
    udpChannel2.configureBlocking(false);
    LOGGER.info("正在进行UDP服务监听绑定:[" + currentIp2 + ":" + currentPort2 + "]");
    udpChannel2.socket().bind(new InetSocketAddress(currentIp2 , currentPort2));
    udpChannel2.register(selector, SelectionKey.OP_READ);
    localChannelMaps.put(currentIp2 + ":" + currentPort2, udpChannel2);
    // 辅助的PORT
    DatagramChannel assistChannel = DatagramChannel.open();
    assistChannel.configureBlocking(false);
    LOGGER.info("正在进行UDP服务监听绑定:[" + currentIp1 + ":" + assistPort + "]");
    assistChannel.socket().bind(new InetSocketAddress(currentIp1 , assistPort));
    assistChannel.register(selector, SelectionKey.OP_READ);
    localChannelMaps.put(currentIp1 + ":" + assistPort, assistChannel);
    
    // 创建并启动线程
    // 基于主IP + PORT的接收线程和发送线程
    Thread acceptorThread_channelOne = new Thread(new CheckServer_Acceptor(selector, messagesQueue, true, slaveMessagesQueue));
    acceptorThread_channelOne.start();
    Thread senderThread_channelOne = new Thread(new CheckServer_Sender(udpChannel1, messagesQueue, assistChannel));
    senderThread_channelOne.start();
    // 基于副IP + PORT的接收线程和发送线程
    Thread acceptorThread_channelTwo = new Thread(new CheckServer_Acceptor(selector, slaveMessagesQueue));
    acceptorThread_channelTwo.start();
    Thread senderThread_channelTwo = new Thread(new CheckServer_Sender(udpChannel2, slaveMessagesQueue));
    senderThread_channelTwo.start();
  }
  
  //.......
  // 这里就是上文中贴出的CheckServer_Acceptor
  // 和CheckServer_Sender部分代码
  //.......
}

(接后文)

posted @ 2018-10-05 14:02  点点爱梦  阅读(321)  评论(0编辑  收藏  举报