Android P2P 通信方案探索
最近研究起了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
然后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 接口获取。返回如下
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/ 测试,如下
则为成功,(为什么要使用turn,上面已经说了,当NAT为对称NAT时,打洞是不行的,只能使用turn中继服务,而stun 服务公开,开放的也有很多可以直接用,而turn推荐还是用自己的,毕竟数据流会经过服务器,而coturn 集成了两者,经测试,当两设备是同一局域网,或者在不同局域网,都可以通过stun进行打洞连接,而当一设备使用4G网络时,只能使用turn中继),还可以上 Trickle-Ice 测试你的turn 服务
填入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物联网方面的,我后期准备基于这个做一个远程控制类的程序,可以让一个实时显示另一台屏幕内容,并控制设备。
因为这个,学到的东西还挺多的,特此记录。
上一篇: 比特币:一种P2P的电子现金*
下一篇: 【Redis】订阅发布