构建即时多人网络应用程序,无需服务器
尝试一下演示吗?
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