by fanxiushu 2022-07-13 转载或引用请注明原始作者。 接续同系列文章: Windows远程桌面实现之十三:浏览器客户端使用WebRTC传输,以及WebRTC和MSE渲染显示(二)_雨中风华的博客-CSDN博客_websocket 远程桌面
本来是不该有此文的,只是现实总让理想绕道。 在上一篇文中讲过, xdisp_virt 软件中实现WebRTC渲染之后,在浏览器中看视频和游戏, 已经不再像之前只有WebGL渲染的情况:浏览器远程看视频或游戏,WebGL会让浏览器占用大量CPU等资源。
然而,WebRTC特殊和复杂的底层通信模式, 在些网络环境中让人非常头大。 在一般的局域网和有线内网中,WebRTC问题都不大。头大的是在复杂的互联网环境下。 一旦屏蔽UDP通信,WebRTC基本就凉凉了。 中国的网络,UDP多少会遭到一定程度的限制或者直接屏蔽。 其实这也可以理解,当UDP因为各种P2P软件的原因,造成主干网带宽不断被挤压, 而正常的TCP服务通信遭到威胁的时候,作为运营商,肯定得想办法限制这种情况。 因此这种情况也并不会限于国内,其他国家地区根据情况多少也会有些限制。 还比如有防火墙下,或者公司内部只允许有限的端口上网(比如只开启443,80等端口),这些情况下,webrtc基本也是凉凉。 比如还有些个人情况,有人想用 ngrok等类似的反向代理工具 搭建一个能访问内网的环境,然后想用 xdisp_virt进行远控。 这种情况,webrtc也只能凉凉,现在唯一能用的就只有 xdisp_virt中的WebGL渲染模式, 因为他是通过websocket方式传输数据,能透过 ngrok 的代理模式。 。。。等等,这些情况,在现实网络环境中很多的这类情景。
所以说得难听点,WebRTC只能在理想的网络环境中使用。 于是,牛哄哄的集成各种通信协议的WebRTC, 什么 ICE,STUN/TURN,DTLS,SRTP/SCTP 等等,某些时候好像还真成了玩具。 若不是为了让xdisp_virt能在浏览器中进行远控,其实也不会去实现WebRTC。 实现WebRTC的目的其实就是为了让浏览器能用video标签渲染图像。 发现了WebRTC这种通讯问题之后,尝试着想办法在浏览器端只让WebRTC渲染图像,去掉它的通信功能。 但是好像没办法,反正最终没能成功。
于是再次想到,能不能在浏览器端,用js同时实现 WebRTC的服务端和客户端, 也就是通过常规websocket方式把H264等视频编码数据发到浏览器端, 然后浏览器实现一个WebRTC接收这个数据,作为WebRTC的数据提供者, 然后在同一个浏览器内部,用js再实现另一个WebRTC,用来接收前一个WebRTC的数据并用video标签显示出图像。 虽然这样非常迂回绕道,但是只要能实现,也能解决通信问题。 只是非常可惜,花了许多时间去研究,依然无法实现。
再后来发现TURN协议支持TCP中转传输,是作为 TURN的一个附加协议添加的。 这给WebRTC处理上面的网络问题带来了一点希望, 因为不确定现代浏览器是否都支持 TCP传输的 TURN,于是在linux服务器搭建了coturn服务器。 测试下来,最新版本的chrome, firefox,iOS的safari等的WebRTC都支持TCP方式的TURN中转传输, Android因为没设备所以没测试,不过既然与chrome是同一家,应该也支持。
写这篇文章正是基于以上各种网络通信原因,不得不实现 WebRTC中的TCP的TURN中转通信, 从而达到在浏览器端既能使用video标签进行高效渲染,也能很大程度上解决某些受限制的网络环境也能正常使用的问题。 总结起来就一点:在xdisp_virt中实现WebRTC的 TURN的TCP传输。 使用的webrtc依然是开源的亚马逊WebRTC。 不过TCP的TURN服务端是需要自己实现,因为我需要把它同 websocket,http/https等协议集成到同一个端口中, 这样使用起来更加方便。 好在 TCP的TURN协议并不太复杂。
WebRTC的TURN,与我们通常理解的中转服务器,其实没什么本质区别,无非就是实现的协议不同而已。 xdisp_virt实现目标: 在xdisp_virt中实现kvswebrtc(亚马逊的WebRTC),同时实现TCP协议的Turn服务端 tcp turnserver, kvswebrtc获取视频数据,并且转发到同一个程序内部的tcp turnserver,因为在同一个程序内,它们可以直接使用 127.0.0.1通信。 tcp turnserver同时也接收来自客户端浏览器的连接。 这样tcp turnserver同时中转处理 kvswebrtc和客户端浏览器的webrtc。 而在外面的网络看起来, 客户端浏览器和xdisp_virt之间的WebRTC连接通信都是通过 tcp turnserver,不再像WebRTC默认的行为模式有一大堆的UDP请求。
在kvswebrtc和浏览器中实现WebRTC的 TCP TURN中转,其实挺简单。 在kvswebrtc中,只需在 turn的url中添加 ?transport=tcp ,比如: turn:127.0.0.1:11000?transport=tcp 并且在 createPeerConnection创建接口的时候设置 ICE_TRANSPORT_POLICY_RELAY 参数即可。 在浏览器中javascript实现与kvswebrtc差不多。如下伪代码: ? ?ice_svr_addr = { ???????? ?iceServers: [ ??????????? ? ?{ urls: ['turn:192.168.88.8:11000?transport=tcp'], ???????????????? ?credential: '123456', username: 'turn' }], ? ? ? ? ? iceTransportPolicy: 'relay' }
? pc = new RTCPeerConnection(ice_svr_addr );
经过这样的设置,浏览器的WebRTC 和 kvswebrtc都只会直接连接到 TCP ?turnserver服务器端。 当然WebRTC的基本步骤依然没变,依然会收集 sdp,candidate等信息,这些信息依然要通过其他方式发送到对方。 不过candidate信息就只有有关 TURN中转服务器信息,不再包括webrtc所在电脑的信息。
kvswebrtc全程是通过TLS与TURN连接的,而浏览器却很奇怪,好像并没对TCP的TURN TLS提供支持, 反正我把上面的 turn改成 turns,目前所有的浏览器都会报错。 这会带来什么结果,其实也没啥,就是我们用抓包软件能顺抓取识别到 STUN数据包,防火墙想阻止STUN数据包,也能顺阻止。
接着我们再来看看 tcp turnserver的通信过程。 TCP 的TURN协议具体可以查询 RFC6062以及RFC5766等,反正也有好几个RFC文档, 看下来也会觉得挺累的。因此最好的办法是经过实践之后,再来结合RFC文档查看。
好在我们只需实现 TCP 的TURN,而且经过抓包分析,基本上也只需要以下几个命令: 1,ALLOCATE 2, CREATE PERM 3,REFRESH lifetime 4,? ?BINDCHANNEL 5, INDICATION DATA
1,不管是 kvswebrtc还是浏览器的webrtc,经过相互交换 candidate等信息之后,然后连接到TCP turn server, 第一个请求命令就是 ALLOCATE,意思是请求分配relay port, 实际上就是告诉TURN服务器:请给我留一个位置,好让我与其他连上来的WebRTC进行通讯。 turn 服务端需要检查身份信息,通过查看 是否存在USERNAME和MESSAGE-INTEGARY, 并且做 long-term验证,具体如何验证,可以去查阅 RFC文档。 这里只是简单阐述一下, 每个TURN消息,都有个消息头,消息头大小是 20字节,STUN和TURN消息头的格式都是一样的。如下 /// protocol stun header struct proto_stun_header_t { ?? ?uint16_t?? type; ?? ?uint16_t?? length; ?? ?uint32_t?? cookie; ?? ?uint8_t??? number[12]; }; 验证办法是 user:realm:password 组成字符串,然后做MD5运算, 运算的16字节作为 HMAC的key,然后把数据包括 TURN头(但是除开MESSAGE-INTEGARY)做 HMAC运算。 运算结果与MESSAGE-INTEGARY属性里的结果进行比较,如果前后一致,说明验证通过。 基本上除了 INDICATION DATA 和 Channel数据之外,基本都需要做验证。
2,分配的relay port有时间限制的,客户端就需要定时发送REFRESH liftetime命令续命。 当lifetime=0表示主动结束连接。
3,当ALLOCATE成功后,其实就可以开始传输数据了,但是为了防止没有授权的乱发。 所以有了 CREATE PERM命令,表示告诉TCP TURN服务端,我接下来需要把数据转发给谁。
4,当 CREATE PERM之后,其实就可以使用 INDICATION 传输数据了。 INDICATION 主要包括两个属性, 一是 XOR-PEER-ADDRESS,表示我这个数据的传输目的站, 二是 DATA,这个就是具体的数据内容了。
5,如果使用 INDICATION传输数据,则会每次都要包含20字节的STUN头和16个字节属性,也就是有36个字节的额外损耗。 因此为了减少这种通讯损耗,采用了 BINDCHANNEL方式。 告诉turn服务器,与某个客户端的数据转发,与2个字节的chanel number绑定。 这样以后每次通信只需要 2个字节的channel number + 2字节数据大小 +数据内容就可以了。 这样把原本需要36个字节的头,减少为只需要4字节的头部。 这样会出现一个问题,如何区分到来的数据包究竟是 常规的STUN头部的数据包,还是 channel number的数据? 好在RFC文档规定,范围在 0x4000-0x7FFF是channel number。 因此我们可以这样判断,先接收2个字节的数据,然后判断是STUN还是 channel number。 然后再做后面的处理。
TCP TurnServer通信内容大致就是这些。 至于更多细节,请查阅RFC,如果查看RFC文档比较头大,可以一边抓包分析,一般查看RFC。
下图是实现了 WebRTC的TCP 方式传输的 xdisp_virt : 有兴趣可以去我的GITHUB下载使用。
?
|