使用Webrtc和React Js在网络上共享跨平台的点对点文件
正文字?jǐn)?shù):3764? 閱讀時長:9分鐘
我們希望實(shí)現(xiàn)一個零思想的文件傳輸機(jī)制,即在兩個設(shè)備或個人之間共享文件,不需要考慮如何、在哪里、為什么和什么。
文 /?Dev
原文鏈接:https://medium.com/@dev2919/cross-platform-peer-to-peer-file-sharing-over-the-web-using-webrtc-and-react-js-525aa7cc342c
我的動機(jī)
我們的目標(biāo)是制作一個精簡易用的點(diǎn)對點(diǎn)文件共享網(wǎng)絡(luò)應(yīng)用程序,將更多的精力投入到用戶體驗(yàn)與簡單地辦事上。這個網(wǎng)絡(luò)應(yīng)用程序不只是針對特定的個人群體服務(wù)的,而是針對整個社區(qū)服務(wù)。
既然有這么多文件共享網(wǎng)站,為什么我們還要做這些呢?
當(dāng)然,我也思考過這個問題,但所有的這些網(wǎng)站都沒有真正地說明過這些文件在哪里共享或存儲。這可能是一種隱私威脅,因?yàn)樵诋?dāng)前疫情的情況下,許多人或許經(jīng)常使用這些服務(wù)來共享文件甚至機(jī)密文件。使用安全的點(diǎn)對點(diǎn)連接和它的數(shù)據(jù)通道可以傳輸大量的文件,卻不需要存儲在任何服務(wù)器上,這使得它真正地結(jié)實(shí)與私有,因?yàn)橹挥羞B接的客戶端/對等端直接與中間服務(wù)器通信,不需要中間服務(wù)器進(jìn)行傳輸。
WebRTC使對等連接和數(shù)據(jù)通道成為可能。WebRTC基本上是一種相互通信與傳送數(shù)據(jù)的全球網(wǎng)絡(luò)方式,類似于藍(lán)牙、NFC和WIFI數(shù)據(jù)共享。我們可以使用WebRTC實(shí)現(xiàn)跨平臺支持,因?yàn)樗腔诰W(wǎng)絡(luò)的。
讓我們更深入地研究WebRTC。
WebRTC
“WebRTC是一個免費(fèi)的開放項(xiàng)目,通過簡單的APIs為瀏覽器與移動應(yīng)用程序提供實(shí)時通信(RTC)功能。WebRTC組件已經(jīng)進(jìn)行了優(yōu)化,以更好地滿足這一目的。”
webrtc.org
好吧,假設(shè),一個“點(diǎn)對點(diǎn)”關(guān)聯(lián)考慮兩部設(shè)備之間發(fā)送的直接信息,而不需要服務(wù)器保存這些信息。聽起來這對我們的情況很理想對吧?不幸的是,這不是WebRTC工作的方式!
圖為使用WebRTC進(jìn)行數(shù)據(jù)傳輸
盡管WebRTC實(shí)現(xiàn)了點(diǎn)對點(diǎn)連接,但它確實(shí)需要一個稱為信令服務(wù)器的服務(wù)器,該服務(wù)器用于共享有關(guān)預(yù)期將其相互連接的設(shè)備的數(shù)據(jù)。這些微妙之處可以通過任何傳統(tǒng)的信息共享技術(shù)來共享。WebSockets在這里受到青睞,因?yàn)樗鼫p少了在一個龐大的建立關(guān)聯(lián)的系統(tǒng)中共享這些額外數(shù)據(jù)的惰性。
簡而言之,信令服務(wù)器幫助建立連接,然而,當(dāng)連接建立后,服務(wù)器將不再涉及相關(guān)設(shè)備之間共享的信息。
?
一年前,當(dāng)我開始我的第一個WebRTC項(xiàng)目時,很難找到一個在“production”級別下工作得像樣的模型。后來我在網(wǎng)上找到了這個Youtube頻道編碼。開發(fā)人員給出了關(guān)于可用于生產(chǎn)的WebRTC應(yīng)用程序的一些很好的例子。
WebRTC如何創(chuàng)建一個連接(技術(shù))
好吧,沒有簡單的方法來解釋這一點(diǎn),但我的看法是,在網(wǎng)絡(luò)上所有數(shù)量可觀的設(shè)備中,無論如何都必須有一個設(shè)備通過產(chǎn)生信號來啟動連接,并將其發(fā)送到信令服務(wù)器上。這個對等點(diǎn)被稱為啟動器,在simple-peer(此項(xiàng)目中使用的模塊)中,當(dāng)創(chuàng)建一個啟動器對等點(diǎn)時,{initiator:true}會被傳遞給制作者/構(gòu)造函數(shù)。
如圖:信號服務(wù)器在運(yùn)行
當(dāng)我們得到對等點(diǎn)的信號信息時,這些信息應(yīng)該通過某種方式通過信令服務(wù)器發(fā)送到不同的集線器。不同的集線器獲取此信息并嘗試與發(fā)起程序建立關(guān)聯(lián)。在這個過程中,這些對等體同樣產(chǎn)生它們的信號信息并被發(fā)送給發(fā)起方。發(fā)起方獲取此信息并嘗試與其余對等方建立連接。
瞧!這些設(shè)備現(xiàn)在已經(jīng)連接起來,現(xiàn)在有一個數(shù)據(jù)通道,可以在沒有中間服務(wù)器的情況下共享信息。
盡量不要過分強(qiáng)調(diào)你無法理解WebRTC的上述工作方式以及簡單對等點(diǎn)如何把它抽象化。當(dāng)我一開始擺弄WebRTC時,它嚇了我一大跳。接下來的部分將對這一點(diǎn)進(jìn)行更簡單和細(xì)致的解釋。
與WebRTC共享文件(使用simple-peer)
const express = require("express"); const http = require("http"); const app = express(); const server = http.createServer(app); const socket = require("socket.io"); const io = socket(server); const users = {}; const socketToRoom = {}; io.on('connection', socket => { socket.on("join room", roomID => { if (users[roomID]) { const length = users[roomID].length; if (length === 2) { socket.emit("room full"); return; } users[roomID].push(socket.id); } else { users[roomID] = [socket.id]; } socketToRoom[socket.id] = roomID; const usersInThisRoom = users[roomID].filter(id => id !== socket.id); socket.emit("all users", usersInThisRoom); }); socket.on("sending signal", payload => { io.to(payload.userToSignal).emit('user joined', { signal: payload.signal, callerID: payload.callerID }); }); socket.on("returning signal", payload => { io.to(payload.callerID).emit('receiving returned signal', { signal: payload.signal, id: socket.id }); }); socket.on('disconnect', () => { const roomID = socketToRoom[socket.id]; let room = users[roomID]; if (room) { room = room.filter(id => id !== socket.id); users[roomID] = room; socket.broadcast.emit('user left', socket.id); } }); }); server.listen(process.env.PORT || 8000, () => console.log('server is running on port 8000'));Websocket服務(wù)器JscodeReact前端編碼器
import React, { useEffect, useRef, useState } from "react";import io from "socket.io-client";import Peer from "simple-peer";import styled from "styled-components";import streamSaver from "streamsaver"; const Container = styled.div` padding: 20px; display: flex; height: 100vh; width: 90%; margin: auto; flex-wrap: wrap;`; const worker = new Worker("../worker.js"); const Room = (props) => { const [connectionEstablished, setConnection] = useState(false); const [file, setFile] = useState(); const [gotFile, setGotFile] = useState(false);const chunksRef = useRef([]); const socketRef = useRef(); const peersRef = useRef([]); const peerRef = useRef(); const fileNameRef = useRef("");const roomID = props.match.params.roomID;useEffect(() => { socketRef.current = io.connect("/"); socketRef.current.emit("join room", roomID); socketRef.current.on("all users", users => { peerRef.current = createPeer(users[0], socketRef.current.id); });socketRef.current.on("user joined", payload => { peerRef.current = addPeer(payload.signal, payload.callerID); });socketRef.current.on("receiving returned signal", payload => { peerRef.current.signal(payload.signal); setConnection(true); });socketRef.current.on("room full", () => { alert("room is full"); })}, []);function createPeer(userToSignal, callerID) { const peer = new Peer({ initiator: true, trickle: false, });peer.on("signal", signal => { socketRef.current.emit("sending signal", { userToSignal, callerID, signal }); });peer.on("data", handleReceivingData);return peer; }function addPeer(incomingSignal, callerID) { const peer = new Peer({ initiator: false, trickle: false, });peer.on("signal", signal => { socketRef.current.emit("returning signal", { signal, callerID }); });peer.on("data", handleReceivingData);peer.signal(incomingSignal); setConnection(true); return peer; }function handleReceivingData(data) { if (data.toString().includes("done")) { setGotFile(true); const parsed = JSON.parse(data); fileNameRef.current = parsed.fileName; } else { worker.postMessage(data); } }function download() { setGotFile(false); worker.postMessage("download"); worker.addEventListener("message", event => { const stream = event.data.stream(); const fileStream = streamSaver.createWriteStream(fileNameRef.current); stream.pipeTo(fileStream); }) }function selectFile(e) { setFile(e.target.files[0]); }function sendFile() { const peer = peerRef.current; const stream = file.stream(); const reader = stream.getReader();reader.read().then(obj => { handlereading(obj.done, obj.value); });function handlereading(done, value) { if (done) { peer.write(JSON.stringify({ done: true, fileName: file.name })); return; }peer.write(value); reader.read().then(obj => { handlereading(obj.done, obj.value); }) }}let body; if (connectionEstablished) { body = ( <div> <input onChange={selectFile} type="file" /> <button onClick={sendFile}>Send file</button> </div> ); } else { body = ( <h1>Once you have a peer connection, you will be able to share files</h1> ); }let downloadPrompt; if (gotFile) { downloadPrompt = ( <div> <span>You have received a file. Would you like to download the file?</span> <button onClick={download}>Yes</button> </div> ); }return ( <Container> {body} {downloadPrompt} </Container> );}; export default Room;在此Repo上找到整個代碼。如果你在瀏覽器中嘗試應(yīng)用上述代碼并選擇一些圖片文件(最好小于100KB),它會立即下載這些圖片文件。這是因?yàn)檫@個對等點(diǎn)位于一個類似的瀏覽器中,而發(fā)送方處于提示狀態(tài)。
傳送和獲取的信息的大小是相等的。這表明我們可以選擇一次性移動整個記錄!
為什么使用數(shù)據(jù)緩沖區(qū)而不是blob?
?
在我們過去的代碼中,如果我們選擇了一個巨大的文件(大于100KB),那么文檔很可能不會被發(fā)送,這是WebRTC通道的某些約束的直接結(jié)果。
如圖:數(shù)組緩沖區(qū)漫畫插圖(mozilla.org)
每個數(shù)組緩沖區(qū)一次只能有16KB的限制。簡而言之,這意味著我們必須將文檔劃分成小數(shù)組緩沖區(qū)。
小文件可以通過WebRTC一次性發(fā)處,然而,對于大文檔,明智的做法是將文件隔離到較小的數(shù)組緩沖區(qū)中,并同樣發(fā)送每個部分。ArrayBuffer和Blob對象都有削減容量,這使得此過程更加簡單。為此,如果你仔細(xì)查看代碼,你會發(fā)現(xiàn)我們使用了一個名為stream saver的模塊,它可以將數(shù)組緩沖區(qū)轉(zhuǎn)換回blob。
筆記
因?yàn)閖avascript是單線程的。處理大量數(shù)組緩沖區(qū)可能導(dǎo)致漂亮的UI無法響應(yīng)。為了解決這個問題,我們將使用服務(wù)工作人員。一個服務(wù)工作人員是瀏覽器在后臺運(yùn)行的腳本,是與Web頁面分離的,這為不需要Web頁面或用戶交互的特性打開大門。
let array = [];self.addEventListener("message", event => { if (event.data === "download") { const blob = new Blob(array); self.postMessage(blob); array = []; } else if (event.data === "abort") { array = []; } else { array.push(event.data); }})在服務(wù)工作程序中處理數(shù)組緩沖區(qū)
將文件劃分為數(shù)組緩沖區(qū)的優(yōu)點(diǎn)
雖然它可能會感覺分隔文件只是一些額外的代碼,并且會讓東西相互糾纏,但我們得到以下好處,并且可以幫助改進(jìn)我們的文檔共享應(yīng)用程序。
跨平臺支持(由mozilla.org提供說明)
支持幾乎所有的瀏覽器
支持龐大的文檔大小——正如前面提到的,這是我們?yōu)槭裁匆獙?shí)現(xiàn)它的基本解釋。
一個更好的方法來破譯所發(fā)送信息的度量——通過在緩沖區(qū)中發(fā)送一個記錄,我們現(xiàn)在可以顯示信息,例如,發(fā)送的文檔的級別,發(fā)送記錄的速度等等。
識別未完成發(fā)送的文件——在無法完全發(fā)送文件的情況下,現(xiàn)在能夠以不同的方式獲取和處理文件。
結(jié)論
由于我們有一個使用WebRTC的文檔直接共享程序,而且它還利用了ArrayBuffer,我們現(xiàn)在應(yīng)該開始考慮為應(yīng)用程序的生產(chǎn)做準(zhǔn)備的東西了。這些細(xì)節(jié)需要更多的探索,而不僅僅是遵循一個直接的教程。
可以補(bǔ)充的更多內(nèi)容:
信令服務(wù)器(STUN和TURN服務(wù)器)。
使多個對等連接可拓展。
當(dāng)WebRTC不能工作時才用的一種混合共享方式。
提高傳輸效率和速度。
我希望我已經(jīng)提供了足夠的信息讓你們開始使用WebRTC應(yīng)用程序。
LiveVideoStackCon 2020?北京
2020年10月31日-11月1日
點(diǎn)擊【閱讀原文】了解更多詳細(xì)信息
總結(jié)
以上是生活随笔為你收集整理的使用Webrtc和React Js在网络上共享跨平台的点对点文件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HEVC流媒体服务器被过度炒作的5个原因
- 下一篇: Google调查了人们过去24小时的观看