欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android P2P 通信方案探索

程序员文章站 2022-05-21 14:19:06
...

最近研究起了P2P网络,p2p网络其它很早就有了,但是用到的地方不多,以前最多用来p2p种子下载音乐视频这类的应用,对它的原理也一知半解,以p2p下载视频为例,大概原理:服务器里并不保存视频资源,只是保存哪些用户客户端里有此视频,相当于索引,用户A下载视频a,从服务器查询到对应的用户端B有此视频,然后让用户A和用户B建立连接,这样A就是直接从B下载了,减轻了服务器压力,而且A还可以同时从多个有此资源的客户端B1,B2,B3...同时下载不同分段,这样就更加加快下载速度,而且当下载完分段a1后,还可以和其它客户端C建立连接,客户C可以从客户A下载此a1分段。(大致原理是这样,但要实现那有那么简单,汗)

而最近的区块链,最底层的网络技术也用到了p2p网络来进行结点之间的数据同步。这个p2p网络其实有很大用途,当今主流架构是CS模型,也就是客户端直接和服务器直接通讯,客户端和客户端不进行通信,就算你看到的很像客户端与客户端通信的也不是,如微信/QQ,A发送消息到B ,表面上是A直接与B 通信聊天,其实中间经过了服务器,A 发送给服务器,服务器中转发给B ,只不过是长连接。但是这样会有问题,服务器其实是保存了你的聊天记录(微信不知道有没有保存,QQ是肯定有的),如果你的聊天内容很隐私,或者其它原因,不想让服务器保存,那么就可以用到P2P通信,A直接与B通信,服务器没有任何备份,通信内容只在A,B本地保存。这里不考虑网络攻击,中间人拦截等黑客技术,虽然它们也能拿到通信内容,但不在本场景考虑,这就是区块链和p2p的愿景:去服务器(本文不介绍区块链)

P2P通信基本原理可以参考这篇文章:P2P通信基本原理与实现 

还有那个例子也挺好的:P2P-Over-MiddleBoxes-Demo 我在局域网测试可以,没有在公网测试

好的,看完上篇文章,对p2p网络应该有大概理解和实现,简单理一下个人理解:因为公网ip不够,不可能每台设备都分配个公网ip,所以出现 网络地址转换器(NAT),可以理解为路由器或防火墙,把ip分为公网ip和内网ip(192.168.0.0~ 192.168.255.255)也就是局域网内的ip,这里可以看下自己电脑ip,应该都是192.168.xx.xx,然后因为基本NAT不常见,主要考虑两种NAT,锥形NAT(Cone NAT)和对称型NAT(Symmetric NAT),虽然锥形NAT分好几种,但是不影响实现,统称锥形NAT

                           Server S1                             
                        18.181.0.31:1235                     
                              |
                              |
                              |
  ^  Session 1 (A-S1)  ^      |      
  |  18.181.0.31:1235  |      |     
  v 155.99.25.11:62000 v      |  
                              |
                           Cone NAT
                         155.99.25.11
                              |
  ^  Session 1 (A-S1)  ^      |  
  |  18.181.0.31:1235  |      | 
  v  192.168.0.1:1234  v      | 
                              |
                           Client A
                        192.168.0.1:1234

当内网192.168.0.1:1234设备请求18.181.0.31:1235服务器时,cone NAT会建立一个映射关系,把请求包转成外网ip:155.99.25.11:6200-->18.181.0.31:1235,当服务器回复时,NAT把结果包又转成内网地址,然后转发给对应客户端端口,服务器只知道客户端的公网ip 155.99.25.11:6200。

然后又介绍了p2p实现的几种方式:

1、中继:也就是服务器当转发器,A无法与B直接通信,只好通过中转服务器 :A-->Server-->B,B-->Server-->A,这也是最简单,或者这根本不能称为p2p,因为就是传统通信方式,最直接,最稳定,但节点多对服务器有压力

2、逆向链接:这个不用管,因为客户端很少有公网Ip

3、p2p打洞:打洞的目的就是让客户端A与B直接通信,打洞过程最常见为 "端点在不同的NAT",也就是A和B在两个不同网络下的局域网

端点在不同的NAT(原文解释)

假设客户端A和客户端B的地址都是内网地址,且在不同的NAT后面. A、B上运行的P2P应用程序和服务器S都使用了UDP端口1234,A和B分别初始化了 与Server的UDP通信,地址映射如图所示:

                            Server S
                        18.181.0.31:1234
                               |
                               |
        +----------------------+----------------------+
        |                                             |
      NAT A                                         NAT B
155.99.25.11:62000                            138.76.29.7:31000
        |                                             |
        |                                             |
     Client A                                      Client B
  10.0.0.1:1234                                 10.1.1.3:1234

现在假设客户端A打算与客户端B直接建立一个UDP通信会话. 如果A直接给B的公网地址138.76.29.7:31000发送UDP数据,NAT B将很可能会无视进入的 数据(除非是Full Cone NAT),因为源地址和端口与S不匹配,而最初只与S建立过会话. B往A直接发信息也类似.

假设A开始给B的公网地址发送UDP数据的同时,给服务器S发送一个中继请求,要求B开始给A的公网地址发送UDP信息. A往B的输出信息会导致NAT A打开 一个A的内网地址与与B的外网地址之间的新通讯会话,B往A亦然. 一旦新的UDP会话在两个方向都打开之后,客户端A和客户端B就能直接通讯, 而无须再通过引导服务器S了.

打洞的方式分UDP,TCP打洞,因为一些防火墙可能会过滤掉UDP请求,所以TCP打洞成功率更高,UDP效率高,连接少,所以优先UDP打洞,不行再用TCP。但是打洞也不是万能的,上面介绍过两种NAT,如果是Cone NAT就可以通过打洞来实现p2p连接,但如果是对称NAT,那么打洞也不行,只能用中继服务器当转发。

介绍完p2p基本原理与实现,然后去找想关的项目实现,发现介绍理论很多,但真正能用的项目代码很好,本来想用P2P-Over-MiddleBoxes 集成到android中,但是没成功。。编译不行。。

后来发现WebRTC项目,是实现网页之间实时、无插件的音频、视频和数据通信,在各大浏览器已支持,替换了传统要加载插件式等方式,谷歌开源,也挺成熟,项目介绍 :https://webrtc.github.io/samples/(需*)

中文介绍:https://blog.csdn.net/caoshangpa/article/details/53306992

这里有个学习例子:ProjectRTC ,AndroidRTC ,clone 这两个,在服务器上运行ProjectRTC,需安装node js,

  • cd ProjectRTC/
  • npm install
  • node app.js

在浏览器访问ip:3000

Android P2P 通信方案探索

然后start,选择摄像头,然后得到分享链接,在其它电脑上打开链接,选择view,就可以看见刚才摄像头的画面了,此时把服务器关闭,依然可以观看,说明p2p连接建立,不再需要服务器,当然这是同在一局域网下测试。如果想用android和web视频通话,把刚才创建的链接,最后面为Id,复制,

AndroidRTC clone ,在string.xml 中修改

<string name="host">server ip</string>

host 为运行projectRTC 的服务器ip,端口不用变,默认3000,修改

RtcActivity.oncreate 
 final Intent intent = getIntent();
        final String action = intent.getAction();
        callerId="7zamYe1fznlVAUZgAAC3";
/*
        if (Intent.ACTION_VIEW.equals(action)) {
            final List<String> segments = intent.getData().getPathSegments();
            callerId = segments.get(0);
        }*/

对callerId赋值为刚才的id,运行之后没问题就可以双向视频聊天了。

名词理解:结点=客户端=client=Peer

再说一下WebRTC 中,共需要有三台服务,三个服务可以运行在一台服务器上

1、信令服务(Signaling Server):这里是ProjectRTC 项目,当结点打开时,从信令服务器获取唯一id,对应android

client.on("id", messageHandler.onId);

表明已上线,要获取所有在线结点,可通过http://ip:3000/streams.json 接口获取。返回如下

Android P2P 通信方案探索

name为流名,可自定,id 为节点id,节点和节点之间通过id进行连接,

2、stun 服务:p2p原理中打洞服务,先udp,再tcp 

3、turn 服务:p2p原理中的中继服务器,备用服务

ICE : 整合stun+turn ,也就是当stun 打洞不行后,用turn 服务器进行通信的一整套方案

以推流为例,整个建立连接过程如下:

推流端                        信令服务器                    接收端
clientA                      Signaling                    clientB
                                        请求建立连接
    <----------------------------<------init--------<----------
收到请求,创建视频流
    |                 offer 同意推流
    V---->----------------------->----------------------------->
                                                               |
                                        回复已收到offer          |
    <----------------------------<------answer--------<--------V
    

    candidate 交换候选ip:port (若干次)
    ---->----------------------->----------------------------->
                           

    candidate 交换候选ip:port(若干次)
    <----------------------------<---------------------<--------
                          
                           ICE
                            |
        ------------ ICE 通过ip:port 尝试建立连接  ------------
                            |
                  连接建立,B收到A的视频流
    

其中B为主动请求连接A的客户端,经过init,answer,offer 后,这三步很像Tcp 的三次握手,可参考理解。然后 candidate步骤为交换双方所有可连接ip:port ,这其中有内网ip,tcp,udp 各种连接方式,因为有多个候选,所以candidate 重复执行多次。ice就是寻找最合适的连接进行连接,上面说了信令服务可以用ProjectRTC ,而stun+turn 这两个服务可以用 coturn ,conturn 集成了stun+turn,所以搭建好这一个就行了,搭建方法参考文章,基于coturn的webrtc iceserver搭建 和其它文章。

当搭建成功后可以通过 http://ip:3478/ 测试,如下

Android P2P 通信方案探索

则为成功,(为什么要使用turn,上面已经说了,当NAT为对称NAT时,打洞是不行的,只能使用turn中继服务,而stun 服务公开,开放的也有很多可以直接用,而turn推荐还是用自己的,毕竟数据流会经过服务器,而coturn 集成了两者,经测试,当两设备是同一局域网,或者在不同局域网,都可以通过stun进行打洞连接,而当一设备使用4G网络时,只能使用turn中继),还可以上 Trickle-Ice 测试你的turn 服务

Android P2P 通信方案探索

填入stun或turn server,账户,密码(stun 不需要账户密码,turn 需要),格式为stun:ip:port 和turn:ip:port?transport=tcp或udp,然后点击gather candidates 按钮,可以通过增删server观察得到的ip数目变化,如果增加turn server 后,数目有增加,可表示turn server 启动成功。或者通过Component Type ,如果是 relay ,表示这为中继候选。

好了,上面说完WebRTC ,其实webRTC是一个很大的项目,值得研究的点也很多,主要分为视频的录制,编解码,网络传输几大块,这里给个中文的webrtc学习网站 :http://webrtc.org.cn/ 大部分资料都是基于js的,因为这里我只想用它的p2p传输,只研究了相关部分。

回到AndroidRTC项目,这个demo 代码没多少,为更好理解大概原理,可以看看,主要是 WebRtcClient.java 类。因为node我不懂,不关心服务器实现,只能看客户端了。在AndroidRTC 中,视频和音频是以流的方式传输,而且被封装进C层,不太清楚实现,我最终目的并不是传视频,但是我知道,视频流都能传,那其它数据肯定也能传,果然,webrtc中还有一个DataChannel,就是专门用来传输其它数据的,传输对象为bytebuffer,也就是byte[] 。

dataChannel 传输数据

channel.send(new DataChannel.Buffer(ByteBuffer.wrap("message".getBytes()), true));

接收数据:

 channel.registerObserver(new DataChannel.Observer() {


                @Override
                public void onStateChange() {
                    Log.d(TAG, "registerObserver onStateChange" + channel.state());
                    mListener.onStatusChanged("DataChannel " + channel.state());
                }

                @Override
                public void onMessage(DataChannel.Buffer buffer) {
                    Log.d(TAG, "registerObserver onMessage" + buffer + " data=" + buffer.data + " isBinary=" + buffer.binary);

                    int capacity = buffer.data.capacity();
                    byte[] bytes = new byte[capacity];//接收到的byte[]数据
                    buffer.data.get(bytes);

                }
        });

这段建立连接后通过dataChannel发送/接收数据的主要代码,,但是这个dataChannel还有个坑,ByteBuffer.wrap(byte[])  参数byte[] ,如果一段文字的还好,可以正常传输,但是这个是有大小限制的,经试验,单次包最大 66528 字节,~=64kb 也就是如果你传输一张图片的话,是会传输失败并断开连接,1百多k的图片会自动分包,通过打印分包后的大小,最大被分成65528个字节的包。

如果你不想跑这个视频通话,或者也只想传输其它数据,那么可以跑我上传的 AndroidP2pTest 这个项目,也是 ProjectRTC + AndroidP2pTest 的,主要删除了音视频流的传输,增加了文字,图片,文件传输。并且解决了上层传输数据大小超过64k的问题,内部做好了分包,详情见AndroidP2pTest。可以在此基础上修改为你想要的功能,项目运行见github。

发送数据直接用

client.sendData(dataChannel, data,1);

其中type为1,在接收的时候可对type区分,可自定义

接收数据处理

client.setIMessageReceiver(new IMessageReceiver() {
            @Override
            public void onReceiverStart() {
                appText("开始接收");
            }

            @Override
            public void onReceiverProcess(float process) {
                appText("接收中" + process);
            }

            @Override
            public void onReceiverSuccess(byte[] data, int type) {
                appText("接收完成" + data.length);

                if (type == 1) {//text
                    appText("收到 " + new String(data));
                } else if (type == 2) {//bitmap
                    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                    appText("收到  bitmap" + bitmap.getWidth() + " *" + bitmap.getHeight() + " =" + bitmap.getByteCount());
                    Utils.safeShowBitmapDialog(RtcActivity.this, bitmap);
                } else if (type == 3) {//文件
                    //写入文件。。。
                    Log.d(TAG, "file size=" + data.length);
                    appText("收到 文件 fileSize"+data.length);
                }
            }

        });

其中的信令服务,stun,turn 都是跑在一台服务器上,后期可能会关闭,毕竟只是用来开发,商业可千万别用这个,里面也列了一些免费的stun,turn服务,都可拿来玩。经测试,无论是同一局域网,还是不同局域网,还是4g,都能通信,其中4g为turn。

实现p2p后可以拿来做什么?

基于这之上,可以用来做个p2p聊天程序,视频通话……总之能想到的用户对用户应该都能实现,因为现在做android物联网方面的,我后期准备基于这个做一个远程控制类的程序,可以让一个实时显示另一台屏幕内容,并控制设备。

因为这个,学到的东西还挺多的,特此记录。