从本篇起,我们将迈入新的领域:网络传输。首先我们看看 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 了,大家可以尽情享用啦!

脚注

  1. SDP: Session Description Protocol
  2. JavaScript Session Establishment Protocol
  3. 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 使用的更多相关文章

  1. Envoy 源码分析--程序启动过程

    目录 Envoy 源码分析--程序启动过程 初始化 main 入口 MainCommon 初始化 服务 InstanceImpl 初始化 启动 main 启动入口 服务启动流程 LDS 服务启动流程 ...

  2. Spring源码分析之Bean的创建过程详解

    前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...

  3. SpringBoot源码分析之SpringBoot的启动过程

    SpringBoot源码分析之SpringBoot的启动过程 发表于 2017-04-30   |   分类于 springboot  |   0 Comments  |   阅读次数 SpringB ...

  4. Spring源码分析专题 —— IOC容器启动过程(上篇)

    声明 1.建议先阅读<Spring源码分析专题 -- 阅读指引> 2.强烈建议阅读过程中要参照调用过程图,每篇都有其对应的调用过程图 3.写文不易,转载请标明出处 前言 关于 IOC 容器 ...

  5. WebRTC 源码分析(三):安卓视频硬编码

    数据怎么送进编码器? 怎么从编码器取数据? 如何做流控? 在开始之前,我们先了解一下 MediaCodec 的基本知识. MediaCodec 基础 Developer 官网 上的描述已经很清楚了,下 ...

  6. MPTCP 源码分析(五) 接收端窗口值

    简述:      在TCP协议中影响数据发送的三个因素分别为:发送端窗口值.接收端窗口值和拥塞窗口值. 本文主要分析MPTCP中各个子路径对接收端窗口值rcv_wnd的处理.   接收端窗口值的初始化 ...

  7. Vue系列---理解Vue.nextTick使用及源码分析(五)

    _ 阅读目录 一. 什么是Vue.nextTick()? 二. Vue.nextTick()方法的应用场景有哪些? 2.1 更改数据后,进行节点DOM操作. 2.2 在created生命周期中进行DO ...

  8. ABP源码分析五:ABP初始化全过程

    ABP在初始化阶段做了哪些操作,前面的四篇文章大致描述了一下. 为个更清楚的描述其脉络,做了张流程图以辅助说明.其中每一步都涉及很多细节,难以在一张图中全部表现出来.每一步的细节(会涉及到较多接口,类 ...

  9. WebRTC源码分析四:视频模块结构

    转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...

随机推荐

  1. django官方文档--对静态文件的管理

    一.入门级理解: 在django中对静态文件的管理和模板(template)的思路是一样的.在模板的管理中django是把app用到 到的模板都保存到app目录下的templates子目录中. 静态文 ...

  2. 微信小程序:冒泡事件及其阻止

    事件的类别分为几种: 点击事件:tap 长按事件:longtap 触摸事件:touchstart: touchend:touchcancel:touchmove 其他:input:submit.... ...

  3. MySQL的体系结构图

    http://attachbak.dataguru.cn/attachments/forum/201408/30/231337wrbs3xpr5kbs3ryh.png

  4. php分享十四:php接口编写

    一:加密协议选择 二:常用url传递函数介绍 urlencode 和 rawurlencode 区别是: urlencode把空格变为+号,而rawurlencode编码为20%: base64_en ...

  5. IOS UIView自动调整尺寸

    自动尺寸调整行为 当您改变视图的边框矩形时,其内嵌子视图的位置和尺寸往往也需要改变,以适应原始视图的新尺寸.如果视图的autoresizesSubviews属性声明被设置为YES,则其子视图会根据au ...

  6. FFmpeg AVPacket和AVFrame区别

    简介 AVPacket:存储压缩数据(视频对应H.264等码流数据,音频对应AAC/MP3等码流数据)AVFrame:存储非压缩的数据(视频对应RGB/YUV像素数据,音频对应PCM采样数据)

  7. .NET CORE EF 框架调用存储过程

    ; //多个参数多表组合值 SqlParameter[] Param = { new SqlParameter("@UserId", System.Data.SqlDbType.V ...

  8. CMD命令下访问Oracle数据库

    1.非集群下 Windows环境下数据库 127.0.0.1 只是个IP代表  实际要输入你要访问的数据库服务器IP地址的 如果数据库服务器不在本机上,需要加上数据库服务器的地址:用户名/密码@IP地 ...

  9. 每日英语:Got a Case of the Mondays? Blame the Sunday Blues

    Welcome to Monday morning at the office. Did you have trouble sleeping last night? Was your stomach ...

  10. COMPILING ACTIONSCRIPT 3.0 WITH SUBLIME TEXT 2

    At Clock we typically spend our time developing JavaScript and PHP, however, occasionally Flash pres ...