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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM