我是一名iOS开发者,我是一名非计算机专业出生的程序员。
如果学习很幸苦,那就尝试无知的代价
这一次的难题来自智能设备的同学,由于耗费时间比较长(近60h),所以有一定分享的意义,如果你有幸遇到类似的问题,希望这篇文章能够帮到你。
智能设备的项目要求,对执法记录仪的画面实时传输到手机上显示,由于一开始没有执法记录仪的设备,我们只能通过两台测试机来模拟这个场景。如果只是相互传输画面,WebRTC的能力足以满足需求,但在这里有个前置条件,执法记录和设备端设备均不可联网,且为离线状态,也就是没有互联网。
在基于没有联网的设备前提下,我们提供了几个设计思路:
在第1、2点中,涉及到“音视频流传输”技术,一定会用到音视频编码、解码等问题,这里没有一定的音视频开发经验,可能很难做到,所以第3点,就是我们研究的方向。
确定了技术方向,我们很快完成了 webrtc 的流程编码工作,并开始验证工作,结果是失败的。我们发现两台设备在wifi连接的情况下,能够实现音视频通话的效果,然而,使用热点相互连接之后,却不行,无论怎样配置修改都没有画面。
因为这项工作已经持续了一段时间,我们也给了一个解决方案,可以购买一个小型的移动随身路由设备,让两台设备连接到随身路由设备上完成通信,当然也有类似于像 Hi3861 芯片的小型处理器可以实现这样的效果。(大概每台成本30~50块钱差不多)
原以为这项工作可以告一段落,但客户,仍然坚持要求以两台设备直连接为最终目标,所以,我们的同事开始了“热点连接+音视频传输”研究,而且很不幸,卡在视频编码/解码上,此时,还没涉及到音频开发,感觉这一条路也很难走的通。
基于上面提到的种种难题,也就引出了,本文带来的解决方案,模拟Turn协议,“骗取”WebRTC流量,实现基于本地中继服务的WebRTC流转发。
看来,音视频流转发这条路应该是走不通了,所以,我们回来重新分析,如何解决这个问题。这时,我想到一个问题:如何在保证 WebRTC 原来数据不变的情况下,进行UDP离线转发,这样既保证了 WebRTC功能的完整性,又能进行离线点对点通信?
带着这样的问题,我查阅了相关资料,我找到一项关于 WebRTC 的说明。实际上,WebRTC 并不能确保任何情况下都能正常进行实时通讯,在如今的路由器设备种类过多的情况下,很多电子设备都存在于NAT设备后,然而,跨 NAT 间的设备难以通信,进而无法实现 WebRTC,为了确保在 NAT 设备下的电子设备间能有效的进行 WebRTC通讯,WebRTC提供了 ICE 候选能力的配置,也就是 STUN/TURN 协议来辅助WebRTC 在复杂 NAT 场景下的有效通讯。
到这里我有点熟悉了,如果对 STUN/TURN 不太理解的同学,可以阅读我的这篇文章: WebRTC实时通讯技术研究
简单说明下,STUN 是 NAT 内网穿透协议,用于交换两台设备间在 NAT 设备下对公网访问之后的临时端口和公网IP,之后利用通讯维持端口不灭,来保证设备间交换数据,完成通讯。而 TURN 是 STUN 的升级版,TURN协议是两台设备间通过 STUN 交换后,如果没能实现通讯,那会在服务端为其创建中继服务,之后的数据会在中继服务中对两台设备间交换转发流量,对,也就是后续的流量走了云端转发实现通讯。
所以,到这里我们就有了一个思路,能否模拟 TURN协议,获取到 WebRTC 中的流量,再通过 UDP 本地转发给另外一台设备,实现 WebRTC 通讯,没错,这是可行的。
TURN 协议的默认端口 3478 ,所以最简单的操作,就是编写一个本地的 Socket 服务,监听 3478 端口,然后在 WebRTC ICE 配置上配置 TURN 协议的IP。
public PeerConnection createPeerConnection(PeerConnection.Observer observer) {
ArrayList<PeerConnection.IceServer> iceServers = new ArrayList<>();
iceServers.add(
new PeerConnection.IceServer("turn:192.168.43.1", "demo", "123456")
);
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
return factory.createPeerConnection(rtcConfig, observer);
}
很顺利,我截获了 TURN 协议的相关流量,如下:
reqst 000100002112a44263754d6935426e4e4f575933
reqst 000300082112a4424e5a3252644b6d39546553580019000411000000
reqst 000300682112a4423868526b4849615779754c5500190004110000000006000464656d6f0014001561746c616e7469732d736f6674776172652e6e6574000000001500203432396539363330346639303062396532623365313566393134646335326565000800149530199a90863c1d259af2fd5a042eab0
这些字节,肯定看不出是什么,如果有协议开发经验的同学,这些字节都有着一定的含义。TURN 协议遵循了RFC5766 协议标准,这种标准一般都有公开的文档和说明,地址如下:
TURN 协议的协议数据结构如下:
我们举个例子,原始字节数据例如:
000100002112a44263754d6935426e4e4f575933
经过我们拆解分析就是:
因为这条消息的数据长度为0,所以也就是没有消息数据,接下来我们看个有数据的消息:
000300682112a4423868526b4849615779754c5500190004110000000006000464656d6f0014001561746c616e7469732d736f6674776172652e6e6574000000001500203432396539363330346639303062396532623365313566393134646335326565000800149530199a90863c1d259af2fd5a042eab0
到这里,我们基本就推算出了,各个消息数据字节的含义,然后我们来看下“消息类型”和“数据类型”代表的含义。
消息类型的含义如下:
数据类型的含义如下,:
由于参数较多,这里我们提到了几个需要用到的参数。
既然,我们得倒来自 WebRTC TURN 协议的请求数据,那我应该如何响应呢,在官方的文档中,并没有切实提到响应数据的结构(主要是看不到响应数据长什么样子),所以这一块比较难分析。
到这里,我们得借助“参照物”程序,有两种方式:
由于 coturn 是 c++ 程序,调试不方便,所以我选择了后者,实现了一个简单的 nodejs 程序部署在云端。
const Turn = require('node-turn');
// 创建 TURN 服务器,设置为无认证
const server = new Turn({
authMech: 'long-term', // 不需要认证
enableTCP: true, // 启用TCP支持
listeningPort: 3478, // TURN 服务器监听的端口
listeningIps: ['0.0.0.0'], // 监听的 IP 地址
relayIps: ['127.0.0.1'], // 中继的 IP 地址
});
server.addUser("demo", "123456")
// 启动 TURN 服务器
server.start(() => {
console.log('TURN server started on port 3478 without authentication');
});
node-turn 是 nodejs turn 协议的三方模块,通过简单几行代码就可以运行 TURN 服务,同时 JS 动态语法的特性,调试起来非常方便,所以我很容易就获取到了响应的数据。
reqst 000100002112a44263754d6935426e4e4f575933
resp 0101000c2112a44263754d6935426e4e4f57593300200008000115f7fb169357
reqst 000300082112a4424e5a3252644b6d39546553580019000411000000
error resp 011300542112a4424e5a3252644b6d39546553580014001561746c616e7469732d736f6674776172652e6e65740000000015002034323965393633303466393030623965326233653135663931346463353265650009001000000401556e617574686f72697a6564
reqst 000300682112a4423868526b4849615779754c5500190004110000000006000464656d6f0014001561746c616e7469732d736f6674776172652e6e6574000000001500203432396539363330346639303062396532623365313566393134646335326565000800149530199a90863c1d259af2fd5a042eab0c2d4ee3
resp 010300482112a4423868526b4849615779754c55001600080001329a5e12a443000d00040000025800200008000115f7fb169357802200096e6f64652d7475726e00000000080014d7d90553df0a39596c0673c746a92f2a95f0d828
......
经过对比分析得出,响应数据的结构和请求消息结构是一样的,消息类型的响应值不一样:
整体的消息结构一致,响应的数据体内容不一样,例如:
在原响应文中:0101000c2112a44263754d6935426e4e4f57593300200008000115f7fb169357
分析得处:
这是首个请求的响应,其他响应的数据也是类似,复杂的是不同的数据类型需要不同的解析。
好,到这里,结合 node-turn 的源码,我们基本掌握了 Turn 协议的数据解析和组装,接下来,我们需要对 Turn 协议的发送流程进行拼装,这里我们需要回到 [RFC5766协议原文](https://datatracker.ietf.org/doc/html/rfc5766)
中,这里有一些流程图,结合 node-turn 的流程基本是正确的。
按照的上面步骤,就可以一步一步套出 WebRTC 的流量数据,虽然流程是这样,单我们仍然有几个问题:
为什么有两个 0x0003? 经过测试,第一次 0x0003 的请求,不携带身份信息,这次访问是针对 STUN 的逻辑路径,如果返回成功信息,后续将不会触发中继流量,所以这里一定要返回失败并且信息是未授权401,这样才可以触发后续的中继服务逻辑。
如何知道两个 Peer 是对等方?实际上,在第二次 0x0003 携带身份请求中,我们需要返回分配的中继 IP 地址+端口号,每个 Peer 都有一个自己的中继 IP +端口号,类似于标记当前 Peer 在服务器中的 ID,在 node-turn 中可是真正意义上的初始化了一个 socket 服务:
在客户端我们就没必要这么做,所有的IP都可以是默认的 127.0.0.1,端口号取 49152 ~ 65535 中间的不重号即可,为什么是最大 65535, 因为端口号占两个字节,最大值是 0xFFFF 就是 65535,响应成功之后。客户端后续会发送 0x0008 的请求,用户绑定当前推送方对指定中继端口号进行授权,端口号包含在数据体中,这里端口号就是我们从 49152 ~ 65535 的端口号,所以我们需要在 0x0003 分配端口号的时候,用 HashMap 把对应端口号指定的客户端IP+端口存储下来。
对第2点的补充,后续我发现 0x16 的数据,本身也会携带这条数据向谁发的端口号,这样就可以绑定了Peer对等方的关系。
在 0x0009 请求之后响应成功之后,接收到 4000 开头的数据? 0x0009 是绑定对等方之间的渠道号ChannelNumber,响应成功之后,后续流量数据会使用 ChannelNumber 直接转发,所以我们需要在 0x0009 的逻辑中,将 ChannelNumber 以及对应的两个 Peer 对等方存储下来,在后续接受到 4000 的数据之后,直接从 HashMap 中找到对等方直接转发流量即可。
实际上,完成上述操作后,仍然没有画面,我花了很长时间找原因,我发现作为热点站点方的设备,没有和连接方的设备产生握手,图示例如下:
我检查了很久,udp 信令服务之间可以正常通信,但 Turn 协议服务,始终只有 192.168.43.9 的请求数据和一条不明所以的 127.0.0.1 + 端口号的请求并且只发送了一次(一开始没有注意到🐶),并没有来自 192.168.43.1 的数据,我想了很多原因,任何一个原因都有可能导致我前面的工作前功尽弃。后来,我将注意力转移到了 127.0.0.1 的请求上,反复测试后发现,这确实来自站点设备,站点模式下的设备,在发送 WebRTC的请求时,数据会以 127.0.0.1 的请求发出,而实际我在 ICE 配置的时候仍然以 192.168.43.1 配置,我想到了类似于跨域的问题。
也就是站点设备,配置了 192.168.43.1 的 Turn 协议,然后以 127.0.0.1 的源头发送给 Turn 服务,所以他的请求IP肯定是 192.168.43.1 ,但他的源头是 127.0.0.1,熟悉跨域问题的同学肯定知道, Host 和 Origin 不匹配的时候,就会造成跨域并抛错误,所以,我的解决方案将 Turn 配置成 127.0.0.1 后,重新操作,握手成功!!,果然发送成功,画面也有了!!
我仍然不确实是什么问题,但确实成功了,我由此联想到了热点模式两台设备无法直接进行 webrtc 的问题,是不是也是类似问题导致的。
最后附一张实机效果图