建立即時多人網路應用程序,無需伺服器
嘗試一下演示嗎?
trystero管理一個秘密的快遞網絡,讓您的應用程式的用戶可以直接相互交談,加密且無需伺服器中間人。
這個網路充滿了開放、去中心化的通訊管道:洪流追蹤器、物聯網設備經紀人、精品文件協定和利基社交網路。
trystero搭載在這些網路上,可以在您的應用程式的使用者之間自動建立安全、私密的 p2p 連接,而您無需付出任何努力。
同行可以透過 ?比特流,?諾斯特爾,? MQTT、⚡️ Supabase、Firebase 還是? IPFS – 全部使用相同的 API。
除了自動進行對等配對之外, trystero在 WebRTC 之上也提供了一些很好的抽象:
您可以在此處查看人們使用trystero的內容。
如果您只想嘗試trystero ,您可以跳過此解釋並直接使用它。
為了與 WebRTC 建立直接的點對點連接,需要一個訊號通道來交換對等資訊 (SDP)。通常,這涉及運行您自己的匹配伺服器,但trystero為您抽象化了這一點,並提供了多種用於連接對等點的「無伺服器」策略(目前為BitTorrent、Nostr、MQTT、Supabase、Firebase 和IPFS)。
要記住的重要一點是:
除了對等發現之外,您的應用程式的資料永遠不會接觸策略介質,而是在使用者之間直接點對點和端對端加密發送。
?
您可以在這裡比較策略。
您可以使用 npm ( npm i trystero
) 安裝並像這樣匯入:
import { joinRoom } from ' trystero '
或者也許您更喜歡簡單的腳本標籤?從最新版本下載預先建置的 JS 檔案並將其匯入本機:
< script type =" module " >
import { joinRoom } from './ trystero -torrent.min.js'
</ script >
預設情況下,使用Nostr策略。要使用不同的,只需像這樣深度導入(您的捆綁程序應該處理僅包含相關程式碼):
import { joinRoom } from ' trystero /mqtt' // ( trystero -mqtt.min.js with a local file)
// or
import { joinRoom } from ' trystero /torrent' // ( trystero -torrent.min.js)
// or
import { joinRoom } from ' trystero /supabase' // ( trystero -supabase.min.js)
// or
import { joinRoom } from ' trystero /firebase' // ( trystero -firebase.min.js)
// or
import { joinRoom } from ' trystero /ipfs' // ( trystero -ipfs.min.js)
接下來,將使用者加入 ID 的房間:
const config = { appId : 'san_narciso_3d' }
const room = joinRoom ( config , 'yoyodyne' )
第一個參數是需要appId
的配置物件。這應該是您的應用程式的完全唯一的識別碼。第二個參數是房間 ID。
為什麼是房間?瀏覽器一次只能處理有限數量的 WebRTC 連接,因此建議設計您的應用程序,將使用者分為群組(或房間、命名空間或通道……無論您如何稱呼它們)。
1 使用 Firebase 時, appId
應該是您的databaseURL
,而使用 Supabase 時,它應該是您的專案 URL。
聆聽加入房間的同伴:
room . onPeerJoin ( peerId => console . log ( ` ${ peerId } joined` ) )
聆聽同伴離開房間的聲音:
room . onPeerLeave ( peerId => console . log ( ` ${ peerId } left` ) )
監聽發送音訊/視訊串流的同伴:
room . onPeerStream (
( stream , peerId ) => ( peerElements [ peerId ] . video . srcObject = stream )
)
若要取消訂閱活動,請離開房間:
room . leave ( )
您可以透過匯入selfId
來存取本機使用者的對等 ID,如下所示:
import { selfId } from ' trystero '
console . log ( `my peer ID is ${ selfId } ` )
向同行發送您的視訊串流:
room . addStream (
await navigator . mediaDevices . getUserMedia ( { audio : true , video : true } )
)
發送並訂閱自訂 P2P 操作:
const [ sendDrink , getDrink ] = room . makeAction ( 'drink' )
// buy drink for a friend
sendDrink ( { drink : 'negroni' , withIce : true } , friendId )
// buy round for the house (second argument omitted)
sendDrink ( { drink : 'mezcal' , withIce : false } )
// listen for drinks sent to you
getDrink ( ( data , peerId ) =>
console . log (
`got a ${ data . drink } with ${ data . withIce ? '' : 'out' } ice from ${ peerId } `
)
)
您也可以使用操作發送二進位數據,例如影像:
const [ sendPic , getPic ] = room . makeAction ( 'pic' )
// blobs are automatically handled, as are any form of TypedArray
canvas . toBlob ( blob => sendPic ( blob ) )
// binary data is received as raw ArrayBuffers so your handling code should
// interpret it in a way that makes sense
getPic (
( data , peerId ) => ( imgs [ peerId ] . src = URL . createObjectURL ( new Blob ( [ data ] ) ) )
)
假設我們希望用戶能夠命名自己:
const idsToNames = { }
const [ sendName , getName ] = room . makeAction ( 'name' )
// tell other peers currently in the room our name
sendName ( 'Oedipa' )
// tell newcomers
room . onPeerJoin ( peerId => sendName ( 'Oedipa' , peerId ) )
// listen for peers naming themselves
getName ( ( name , peerId ) => ( idsToNames [ peerId ] = name ) )
room . onPeerLeave ( peerId =>
console . log ( ` ${ idsToNames [ peerId ] || 'a weird stranger' } left` )
)
操作非常智能,可以在幕後為您處理序列化和分塊。這意味著您可以發送非常大的文件,並且您發送的任何資料都將在另一端以相同類型接收(數字作為數字,字串作為字串,物件作為對象,二進製作為二進位等) 。
以下是如何建立音訊聊天室的簡單範例:
// this object can store audio instances for later
const peerAudios = { }
// get a local audio stream from the microphone
const selfStream = await navigator . mediaDevices . getUserMedia ( {
audio : true ,
video : false
} )
// send stream to peers currently in the room
room . addStream ( selfStream )
// send stream to peers who join later
room . onPeerJoin ( peerId => room . addStream ( selfStream , peerId ) )
// handle streams from other peers
room . onPeerStream ( ( stream , peerId ) => {
// create an audio instance and set the incoming stream
const audio = new Audio ( )
audio . srcObject = stream
audio . autoplay = true
// add the audio to peerAudio object if you want to address it for something
// later (volume, etc.)
peerAudios [ peerId ] = audio
} )
對視訊執行相同的操作是類似的,只需確保將傳入串流新增至 DOM 中的視訊元素即可:
const peerVideos = { }
const videoContainer = document . getElementById ( 'videos' )
room . onPeerStream ( ( stream , peerId ) => {
let video = peerVideos [ peerId ]
// if this peer hasn't sent a stream before, create a video element
if ( ! video ) {
video = document . createElement ( 'video' )
video . autoplay = true
// add video element to the DOM
videoContainer . appendChild ( video )
}
video . srcObject = stream
peerVideos [ peerId ] = video
} )
假設您的應用程式支援發送各種類型的文件,並且您希望使用有關如何解釋它們的元資料來註釋發送的原始位元組。您可以簡單地在二進位負載的傳送者操作中傳遞元資料參數,而不是手動將元資料位元組新增至緩衝區:
const [ sendFile , getFile ] = makeAction ( 'file' )
getFile ( ( data , peerId , metadata ) =>
console . log (
`got a file ( ${ metadata . name } ) from ${ peerId } with type ${ metadata . type } ` ,
data
)
)
// to send metadata, pass a third argument
// to broadcast to the whole room, set the second peer ID argument to null
sendFile ( buffer , null , { name : 'The Courierʼs Tragedy' , type : 'application/pdf' } )
操作發送者函數傳回一個承諾,該承諾在發送完成後解析。您可以選擇使用它來指示使用者何時完成大量傳輸。
await sendFile ( amplePayload )
console . log ( 'done sending to all peers' )
動作發送器函數還採用一個可選的回調函數,該函數將在傳輸過程中不斷呼叫。這可用於向寄件者顯示大額傳輸的進度條。使用 0 到 1 之間的百分比值以及接收對等點的 ID 來呼叫回呼:
sendFile (
payload ,
// notice the peer target argument for any action sender can be a single peer
// ID, an array of IDs, or null (meaning send to all peers in the room)
[ peerIdA , peerIdB , peerIdC ] ,
// metadata, which can also be null if you're only interested in the
// progress handler
{ filename : 'paranoids.flac' } ,
// assuming each peer has a loading bar added to the DOM, its value is
// updated here
( percent , peerId ) => ( loadingBars [ peerId ] . value = percent )
)
同樣,您可以作為接收者監聽進度事件,如下所示:
const [ sendFile , getFile , onFileProgress ] = room . makeAction ( 'file' )
onFileProgress ( ( percent , peerId , metadata ) =>
console . log (
` ${ percent * 100 } % done receiving ${ metadata . filename } from ${ peerId } `
)
)
請注意,任何元資料都會與進度事件一起傳送,因此您可以向接收使用者顯示正在進行傳輸,並且可能會顯示傳入檔案的名稱。
由於對等點可以並行發送多個傳輸,因此您還可以使用元資料來區分它們,例如透過發送唯一的 ID。
一旦對等點相互連接,他們的所有通訊都會進行端對端加密。在初始連接/發現過程中,對等點的 SDP 透過所選對等策略介質發送。預設情況下,SDP 使用從您的應用程式 ID 和房間 ID 派生的金鑰進行加密,以防止明文會話資料出現在日誌中。這對於大多數用例來說都很好,但是中繼策略操作員可以使用房間和應用程式 ID 對金鑰進行逆向工程。更安全的選項是在應用程式配置物件中傳遞password
參數,該參數將用於派生加密金鑰:
joinRoom ( { appId : 'kinneret' , password : 'MuchoMaa$' } , 'w_a_s_t_e__v_i_p' )
這是一個必須提前知道的共享秘密,並且密碼必須與房間中的所有對等方相匹配,以便他們能夠連接。一個範例用例可能是私人聊天室,使用者透過外部方式獲知密碼。
trystero函數是冪等的,因此它們已經可以像 React hooks 一樣開箱即用。
這是一個簡單的範例組件,其中每個同伴將他們最喜歡的顏色同步給其他人:
import { joinRoom } from ' trystero '
import { useState } from 'react'
const trystero Config = { appId : 'thurn-und-taxis' }
export default function App ( { roomId } ) {
const room = joinRoom ( trystero Config , roomId )
const [ sendColor , getColor ] = room . makeAction ( 'color' )
const [ myColor , setMyColor ] = useState ( '#c0ffee' )
const [ peerColors , setPeerColors ] = useState ( { } )
// whenever new peers join the room, send my color to them:
room . onPeerJoin ( peer => sendColor ( myColor , peer ) )
// listen for peers sending their colors and update the state accordingly:
getColor ( ( color , peer ) =>
setPeerColors ( peerColors => ( { ... peerColors , [ peer ] : color } ) )
)
const updateColor = e => {
const { value } = e . target
// when updating my own color, broadcast it to all peers:
sendColor ( value )
setMyColor ( value )
}
return (
< >
< h1 > trystero + React </ h1 >
< h2 > My color: </ h2 >
< input type = "color" value = { myColor } onChange = { updateColor } />
< h2 > Peer colors: </ h2 >
< ul >
{ Object . entries ( peerColors ) . map ( ( [ peerId , color ] ) => (
< li key = { peerId } style = { { backgroundColor : color } } >
{ peerId } : { color }
</ li >
) ) }
</ ul >
</ >
)
}
聰明的讀者可能會注意到上面的例子很簡單,並沒有考慮我們是否要更改組件的房間ID或卸載它。對於這些場景,您可以使用這個簡單的useRoom()
鉤子來相應地取消訂閱房間事件:
import { joinRoom } from ' trystero '
import { useEffect , useRef } from 'react'
export const useRoom = ( roomConfig , roomId ) => {
const roomRef = useRef ( joinRoom ( roomConfig , roomId ) )
const lastRoomIdRef = useRef ( roomId )
useEffect ( ( ) => {
if ( roomId !== lastRoomIdRef . current ) {
roomRef . current . leave ( )
roomRef . current = joinRoom ( roomConfig , roomId )
lastRoomIdRef . current = roomId
}
return ( ) => roomRef . current . leave ( )
} , [ roomConfig , roomId ] )
return roomRef . current
}
要使用 Supabase 策略:
appId
,複製anon public
API 金鑰並將其設定為trystero配置中的supabaseKey
如果您想使用 Firebase 策略且沒有現有項目:
databaseURL
並將其用作trystero配置中的appId
{
"rules" : {
".read" : false ,
".write" : false ,
"__ trystero __" : {
".read" : false ,
".write" : false ,
"$room_id" : {
".read" : true ,
".write" : true
}
}
}
}
這些規則確保只有在事先知道房間名稱空間的情況下,房間對等方的存在才是可讀的。
joinRoom(config, roomId, [onError])
將本地用戶新增至房間,同一命名空間中的其他對等方將開啟通訊通道並發送事件。使用相同的命名空間多次呼叫joinRoom()
將傳回相同的房間實例。
config
- 包含以下鍵的配置物件:
appId
- (必要)標識您的應用程式的唯一字串。使用 Supabase 時,應將其設定為您的專案 URL(請參閱 Supabase 設定說明)。如果使用 Firebase,這應該是 Firebase 配置中的databaseURL
(另請參閱下面的firebaseApp
,以了解配置 Firebase 策略的替代方法)。
password
- (可選)一個字串,用於在會話描述透過對等互連媒體時透過 AES-GCM 對其進行加密。如果未設置,會話描述將使用從應用程式 ID 和房間名稱派生的金鑰進行加密。房間中的任何對等點之間的自訂密碼必須匹配才能進行連接。有關詳細信息,請參閱加密。
rtcConfig
- (可選)為所有對等連線指定自訂RTCConfiguration
。
relayUrls
- (可選,僅 BitTorrent、Nostr、MQTT)用於引導 P2P 連接的策略的自訂 URL 清單。它們分別是 BitTorrent 追蹤器、Nostr 中繼器和 MQTT 代理程式。它們必須支援安全的 WebSocket 連線。
relayRedundancy
- (可選,僅限 BitTorrent、Nostr、MQTT)整數,指定要同時連接多少個 torrent 追蹤器,以防某些追蹤器失敗。傳遞relayUrls
選項將導致該選項被忽略,因為將使用整個清單。
supabaseKey
- (必需,僅限 ⚡️ Supabase)您的 Supabase 專案的anon public
API 金鑰。
firebaseApp
- (可選,僅限 Firebase)您可以傳遞已初始化的 Firebase 應用程式實例而不是appId
。通常, trystero會根據appId
初始化 Firebase 應用程序,但如果您已經初始化它以便在其他地方使用,則會失敗。
rootPath
- (可選,僅限 Firebase)字串,指定trystero在資料庫中寫入匹配資料的路徑(預設為'__ trystero __'
)。如果您想使用相同資料庫執行多個應用程式並且不想擔心命名空間衝突,則更改此設定非常有用。
libp2pConfig
- (可選,?僅限 IPFS) Libp2pOptions
,您可以在其中指定用於引導的靜態對等點清單。
roomId
- 房間內命名空間對等點和事件的字串。
onError(details)
- (可選)如果由於密碼不正確而無法加入房間,將呼叫的回調函數。 details
是一個包含appId
、 roomId
、 peerId
和描述錯誤的error
物件。
使用以下方法傳回一個物件:
leave()
從房間中刪除本地用戶並取消訂閱房間活動。
getPeers()
返回房間中存在的對等點的RTCPeerConnection
映射(不包括本地用戶)。該物件的鍵是各個對等點的 ID。
addStream(stream, [targetPeers], [metadata])
向其他對等方廣播媒體串流。
stream
- 帶有音訊和/或視訊的MediaStream
,可發送給房間中的其他人。
targetPeers
- (可選)如果指定,則流僅傳送至目標對等點 ID(字串)或對等點 ID 清單(陣列)。
metadata
- (可選)要隨流發送的附加元資料(任何可序列化類型)。這在發送多個串流時非常有用,以便接收者知道哪個是哪個(例如網路攝影機與螢幕截圖)。如果您想要使用元資料參數向房間中的所有對等方廣播串流,請傳遞null
作為第二個參數。
removeStream(stream, [targetPeers])
停止向其他對等方發送先前發送的媒體串流。
stream
- 先前發送的MediaStream
以停止發送。
targetPeers
- (可選)如果指定,則僅從目標對等 ID(字串)或對等 ID 清單(陣列)中刪除流。
addTrack(track, stream, [targetPeers], [metadata])
將新媒體軌道新增至串流。
track
- 新增到現有串流的MediaStreamTrack
。
stream
- 要附加新軌道的目標MediaStream
。
targetPeers
- (可選)如果指定,則追蹤僅傳送至目標對等點 ID(字串)或對等點 ID 清單(陣列)。
metadata
- (可選)與軌道一起發送的附加元資料(任何可序列化類型)。有關更多詳細信息,請參閱上面addStream()
的metadata
註釋。
removeTrack(track, stream, [targetPeers])
從串流中刪除媒體軌。
track
- 要刪除的MediaStreamTrack
。
stream
- 軌道附加到的MediaStream
。
targetPeers
- (可選)如果指定,則僅從目標對等 ID(字串)或對等 ID 清單(陣列)中刪除追蹤。
replaceTrack(oldTrack, newTrack, stream, [targetPeers])
用新媒體軌道取代媒體軌道。
oldTrack
- 要刪除的MediaStreamTrack
。
newTrack
- 要附加的MediaStreamTrack
。
stream
- oldTrack
附加到的MediaStream
。
targetPeers
- (可選)如果指定,則僅取代目標對等點 ID(字串)或對等點 ID 清單(陣列)的軌道。
onPeerJoin(callback)
註冊一個回調函數,當同伴加入房間時將呼叫該函數。如果多次調用,則僅調用最新的註冊回調。
callback(peerId)
- 每當加入等點時執行的函數,使用對等點的 ID 進行呼叫。例子:
onPeerJoin ( peerId => console . log ( ` ${ peerId } joined` ) )
onPeerLeave(callback)
註冊一個回調函數,當同伴離開房間時將呼叫該函數。如果多次調用,則僅調用最新的註冊回調。
callback(peerId)
- 每當對等點離開時執行的函數,使用對等點的 ID 進行呼叫。例子:
onPeerLeave ( peerId => console . log ( ` ${ peerId } left` ) )
onPeerStream(callback)
註冊一個回呼函數,當對等方發送媒體流時將呼叫該函數。如果多次調用,則僅調用最新的註冊回調。
callback(stream, peerId, metadata)
- 每當對等方發送媒體流時運行的函數,使用對等方的流、ID 和可選元資料呼叫(有關詳細信息,請參閱上面的addStream()
) 。例子:
onPeerStream ( ( stream , peerId ) =>
console . log ( `got stream from ${ peerId } ` , stream )
)
onPeerTrack(callback)
註冊一個回呼函數,當對等方發送媒體軌道時將呼叫該函數。如果多次調用,則僅調用最新的註冊回調。
callback(track, stream, peerId, metadata)
- 每當對等方發送媒體軌道時運行的函數,使用對等方的軌道、附加流、ID 和可選元資料進行呼叫(有關詳細信息,請參閱上面的addTrack()
)。例子:
onPeerTrack ( ( track , stream , peerId ) =>
console . log ( `got track from ${ peerId } ` , track )
)
makeAction(actionId)
監聽並傳送自訂資料操作。
actionId
- 用於在所有對等點之間一致註冊此操作的字串。傳回三個函數的陣列:
向對等點發送資料並傳回一個承諾,該承諾在所有目標對等點完成接收資料時解析。
(data, [targetPeers], [metadata], [onProgress])
data
- 要傳送的任何值(原始值、物件值、二進位值)。序列化和分塊是自動處理的。二進位資料(例如Blob
、 TypedArray
)由其他對等點作為不可知的ArrayBuffer
接收。
targetPeers
- (可選)對等 ID(字串)、對等 ID 陣列或null
(指示發送到房間中的所有對等)。
metadata
- (可選)如果資料是二進位的,您可以傳送描述它的可選元資料物件(請參閱二進位元資料)。
onProgress
- (可選)一個回調函數,將在傳輸每個對等方的每個區塊時呼叫。該函數將使用 0 到 1 之間的值以及對等 ID 進行呼叫。有關範例,請參閱進度更新。
註冊一個回調函數,該函數在從其他對等方收到此操作的資料時運行。
(data, peerId, metadata)
data
- 發送方傳輸的值。反序列化是自動處理的,即數字將作為數位接收,物件作為物件接收,等等。
peerId
- 發送方的 ID 字串。
metadata
- (可選)如果data
是二進位的,例如檔案名,則由發送者提供的可選元資料物件。
註冊一個回調函數,該函數在從對等點接收到部分資料時運行。您可以使用它來追蹤大型二進位傳輸。有關範例,請參閱進度更新。
(percent, peerId, metadata)
percent
- 0 到 1 之間的數字,表示傳輸完成的百分比。
peerId
- 發送方的 ID 字串。
metadata
- (可選)發送者提供的可選元資料物件。
例子:
const [ sendCursor , getCursor ] = room . makeAction ( 'cursormove' )
window . addEventListener ( 'mousemove' , e => sendCursor ( [ e . clientX , e . clientY ] ) )
getCursor ( ( [ x , y ] , peerId ) => {
const peerCursor = cursorMap [ peerId ]
peerCursor . style . left = x + 'px'
peerCursor . style . top = y + 'px'
} )
ping(peerId)
取得對等點 ID 並傳回一個承諾,該承諾解析為到該對等點的往返時間所花費的毫秒數。使用它來測量延遲。
peerId
- 目標對等點的對等 ID 字串。例子:
// log round-trip time every 2 seconds
room . onPeerJoin ( peerId =>
setInterval (
async ( ) => console . log ( `took ${ await room . ping ( peerId ) } ms` ) ,
2000
)
)
selfId
一個唯一的 ID 字串,其他對等方將在全域範圍內了解本機使用者。
getRelaySockets()
(僅?BitTorrent、?Nostr、?MQTT)傳回映射到其 WebSocket 連線的中繼 URL 鍵的物件。這對於確定使用者與中繼的連線狀態以及處理任何連線故障非常有用。
例子:
console . log ( trystero . getRelaySockets ( ) )
// => Object {
// "wss://tracker.webtorrent.dev": WebSocket,
// "wss://tracker.openwebtorrent.com": WebSocket
// }
getOccupants(config, roomId)
(僅限 Firebase)傳回解析為給定命名空間中存在的使用者 ID 清單的承諾。這對於檢查房間中有多少用戶而不加入房間非常有用。
config
- 配置對象roomId
- 您傳遞給joinRoom()
命名空間字串。例子:
console . log ( ( await trystero . getOccupants ( config , 'the_scope' ) ) . length )
// => 3
一次性設定1 | 捆綁尺寸² | 連接時間³ | |
---|---|---|---|
?諾斯特爾 | 沒有任何 ? | 54K | ⏱️⏱️ |
? MQTT | 沒有任何 ? | 332K | ⏱️⏱️ |
? BT | 沒有任何 ? | 25K? | ⏱️⏱️ |
⚡️蘇帕巴斯 | 〜5分鐘 | 15萬 | ⏱️? |
火力基地 | 〜5分鐘 | 177K | ⏱️? |
? IPFS | 沒有任何 ? | 945K | ⏱️⏱️ |
1除 Firebase 之外的所有策略都需要零設定。 Firebase 是一種託管策略,需要設定一個帳戶。
²透過 Rollup 捆綁 + Terser 壓縮計算。
³加入房間時同伴相互連結的相對速度。 Firebase 幾乎是即時的,而其他策略交換對等資訊的速度稍慢。
trystero的獨特優勢在於它需要零後端設置,並且在大多數情況下使用去中心化的基礎設施。這允許無摩擦的實驗並且沒有單點故障。一個潛在的缺點是,即使使用trystero使用的冗餘技術,也很難保證其使用的公共基礎設施始終具有高可用性。雖然其他策略是去中心化的,但 Supabase 和 Firebase 策略是一種更易於管理的方法,具有更強的控制力和 SLA,這可能更適合「生產」應用程式。
trystero使得策略之間的切換變得很簡單——只需更改單個導入行並快速進行實驗:
import { joinRoom } from ' trystero /[torrent|nostr|mqtt|supabase|firebase|ipfs]'
莫森貝克爾(Dan Motzenbecker)的trystero