WebRTC 源码分析(五):安卓 P2P 连接过程和 DataChannel 使用
从本篇起,我们将迈入新的领域:网络传输。首先我们看看 P2P 连接的建立过程,以及 DataChannel 的使用,最终我们会利用 DataChannel 实现一个 P2P 的文字聊天功能。
P2P 连接过程
首先总结一下 WebRTC 建立 P2P 连接的过程(就是喜欢手稿):
我们先来一个简单的名词解释。
SDP
SDP 全称 Session Description Protocol,顾名思义,它是一种描述会话的协议。一次电话会议,一次网络电话,一次视频流传输等等,都是一次会话。那会话需要哪些描述呢?最基础的有多媒体数据格式和网络传输地址,当然还包括很多其他的配置信息。1
为什么需要描述会话?因为参与会话的各个成员能力不对等。大家可能会想到使用所有人都支持的媒体格式,我们暂且不考虑这样的格式是否存在,我们思考另一个问题:如果参与本次会话的成员都比较牛,可以支持更高质量的通话,那使用通用的、普通质量的格式,是不是很亏?既然无法使用固定的配置,那对会话的描述就很有必要了。
最后,一次会话用什么配置,也不是由某一个人说了算,必须所有人的意见达成一致,这样才能保证所有人都能参与会话。那这就涉及到一个协商的过程了,会话发起者先提出一些建议(offer),其他人参与者再根据 offer 给出自己的选择(answer),最终意见达成一致后,才能开始会话。2
上面只是对 SDP 以及协商过程的一个极简理解,详细的定义还得查阅相关的 RFC 文档。
回到 P2P 连接的建立过程,offer 和 answer 其实都是 SDP,而 local/remote 则是相对的,offer 是会话发起者的 local SDP,是会话加入者的 remote SDP,answer 则是会话发起者的 remote SDP,是会话加入者的 local SDP。
SDP 实际上就是一个字符串,它的具体格式定义,可以参考 RFC 文档。它的拼接过程,native 和 Java 代码都有分布,native 代码调用栈还比较深,这里就不展开了,createOffer 主要逻辑就是根据创建 PeerConnection 对象时指定的 MediaConstraints,以及在 createOffer 调用前添加的 VideoTrack/AudioTrack/DataChannel 情况,拼出初始 SDP,最后在 PeerConnectionClient.SDPObserver#onCreateSuccess 中会添加 codec 相关的值。createAnswer 则还会参考 offer SDP 的值。
ICE
ICE 是用于 UDP 媒体传输的 NAT 穿透协议(适当扩展也能支持 TCP 协议),是对 Offer/Answer 模型的扩展,它会利用 STUN、TURN 协议完成工作。ICE 会在 SDP 中增加传输地址记录值(IP + port + 协议),然后对其进行连通性测试,测试通过之后就可以用于发送媒体数据了。3
candidate
每个传输地址记录值都叫做一个 candidate,candidate 可能有三种:
- 客户端从本机网络接口上获取的地址(host);
 - STUN server 看到的该客户端的地址(server reflexive,缩写为 srflx);
 - TURN server 为该客户端分配的中继地址(relayed);
 
两个客户端上述 candidate 的任意组合也许都能连通,但实际上很多组合都不可用,例如 L R 两个客户端处于两个不同的 NAT 网络后面时,网络接口地址都是内网地址,显然无法连通。而 ICE 的任务,就是找出哪些组合可以连通。怎么找?也没有什么黑科技,就是逐个尝试,只不过是有条理地、按照某种顺序去尝试,而不是一通乱搞。
网络接口地址对应的端口号是客户端自己分配的,如果有多个网络接口地址,那就都要带着(看,这里就不是瞎猜哪个地址可用了)。TURN server 可以同时取得 reflexive 和 relayed candidate,而 STUN server 则只能取得 reflexive candidate(这下我就清楚 coturn 到底是 STUN server 还是 TURN server 了)。
三种 candidate 的关系如下图(RFC 画图的技术也是比较高超的):
连通性检查
candidate 收集完毕后,双方的 candidate 两两配对,然后分三步对 candidate 组合进行连通性检查:
- 把 candidates 组合按优先级排序;
 - 按顺序发送检查请求(STUN Binding request),源地址是 candidate 组合的本地 candidate,目的地址是对方 candidate;
 - 收到对方的检查请求后发出响应(STUN Binding response);
 
每次检查实际上是一个四步握手的过程:
STUN 请求和 RTP/RTCP 传输数据使用的是完全一样的地址和端口,解多路复用并不是 ICE 的任务,而是 RTP/RTCP 的任务。
客户端收到的 STUN Binding respose 中也会携带对方的公网地址,如果这个地址和发送请求的 request 地址不一致,那 response 里的地址也会作为一个新的 candidate(peer reflexive),参与到连通性检查中。
如果客户端收到了对方的检查请求,除了发送响应外,也会立即对这个 candidate 组合进行检查,以加快完成一次成功的连通性检查。
candidate 排序
每个客户端会为自己的 candidate 设置权值,双方 candidate 权值之和将作为组合的权值,用于排序。求和的方式确保了双方排序结果的一致性,这个一致性至关重要,因为通常 NAT 都不会允许外部主机的数据包从某个端口进入内网,除非这个端口有数据包发往过这个主机,因此只有双方都发送了检查请求,数据包才可能通过 NAT。
权值的确定,RFC 里面只说明了基本原则:直接的连接比间接的连接要好。但具体如何设置,并没有具体说明。
收集 candidate
candidate 的收集包括两部分:一是 host,二是 srflx 和 relayed。第一部分肯定得在本地网络接口上做文章,第二部分则需要连接 STUN/TURN Server。
WebRTC native 代码量还是很大的,像我这样没什么 C++ 开发经验的朋友,阅读代码将会比较吃力,不过咬咬牙坚持坚持,熟悉起来也就好了,下面简要描述下几个重要过程的代码路径。
candidate 的收集由设备网络连接变化触发:
实际收集 candidate 的过程分为几个阶段:Udp,Relay,Tcp,SslTcp。下面重点分析 Udp 和 Relay 这两个阶段,在这两个阶段里,我们会收集 host,server reflexive 和 relayed 这三种 candidate。
三种 candidate 都会汇报到 BasicPortAllocatorSession::OnCandidateReady 处,从这里最终到达 Java 层的 listener 又还有好几层关卡呢:
上面的过程主要有三个不直接的东西:
- sig slot:简言之就是一个信号处理的框架,A 发一个信号,B 能接收处理,二者完全解耦,具体的可以看看官方文档;
 - message:类似于 Java 里面的 Handler 机制,也是提交消息,接收者进行相关处理,为啥有了 sig slot 还要 message 机制呢?sig slot 无法发送延迟消息是原因之一;
 - 网络:STUN/TURN Server 的访问都是网络请求,为了实现跨平台,网络相关的代码做了不少封装,并且使用的都是操作系统的 C/C++ 接口,这块我也还没有深入看;
 
另外这里推荐一个 STUN/TURN Server 测试工具:Trickle ICE,用来测试服务器是否正确部署,以便排查问题。
使用 candidate
交换了 candidate 之后,WebRTC 会建立连接,发送 STUN ping 检查 candidate 连通性。连通性检查通过后,再交换 DTLS 证书,最后就可以发送音视频数据了。整个过程涉及的代码比较多(中间的步骤我也还没捋得特别清楚),这里就只描述几个关键路径了:
DataChannel 使用
最艰难的部分终于过去了,现在让我们来点轻松的,基于 DataChannel 实现一个 P2P 文字聊天功能。
DataChannel 是 WebRTC 提供的任意数据 P2P 传输的 API,它使用 SCTP 协议,可以灵活配置是否可靠传输。我们可以用它实现文字聊天、文件分享、实时对战游戏等场景下的数据传输,P2P + DTLS 保证了传输数据的安全性。
为了使用 DataChannel,我们先得创建 PeerConnection 对象,而且完成 P2P 连接的建立,具体过程经过上面的分析,我们应该已经了然于胸了,下面只摘录关键代码,完整代码可以查看这个 GitHub 提交。
// 初始化并创建 factoryPeerConnectionFactory.initializeAndroidGlobals(mAppContext,true);mPeerConnectionFactory=newPeerConnectionFactory(null);// 创建 PC 对象mPeerConnection=mPeerConnectionFactory.createPeerConnection(rtcConfig,newMediaConstraints(),this);// 创建 DataChannelDataChannel.Initinit=newDataChannel.Init();init.ordered=true;init.negotiated=true;// false is okinit.maxRetransmits=-1;init.maxRetransmitTimeMs=-1;init.id=0;// must be set, and >= 0mDataChannel=mPeerConnection.createDataChannel("P2P MSG DC",init);mDataChannel.registerObserver(this);// A,创建 offermPeerConnection.createOffer(MsgPcClient.this,mSdpConstraints);// 在 onCreateSuccess 回调中 setLocalDescription// 在 onSetSuccess 回调中把 offer 发出去mPeerConnection.setLocalDescription(MsgPcClient.this,sdp);// B,收到 offer 后 setRemoteDescriptionmPeerConnection.setRemoteDescription(MsgPcClient.this,sdp);// 创建 answermPeerConnection.createAnswer(MsgPcClient.this,mSdpConstraints);// 在 onCreateSuccess 回调中 setLocalDescription// 在 onSetSuccess 回调中把 answer 发出去mPeerConnection.setLocalDescription(MsgPcClient.this,sdp);// A,收到 answer 后 setRemoteDescriptionmPeerConnection.setRemoteDescription(MsgPcClient.this,sdp);// 在 onIceCandidate 回调中把 candidate 发出去// 收到对方的 candidate 后 addIceCandidatemPeerConnection.addIceCandidate(candidate);// 在 onDataChannel 回调中注册消息回调dataChannel.registerObserver(this);// 发送消息byte[]msg=message.getBytes();DataChannel.Bufferbuffer=newDataChannel.Buffer(ByteBuffer.wrap(msg),false);mDataChannel.send(buffer);// onMessage 回调中处理消息ByteBufferdata=buffer.data;finalbyte[]bytes=newbyte[data.capacity()];data.get(bytes);Stringmsg=newString(bytes);Logging.d(TAG,"onMessage "+msg);
创建 DataChannel 时可以通过 DataChannel.Init 的 ordered、maxRetransmitTimeMs、maxRetransmits 参数配置配置可靠性:
- ordered:是否保证顺序传输;
 - maxRetransmitTimeMs:重传允许的最长时间;
 - maxRetransmits:重传允许的最大次数;
 
prebuilt library
最近 WebRTC 官方团队已经开始把 CI 系统打包出来的 aar 上传的 JCenter 了,大家可以尽情享用啦!
脚注
- SDP: Session Description Protocol↩
 - JavaScript Session Establishment Protocol↩
 - Interactive Connectivity Establishment (ICE): A Protocol for Network Address Translator (NAT) Traversal for Offer/Answer Protocols↩
 
https://blog.piasy.com/2017/08/30/WebRTC-P2P-part1/
基于webRTC做的web版聊天示例:
https://www.starrtc.com/demo/h5/
WebRTC 源码分析(五):安卓 P2P 连接过程和 DataChannel 使用的更多相关文章
- Envoy 源码分析--程序启动过程
		
目录 Envoy 源码分析--程序启动过程 初始化 main 入口 MainCommon 初始化 服务 InstanceImpl 初始化 启动 main 启动入口 服务启动流程 LDS 服务启动流程 ...
 - Spring源码分析之Bean的创建过程详解
		
前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...
 - SpringBoot源码分析之SpringBoot的启动过程
		
SpringBoot源码分析之SpringBoot的启动过程 发表于 2017-04-30 | 分类于 springboot | 0 Comments | 阅读次数 SpringB ...
 - Spring源码分析专题 —— IOC容器启动过程(上篇)
		
声明 1.建议先阅读<Spring源码分析专题 -- 阅读指引> 2.强烈建议阅读过程中要参照调用过程图,每篇都有其对应的调用过程图 3.写文不易,转载请标明出处 前言 关于 IOC 容器 ...
 - WebRTC 源码分析(三):安卓视频硬编码
		
数据怎么送进编码器? 怎么从编码器取数据? 如何做流控? 在开始之前,我们先了解一下 MediaCodec 的基本知识. MediaCodec 基础 Developer 官网 上的描述已经很清楚了,下 ...
 - MPTCP 源码分析(五) 接收端窗口值
		
简述: 在TCP协议中影响数据发送的三个因素分别为:发送端窗口值.接收端窗口值和拥塞窗口值. 本文主要分析MPTCP中各个子路径对接收端窗口值rcv_wnd的处理. 接收端窗口值的初始化 ...
 - Vue系列---理解Vue.nextTick使用及源码分析(五)
		
_ 阅读目录 一. 什么是Vue.nextTick()? 二. Vue.nextTick()方法的应用场景有哪些? 2.1 更改数据后,进行节点DOM操作. 2.2 在created生命周期中进行DO ...
 - ABP源码分析五:ABP初始化全过程
		
ABP在初始化阶段做了哪些操作,前面的四篇文章大致描述了一下. 为个更清楚的描述其脉络,做了张流程图以辅助说明.其中每一步都涉及很多细节,难以在一张图中全部表现出来.每一步的细节(会涉及到较多接口,类 ...
 - WebRTC源码分析四:视频模块结构
		
转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...
 
随机推荐
- JQuery Tree插件——zTree
			
Demo:点击下载 zTree 在线操作演示:http://www.ztree.me/v3/demo.php#_101
 - xp中使用grubdos安装ubuntu13.04
			
http://www.cnblogs.com/ggjucheng/archive/2012/08/18/2645916.html 根据以上帖子安装ubuntu13.04 当重启,进入ubuntu in ...
 - 小程序踩过的一个小坑---解析二维码decodeURIComponent() url解码
			
因为我们需要用户扫码进入小程序,每一个货柜都有一个对应的二维码,当然每个二维码里的信息也不一样.用户扫码进入小程序之后,二维码的信息会以参数q带进去,而我们只能在onLoad事件中拿到这个参数, 但是 ...
 - 简简单单搞掂恼人的Laravel 5安装
			
想折腾下Laravel 5了.Laravel是这世界上最好且没有之一的语言──PHP──的众多框架中的一个,是我比较感兴趣的PHP Web Framework. 但是安装Laravel可不是件容易的事 ...
 - mysql性能测试(索引)
			
首先,使用Talend随机生成一千万条数据: 数据库表中现在有1千万+的数据: mysql> select count(*) from zhangchao; +----------+ | cou ...
 - XML5个转义符
			
XML5个转义符:<,>,&,”,©;的转义字符分别如下: < >& " '
 - Atitit html5.1 新特性attilax总结
			
Atitit html5.1 新特性attilax总结 9. 嵌入 header 和 footer1 7. 校验表单1 6. 浏览器的上下文菜单2 1. 响应式图像2 Attilax觉得还不错的心特性 ...
 - hdu 1874 畅通工程续(求最短距离,dijkstra,floyd)
			
题目:http://acm.hdu.edu.cn/showproblem.php?pid=1874 /************************************************* ...
 - .NET MVC5+ Dapper+扩展+微软Unity依赖注入实例
			
1.dapper和dapper扩展需要在线安装或者引用DLL即可 使用nuget为项目增加Unity相关的包 2.model类 public class UserInfo { public int I ...
 - kafka消费者如何才能从头开始消费某个topic的全量数据
			
消费者要从头开始消费某个topic的全量数据,需要满足2个条件(spring-kafka): (1)使用一个全新的"group.id"(就是之前没有被任何消费者使用过); (2)指 ...