在上一小節中了解到了通過瀏覽器自帶的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 }
右邊客戶端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 }
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