udp穿透简单讲解和实现(Java)

  在上一小节中了解到了通过浏览器自带的Webrtc功能来实现P2P视频聊天。在HTML5还没有普及和制定Webrtc标准的前提下,如果要在手机里进行视频实时对话等包括其他功能的话,还是要自己实现,还比较好扩展。所以本次要了解一下udp进行穿透(打洞)。

还是进入正题吧,了解P2P。

1. 原理

  关于原理网上随便就可以找到好多资料了。大部分都是讲解原理的,还配了图,还是不错的。这里不细说。

2. 代码讲解

  本次使用Java语言。网络框架使用Netty4, 其实这些都是次要的,原理看懂才是关键。

服务器代码EchoServer.java

 1 package com.jieli.nat.echo;
 2 
 3 import io.netty.bootstrap.Bootstrap;
 4 import io.netty.channel.ChannelOption;
 5 import io.netty.channel.EventLoopGroup;
 6 import io.netty.channel.nio.NioEventLoopGroup;
 7 import io.netty.channel.socket.nio.NioDatagramChannel;
 8 
 9 public class EchoServer {
10     
11     public static void main(String[] args) {
12         Bootstrap b = new Bootstrap();
13         EventLoopGroup group = new NioEventLoopGroup();
14         try {
15             b.group(group)
16              .channel(NioDatagramChannel.class)
17              .option(ChannelOption.SO_BROADCAST, true)
18              .handler(new EchoServerHandler());
19             
20             b.bind(7402).sync().channel().closeFuture().await();
21         } catch (Exception e) {
22             e.printStackTrace();
23         } finally{
24             group.shutdownGracefully();
25         }
26         
27     }
28 }

服务器代码EchoServerHandler.java

 1 package com.jieli.nat.echo;
 2 
 3 import java.net.InetAddress;
 4 import java.net.InetSocketAddress;
 5 
 6 import io.netty.buffer.ByteBuf;
 7 import io.netty.buffer.Unpooled;
 8 import io.netty.channel.ChannelHandlerContext;
 9 import io.netty.channel.SimpleChannelInboundHandler;
10 import io.netty.channel.socket.DatagramPacket;
11 
12 public class EchoServerHandler extends SimpleChannelInboundHandler<DatagramPacket>{
13 
14     boolean flag = false;
15     InetSocketAddress addr1 = null;
16     InetSocketAddress addr2 = null;
17     /**
18      * channelRead0 是对每个发送过来的UDP包进行处理
19      */
20     @Override
21     protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet)
22             throws Exception {
23         ByteBuf buf = (ByteBuf) packet.copy().content();
24         byte[] req = new byte[buf.readableBytes()];
25         buf.readBytes(req);
26         String str = new String(req, "UTF-8");
27         if(str.equalsIgnoreCase("L")){
28             //保存到addr1中 并发送addr2
29             addr1 = packet.sender();
30             System.out.println("L 命令, 保存到addr1中 ");
31         }else if(str.equalsIgnoreCase("R")){
32             //保存到addr2中 并发送addr1
33             addr2 = packet.sender();
34             System.out.println("R 命令, 保存到addr2中 ");
35         }else if(str.equalsIgnoreCase("M")){
36             //addr1 -> addr2
37             String remot = "A " + addr2.getAddress().toString().replace("/", "")
38                     +" "+addr2.getPort();
39             ctx.writeAndFlush(new DatagramPacket(
40                     Unpooled.copiedBuffer(remot.getBytes()), addr1));
41             //addr2 -> addr1
42             remot = "A " + addr1.getAddress().toString().replace("/", "")
43                     +" "+addr1.getPort();
44             ctx.writeAndFlush(new DatagramPacket(
45                     Unpooled.copiedBuffer(remot.getBytes()), addr2));
46             System.out.println("M 命令");
47         }
48         
49     }
50 
51     @Override
52     public void channelActive(ChannelHandlerContext ctx) throws Exception {
53         System.out.println("服务器启动...");
54 
55         super.channelActive(ctx);
56     }
57 }

左边客户端EchoClient.java

 1 package com.jieli.nat.echo;
 2 
 3 import io.netty.bootstrap.Bootstrap;
 4 import io.netty.channel.ChannelOption;
 5 import io.netty.channel.EventLoopGroup;
 6 import io.netty.channel.nio.NioEventLoopGroup;
 7 import io.netty.channel.socket.nio.NioDatagramChannel;
 8 
 9 /**
10  * 模拟P2P客户端
11  * @author 
12  *
13  */
14 public class EchoClient{
15     
16     public static void main(String[] args) {
17         int port = 7778;
18         if(args.length != 0){
19             port = Integer.parseInt(args[0]);
20         }
21         Bootstrap b = new Bootstrap();
22         EventLoopGroup group = new NioEventLoopGroup();
23         try {
24             b.group(group)
25              .channel(NioDatagramChannel.class)
26              .option(ChannelOption.SO_BROADCAST, true)
27              .handler(new EchoClientHandler());
28             
29             b.bind(port).sync().channel().closeFuture().await();
30         } catch (Exception e) {
31             e.printStackTrace();
32         } finally{
33             group.shutdownGracefully();
34         }
35     }
36 }

左边客户端EchoClientHandler.java

 1 package com.jieli.nat.echo;
 2 
 3 import java.net.InetSocketAddress;
 4 import java.util.Vector;
 5 
 6 import io.netty.buffer.ByteBuf;
 7 import io.netty.buffer.Unpooled;
 8 import io.netty.channel.ChannelHandlerContext;
 9 import io.netty.channel.SimpleChannelInboundHandler;
10 import io.netty.channel.socket.DatagramPacket;
11 
12 //L
13 public class EchoClientHandler extends SimpleChannelInboundHandler<DatagramPacket>{
14     
15     @Override
16     protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet)
17             throws Exception {
18         //服务器推送对方IP和PORT
19         ByteBuf buf = (ByteBuf) packet.copy().content();
20         byte[] req = new byte[buf.readableBytes()];
21         buf.readBytes(req);
22         String str = new String(req, "UTF-8");
23         String[] list = str.split(" ");
24         //如果是A 则发送
25         if(list[0].equals("A")){
26             String ip = list[1];
27             String port = list[2];
28             ctx.writeAndFlush(new DatagramPacket(
29                     Unpooled.copiedBuffer("打洞信息".getBytes()), new InetSocketAddress(ip, Integer.parseInt(port))));
30             Thread.sleep(1000);
31             ctx.writeAndFlush(new DatagramPacket(
32                     Unpooled.copiedBuffer("P2P info..".getBytes()), new InetSocketAddress(ip, Integer.parseInt(port))));
33         }
34         System.out.println("接收到的信息:" + str);
35     }
36     
37     @Override
38     public void channelActive(ChannelHandlerContext ctx) throws Exception {
39         System.out.println("客户端向服务器发送自己的IP和PORT");
40         ctx.writeAndFlush(new DatagramPacket(
41                 Unpooled.copiedBuffer("L".getBytes()), 
42                 new InetSocketAddress("183.1.1.1", 7402)));
43         super.channelActive(ctx);
44     }
45 }

右边客户端EchoClient2.java

 1 package com.jieli.nat.echo;
 2 
 3 import io.netty.bootstrap.Bootstrap;
 4 import io.netty.channel.ChannelOption;
 5 import io.netty.channel.EventLoopGroup;
 6 import io.netty.channel.nio.NioEventLoopGroup;
 7 import io.netty.channel.socket.nio.NioDatagramChannel;
 8 
 9 /**
10  * 模拟P2P客户端
11  * @author 
12  *
13  */
14 public class EchoClient2{
15     
16     public static void main(String[] args) {
17         Bootstrap b = new Bootstrap();
18         EventLoopGroup group = new NioEventLoopGroup();
19         try {
20             b.group(group)
21              .channel(NioDatagramChannel.class)
22              .option(ChannelOption.SO_BROADCAST, true)
23              .handler(new EchoClientHandler2());
24             
25             b.bind(7779).sync().channel().closeFuture().await();
26         } catch (Exception e) {
27             e.printStackTrace();
28         } finally{
29             group.shutdownGracefully();
30         }
31     }
32 }
View Code

右边客户端EchoClientHandler2.java

 1 package com.jieli.nat.echo;
 2 
 3 import java.net.InetSocketAddress;
 4 import java.util.Vector;
 5 
 6 import io.netty.buffer.ByteBuf;
 7 import io.netty.buffer.Unpooled;
 8 import io.netty.channel.ChannelHandlerContext;
 9 import io.netty.channel.SimpleChannelInboundHandler;
10 import io.netty.channel.socket.DatagramPacket;
11 
12 public class EchoClientHandler2 extends SimpleChannelInboundHandler<DatagramPacket>{
13 
14     @Override
15     protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet)
16             throws Exception {
17         //服务器推送对方IP和PORT
18         ByteBuf buf = (ByteBuf) packet.copy().content();
19         byte[] req = new byte[buf.readableBytes()];
20         buf.readBytes(req);
21         String str = new String(req, "UTF-8");
22         String[] list = str.split(" ");
23         //如果是A 则发送
24         if(list[0].equals("A")){
25             String ip = list[1];
26             String port = list[2];
27             ctx.writeAndFlush(new DatagramPacket(
28                     Unpooled.copiedBuffer("打洞信息".getBytes()), new InetSocketAddress(ip, Integer.parseInt(port))));
29             Thread.sleep(1000);
30             ctx.writeAndFlush(new DatagramPacket(
31                     Unpooled.copiedBuffer("P2P info..".getBytes()), new InetSocketAddress(ip, Integer.parseInt(port))));
32         }
33         System.out.println("接收到的信息:" + str);
34     }
35     
36     @Override
37     public void channelActive(ChannelHandlerContext ctx) throws Exception {
38         System.out.println("客户端向服务器发送自己的IP和PORT");
39         ctx.writeAndFlush(new DatagramPacket(
40                 Unpooled.copiedBuffer("R".getBytes()), 
41                 new InetSocketAddress("1831.1.1", 7402)));
42         super.channelActive(ctx);
43     }
44 }
View Code

 

3. 实验环境模拟

实验环境:1台本地主机L,里面安装虚拟机L,地址192.168.182.129. 通过路由器183.1.1.54上网。 1台服务器主机S,服务器地址183.1.1.52:7402, 同时服务器里安装虚拟机R,地址10.0.2.15 .由于外网地址只有两个,所以这能这样测试。通过虚拟机也是可以模拟出测试环境的。  图示如下:

三台测试机ip如下

 

三台测试机器分别启动

然后通过第三方工具发送一个M指定到服务器

一般路由器的缓存会保存一小段时间,具体跟路由器有关。

关于Client R会少接收到一个"打洞消息"这个信息。不是因为UDP的丢包,是Client L 发送的打洞命令。简单说一下。一开始ClientL发送一个UDP到Server,此时ClientL的路由器会保留这样的一条记录(ClientL:IP:Port->Server:IP:Port) 所以Server:IP:Port发送过来的信息,ClientL路由器没有进行拦截,所以可以接收得到。但是ClientR:IP:Port发送过来的消息在ClientL的路由器上是没有这一条记录的,所以会被拒绝。此时ClientL主动发送一条打洞消息(ClientL:IP:Port->ClientR:IP:Port), 使ClientL路由器保存一条记录。使ClientR可以通过指定的IP:Port发送信息过来。不过ClientL的这条打洞信息就不一定能准确的发送到ClientR。原因就是,同理,ClientR路由器上没有ClientL的记录。

由于ClientL ClientR路由器上都没有双方的IP:port,所以通过这样的打洞过程。

我觉得我这样描述还是比较难懂的。如果还不了解,请另外参考其他网上资料。

还有一个就是搭建这样的测试环境还是比较麻烦的。注意如果你只有一台电脑,然后搭建成下面这种测试环境,一般是不行的。因为ClientL和ClientR是通过183.1.1.52路由器进行数据的P2P传输,一般路由器会拒绝掉这种回路的UDP包。

这个时候就要进行内网的穿透了。这个就要像我上一篇博客里面的Webrtc是如何通信一样的了,要通过信令来交换双方信息。

就是发送包括自己内网的所有IP,支持TCPUDP等其他信息封装成信令发送到服务器然后转发到另一端的客户端。使客户端可以对多个IP:Port进行尝试性连接。这个具体的就不展开了。

4.多说两句

  最近智能家具比较火,其中有一类网络摄像头。也是我们公司准备做的一款产品。我简单做了一下前期的预研。目前的一些传统的行业所做的网络摄像头,大部分是基于局域网的网络摄像头,就是只能在自家的路由器上通过手机查看。这类产品,我觉得很难进入普通家庭,因为普通家庭也就那么不到100平方的房子,这种网络摄像头的就体现不是很好了。与普通的监控就是解决了布线的问题了。其他到没有什么提升。

  还有一类是互联网行业做的网络摄像头。小米、360、百度等都有做这类型的网络摄像头。这类型的公司靠自己强大的云存储能力来实现。对这几个产品做了简单的了解,它们是支持本地存储,同时复制一份到云盘上。然后移动端(手机)是通过访问云盘里面的视频来实现监控的。这样虽然有一小段时间的延时,但是这样的效果还是不错的。你想,在监控某个地方,摄像头区域一般画面是不会发生太大的变化的,一个小时里面也就那么几个画面是要看到的。假使一段15分钟的视频,经过分析,得到下面这样。

  然后拖动到高亮的滑动条,高亮的地方,表示视频画面变动较大。这样就比较有针对性,也方便了客户了。还有重要的一点放在网盘,随时随地可以查看。但是也有一点就是隐私问题比较麻烦。其他的还有很多就不展开说明了。

  作为一个小厂,同时作为一名小兵,暂时还不知道公司要做哪种类型的,上级只是让我了解点对点穿透。我猜应该是在家里有个摄像头监控,数据保存在本地。网络部分是移动端发起连接到摄像头,实行点对点的实时监控和读取摄像头本地存储的视频回放,全程只是经过服务器进行命令控制。视频走P2P(走不通应该是转发,这个还不知道。会不会提示当前网络不支持这种提示啊?期待!!毕竟如果转发视频的话很麻烦,很占资源),视频保存本地。我猜目前公司应该是做成这个样子的。(公司非互联网公司,没有那么好的*aaS平台)

 

参考资料:

 

本文地址: http://www.cnblogs.com/wunaozai/p/5545150.html 

posted @ 2016-06-01 08:48  无脑仔的小明  阅读(29371)  评论(8编辑  收藏  举报