上个星期,同学让我帮他找资源(大小6G)并发给千里之外的另一个人,于是就有了下文
当今这么发达的互联网,上有星链,下有5g,大到太阳系,小到物联网,竟然在不同设备间传个文件都这么麻烦,非要用什么QQ,微信,甚至稍大一点的只能用“稳定”到几十kb的某度云。我只想单纯的发几个零和一从这台机器到另一台机器就这么难吗?
所以我想找找有什么技术能改变一下点对点传输的现状,可惜因为内网的存在,让不依靠服务器的点对点十分困难,其实QQ的传输也算的上点对点了(普通用户2G限制),但是我希望尽量少的依赖软件(尤其是会扫你浏览器记录的软件)
终于逛了一圈偌大的互联网后,我发现了一个叫webRTC的东西(Web Real-Time Communication:网页即时通信)大概是2011年左右诞生的,主要负责网页间音视频通信,所以可能在直播,视频聊天等方面比较火,这项技术现在主流浏览器基本都支持(但不同浏览器之间有些差距)。里面的DataChannel可以实现数据的传输
它可以简单的实现内网机器之间打洞传输,整个过程只有两次需要服务器,之后就是浏览器之间直接的P2P连接,所以这篇文章就是围绕应用研究一下webRTC用到的协议,然后看一下能否实现大文件点对点传输(用浏览器)
原理
手动连接
首先尝试在本地的两个浏览器上直接连接
过程:首先一方发送邀请给另一方,另一方收到邀请后根据邀请生成应答再发送回去,于是一个简单的连接就建立成功了
但是怎样才能飘洋过海,穿过NAT,连接的更远呢?
ICE服务器,在生成邀请和应答时把自己在互联网上的位置考虑进去
我写了一个非常简单两个网页,一个负责发送连接邀请,一个回应应答
offer.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>P2P</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> <script src="./offer.js"></script> </head> <body> <p> <button id="send">发送邀请</button> <h2>你的邀请: 请将此邀请发给对方</h2> <textarea id="my_icecandidate" rows="10" cols="50"></textarea> <h2>输入对方应答:</h2> <textarea id="other_icecandidate" rows="10" cols="50"></textarea><br> <button id="start">开始聊天</button> </p> <p id='mess'></p> <p> 输入: <input type="text" id="content"> <button id="input">发送</button> </p> </body> </html>
|
offer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const lc = new RTCPeerConnection() const dc = lc.createDataChannel("123123") dc.onmessage = e => { $("#mess").append($("<h3>").text('对方: '+e.data)) roll() console.log("got it :" + e.data) } dc.onopen = e => { $("#mess").append($("<h2>").text('对方接收成功, 开始聊天')) console.log("connect!!!") }
var restartConfig = {iceServers:[{urls:"stun:stun.gmx.net"}]} lc.setConfiguration(restartConfig)
lc.onicecandidate = e => { $("#my_icecandidate").text(JSON.stringify(lc.localDescription)) console.log("ice" + JSON.stringify(lc.localDescription)) }
$(document).ready(function(){ $("#send").click(function(){ lc.createOffer({"iceRestart": true}).then(o => lc.setLocalDescription(o)).then(a=>console.log("set sucess")) }) $("#start").click(function(){ lc.setRemoteDescription(JSON.parse($("#other_icecandidate").val())) }) $("#input").click(function(){ let data = $("#content").val() $("#mess").append($("<h3>").text('我: '+data)) roll() dc.send(data) }) })
function roll(){ $("html, body").animate({ scrollTop: $('html, body').get(0).scrollHeight }, 100); }
|
answer.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>P2P</title> <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script> <script src="./answer.js"></script> </head> <body> <p> <h2>输入对方邀请:</h2> <textarea id="other_icecandidate" rows="10" cols="50"></textarea><br> <button id="receive">接受聊天邀请</button> <h2>你的应答: 请将此应答发给对方</h2> <textarea id="my_icecandidate" rows="10" cols="50"></textarea> <h2>对方接受后就可以开始聊天了</h2> </p> <p id='mess'> </p> <p> 输入: <input type="text" name="content" id="content"> <button id="input">发送</button> </p> </body> </html>
|
answer.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| const rc = new RTCPeerConnection() var restartConfig = {iceServers:[{urls:"stun:stun.gmx.net"}]} rc.setConfiguration(restartConfig)
rc.onicecandidate = e => { $("#my_icecandidate").text(JSON.stringify(rc.localDescription)) console.log("ice" + JSON.stringify(rc.localDescription)) } rc.ondatachannel = e => { rc.dc = e.channel rc.dc.onmessage = e => { $("#mess").append($("<h3>").text('对方: '+e.data)) roll() console.log("data:"+e.data) } rc.dc.onopen = e => { console.log("connect!!") } } console.log("over")
$(document).ready(function(){ $("#receive").click(function(){ console.log("...") rc.setRemoteDescription(JSON.parse($("#other_icecandidate").val())) rc.createAnswer({"iceRestart": true}).then(a => rc.setLocalDescription(a)).then(a=>{ console.log("ans create") }) }) $("#input").click(function(){ let data = $("#content").val() $("#mess").append($("<h3>").text('我: '+data)) roll() rc.dc.send(data) }) })
function roll(){ $("html, body").animate({ scrollTop: $('html, body').get(0).scrollHeight }, 100); }
|
![屏幕截图 2022-01-24 180402]()
里面用到了一个ICE服务器stun.gmx.net
有了这个服务器,就可以穿过NAT了
加入中介,自动连接
简单来讲,就是PeerA用户问ICE服务器,“我是谁,我在哪?”,然后得到PeerA在网络上的位置
然后PeerA把连接邀请发给TURN服务器,让TURN等到PeerB连接上之后交给PeerB
PeerB在从ICE服务器得知自己位置之后,询问TURN有没有PeerA的邀请
然后PeerB根据邀请生成应答由TURN转交PeerA
SCTP
webRTC的DataChannel即没有用TCP也没有用UDP,而是使用了一个叫SCTP的协议,这个协议也是一个很老的协议,而且根据网上信息,效率和稳定性都比TCP好上不少(具体怎么样,我还不知道怎么去评估)但是到现在不知道为什么,也没人用
![image-20220125162430653]()
SCTP很有意思的一点是,通过设置即可以变得像TCP也可以像UDP,这点看来挺灵活的
![image-20220125161736927]()
1 2 3
| ordered: false maxPacketLifeTime: 3000 maxRetransmits: 10
|
假如ordered是false,maxRetransmits为0,SCTP就变成了UDP
并且RTCDataChannel还使用了DTLS,用SSL保证数据安全(具体怎么用就不知道了,https都还没搞懂)
下面是wireshark抓包,DTLS用来加密发送数据,STUN协议应该是保持连接吧,两个的传输层协议都是UDP
很奇怪的是,明明上面说的用的是SCTP协议,这里却根本找不到SCTP的影子,一种解释是SCTP根本无法通过路由器(大多不支持SCTP协议),所以被转换为UDP,(SCTP over UDP)
![image-20220125164421619]()
传输大文件的可能
内存
首先是DataChannel的send方法,每次发送的大小上限是64KB(道听途说,还未验证),这个没有问题,只要把文件切片,多发几次就行。
但是,发送完之后内容存在了对方浏览器内存里,根本无法实现实时下载,也就是说传输文件大小的上限取决于双方内存大小,更准确一点说是双方的浏览器允许一个标签页占用的内存上限
可以用window.performance.memory.jsHeapSizeLimit检查chrome页面内存上限,我的是4G(我的内存是16G,如果是8G内存,应该是1G)
如果按照大多数人电脑都是8G内存的话,最高能传输的大文件只有1G,而且只能是电脑之间传输,毕竟手机或者平板的内存会更小
NAT
如果传输文件根本不用中介的话,穿越NAT似乎也不是很可靠
我尝试了临近几个宿舍wifi的连接,还有跨越半个中国的连接,得到的结论是大部分wifi都可以成功,远距离连接有些不稳定,有时候会失败
然后是4G网和wifi,4G和4G根本连不上
这个我不太懂4G网络的原理,所以也不知道是什么原因,以后再探究
速度
还未考究
结论
由于浏览器限制,传输大文件还是不行。。。。。
可能的解决方案:
- 16G内存,最高可传输4G文件
- 分成多个文件,最后下载cmd脚本等在本地合并
- 下载端下载exe程序来接收文件(这不就又相当于用其他软件了吗)
- (上述方案对跨平台及不合适)
有一个叫WebTorrent的项目,就是用webRTC来传输文件的,地址:
https://github.com/webtorrent/webtorrent
传输文件演示网站:
https://instant.io/