原文連結:https://medium.com/@dev2919/cross-platform-peer-to-peer-file-sharing-over-the-web-using-webrtc-and-react-js-525aa7cc342c
我的動機
我們的目標是製作一個精簡易用的點對點檔案共享網路應用程式,將更多的精力投入到使用者體驗與簡單地辦事上。這個網路應用程式不只是針對特定的個人群體服務的,而是針對整個社群服務。
既然有這麼多檔案共享網站,為什麼我們還要做這些呢?
當然,我也思考過這個問題,但所有的這些網站都沒有真正地說明過這些檔案在哪裡共享或儲存。這可能是一種隱私威脅,因為在當前疫情的情況下,許多人或許經常使用這些服務來共享檔案甚至機密檔案。使用安全的點對點連線和它的資料通道可以傳輸大量的檔案,卻不需要儲存在任何伺服器上,這使得它真正地結實與私有,因為只有連線的客戶端/對等端直接與中間伺服器通訊,不需要中間伺服器進行傳輸。
WebRTC使對等連線和資料通道成為可能。WebRTC基本上是一種相互通訊與傳送資料的全球網路方式,類似於藍芽、NFC和WIFI資料共享。我們可以使用WebRTC實現跨平臺支援,因為它是基於網路的。
讓我們更深入地研究WebRTC。
WebRTC
“WebRTC是一個免費的開放專案,通過簡單的APIs為瀏覽器與移動應用程式提供實時通訊(RTC)功能。WebRTC元件已經進行了優化,以更好地滿足這一目的。”
webrtc.org
好吧,假設,一個“點對點”關聯考慮兩部裝置之間傳送的直接資訊,而不需要伺服器儲存這些資訊。聽起來這對我們的情況很理想對吧?不幸的是,這不是WebRTC工作的方式!
圖為使用WebRTC進行資料傳輸
儘管WebRTC實現了點對點連線,但它確實需要一個稱為信令伺服器的伺服器,該伺服器用於共享有關預期將其相互連線的裝置的資料。這些微妙之處可以通過任何傳統的資訊共享技術來共享。WebSockets在這裡受到青睞,因為它減少了在一個龐大的建立關聯的系統中共享這些額外資料的惰性。
簡而言之,信令伺服器幫助建立連線,然而,當連線建立後,伺服器將不再涉及相關裝置之間共享的資訊。
一年前,當我開始我的第一個WebRTC專案時,很難找到一個在“production”級別下工作得像樣的模型。後來我在網上找到了這個Youtube頻道編碼。開發人員給出了關於可用於生產的WebRTC應用程式的一些很好的例子。
WebRTC如何建立一個連線(技術)
好吧,沒有簡單的方法來解釋這一點,但我的看法是,在網路上所有數量可觀的裝置中,無論如何都必須有一個裝置通過產生訊號來啟動連線,並將其傳送到信令伺服器上。這個對等點被稱為啟動器,在simple-peer(此專案中使用的模組)中,當建立一個啟動器對等點時,{initiator:true}會被傳遞給製作者/建構函式。
如圖:訊號伺服器在執行
當我們得到對等點的訊號資訊時,這些資訊應該通過某種方式通過信令伺服器傳送到不同的集線器。不同的集線器獲取此資訊並嘗試與發起程式建立關聯。在這個過程中,這些對等體同樣產生它們的訊號資訊並被髮送給發起方。發起方獲取此資訊並嘗試與其餘對等方建立連線。
瞧!這些裝置現在已經連線起來,現在有一個數據通道,可以在沒有中間伺服器的情況下共享資訊。
儘量不要過分強調你無法理解WebRTC的上述工作方式以及簡單對等點如何把它抽象化。當我一開始擺弄WebRTC時,它嚇了我一大跳。接下來的部分將對這一點進行更簡單和細緻的解釋。
與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伺服器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上找到整個程式碼。如果你在瀏覽器中嘗試應用上述程式碼並選擇一些圖片檔案(最好小於100KB),它會立即下載這些圖片檔案。這是因為這個對等點位於一個類似的瀏覽器中,而傳送方處於提示狀態。
傳送和獲取的資訊的大小是相等的。這表明我們可以選擇一次性移動整個記錄!
為什麼使用資料緩衝區而不是blob?
在我們過去的程式碼中,如果我們選擇了一個巨大的檔案(大於100KB),那麼文件很可能不會被髮送,這是WebRTC通道的某些約束的直接結果。
如圖:陣列緩衝區漫畫插圖(mozilla.org)
每個陣列緩衝區一次只能有16KB的限制。簡而言之,這意味著我們必須將文件劃分成小陣列緩衝區。
小檔案可以通過WebRTC一次性發處,然而,對於大文件,明智的做法是將檔案隔離到較小的陣列緩衝區中,並同樣傳送每個部分。ArrayBuffer和Blob物件都有削減容量,這使得此過程更加簡單。為此,如果你仔細檢視程式碼,你會發現我們使用了一個名為stream saver的模組,它可以將陣列緩衝區轉換回blob。
筆記
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); }})
因為javascript是單執行緒的。處理大量陣列緩衝區可能導致漂亮的UI無法響應。為了解決這個問題,我們將使用服務工作人員。一個服務工作人員是瀏覽器在後臺執行的指令碼,是與Web頁面分離的,這為不需要Web頁面或使用者互動的特性開啟大門。
在服務工作程式中處理陣列緩衝區
將檔案劃分為陣列緩衝區的優點
雖然它可能會感覺分隔檔案只是一些額外的程式碼,並且會讓東西相互糾纏,但我們得到以下好處,並且可以幫助改進我們的文件共享應用程式。
跨平臺支援(由mozilla.org提供說明)
支援幾乎所有的瀏覽器支援龐大的文件大小——正如前面提到的,這是我們為什麼要實現它的基本解釋。一個更好的方法來破譯所傳送資訊的度量——通過在緩衝區中傳送一個記錄,我們現在可以顯示資訊,例如,傳送的文件的級別,傳送記錄的速度等等。識別未完成傳送的檔案——在無法完全傳送檔案的情況下,現在能夠以不同的方式獲取和處理檔案。結論
由於我們有一個使用WebRTC的文件直接共享程式,而且它還利用了ArrayBuffer,我們現在應該開始考慮為應用程式的生產做準備的東西了。這些細節需要更多的探索,而不僅僅是遵循一個直接的教程。
可以補充的更多內容:
信令伺服器(STUN和TURN伺服器)。使多個對等連線可拓展。當WebRTC不能工作時才用的一種混合共享方式。提高傳輸效率和速度。我希望我已經提供了足夠的資訊讓你們開始使用WebRTC應用程式。