从本篇起,我们将迈入新的领域:网络传输。首先我们看看 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. OpenSSL证书生成及Mac上Apache服务器配置HTTPS(也适用centos)

    自签名证书 配置Apache服务器SSL 自己作为CA签发证书 这里是OpenSSL和HTTPS的介绍OpenSSLHTTPS 开启HTTPS配置前提是已在Mac上搭建Apache服务器→Mac上Ap ...

  2. php分享二十七:批量插入mysql

    一:思考 1:如果插入的某个字段大于数据库定义的长度了,数据库会怎么处理? 1>如果数据库引擎是myisam,则数据库会截断后插入,不报错 2>如果数据库引擎是innodb,则数据库会报 ...

  3. ios处理键盘

    #pragma mark - Keyboard - (void)addKeyboardNoti { [[NSNotificationCenter defaultCenter] addObserver: ...

  4. eclipse 运行 emulator时,PANIC:Could not open emulator 的解决办法

    使用eclipse启动emulator的时候,出现PANIC:Could not open emulator,模拟器无法正常的运行. 经过搜索得知,因为我的SDK的环境变量出问题,需要重新配置下环境变 ...

  5. Flink安装、高可用性

    Flink JobManager HA模式部署(基于Standalone) SCP 命令 SSH免密码登录,搭建Flink standalone集群 https://blog.csdn.net/jie ...

  6. himall微信支付

    支付目录:

  7. (原创)C++11改进我们的程序之简化我们的程序(四)

    这次要讲的是:c++11统一初始化.统一begin()/end()和for-loop循环如何简化我们的程序 初始化列表 c++11之前有各种各样的初始化语法,有时候初始化的时候还挺麻烦,比较典型的如v ...

  8. 死亡之Makefile。。。

    A=Nothing build: @rm -rf build/$(A)/* > /dev/null .PHONY: build 这是一个Makefile..只需要打开终端,在这个Makefile ...

  9. 4.3之后的PingPong效果实现

    旧版本的Unity提供Animation编辑器来编辑物理动画. 在其下方可以设置动画是Loop或者是Pingpong等运动效果. 但是,在4.3之后,Unity的动画系统发生了较大的变化. 相信很多童 ...

  10. 使用IntelliJ IDEA搭建kafka源码环境时遇到Output path错误解决办法

    kafka源码环境搭建好之后,需要在IntelliJ IDEA开发工具中以debug方式启动kafka服务器来测试消息的生产和消费. 但是在启动kafka.Kafka类中的main方法(也就是运行 k ...