Jsynchronous.js
将您快速变化的应用状态与所有连接的浏览器同步。
Jsynchronous 确保所有客户端看到的数据与服务器上的数据相同 - 即使数据正在变化。速度快到足以用于游戏,灵活到可以用于图形应用,并经过精确测试。
在 Node.js 服务器上使用 jsynchronous 注册一个普通的 JavaScript 数组或对象,连接的浏览器上就会有一个完全相同的副本:
// 服务器端
const physics = {velocity: {x: 5, y: 1.01}};
const $ynchronized = jsynchronous(physics);
// 浏览器端
console.log(jsynchronous());
{ velocity: {x: 5, y: 1.01} }
对该变量的更改将自动传达给浏览器,以便它们始终保持同步:
// 服务器端
$ynchronized.velocity.x += 5;
$ynchronized.velocity.y -= 9.81;
// 浏览器端
console.log(jsynchronous());
{ velocity: {x: 10, y: -8.8} }
以下是您可以用 jsynchronous 同步的各种数据类型:
const data = {
string: '$†®îñG',
integer: 123467890,
floating: 3.141592653589793,
bigint: BigInt('12345678901234567890'),
null: null,
undefined: undefined,
bool: true,
array: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233],
deep: {a: {very: {deeply: {nested: {data: ['structure']}}}}},
circular: {a: {b: {c: null}}}
}
data.circular.a.b.c = data.circular;
const $ynced = jsynchronous(data);
Jsynchronous 还可以处理服务器之间的同步,或(实验性地)浏览器到服务器的同步。
设置
按照示例代码片段进行轻松集成。
Jsynchronous 不会将您限制在特定的传输媒介上,无论是 socket.io、ws、Primus、EventSource 还是 webRTC。任何具有最终有序交付的协议都可以使用。在这个例子中,我们将使用 ws。
服务器端设置包含 3 个必需步骤:
- 指定 jsynchronous.send 函数
- 通过调用 jsynchronous() 创建同步变量
- 使用 .$ync(websocket) 将连接的 WebSocket 注册到您的同步变量
// 服务器端使用 ws 库
const jsynchronous = require('jsynchronous');
const WebSocket = require('ws');
jsynchronous.send = (websocket, data) => websocket.send(data);
const $matrix = jsynchronous([[1, 2], [3, 4]]);
const wss = new WebSocket.Server({port: 8080});
wss.on('connection', (websocket) => {
$matrix.$ync(websocket);
websocket.on('close', () => $matrix.$unsync(websocket));
});
jsynchronous.send = (websocket, data) => {} 函数在每次需要向客户端发送数据时都会被 jsynchronous 自动调用。它必须包含将数据传输到 WebSocket 客户端的代码。
调用 jsynchrounous() 会创建并返回一个同步变量。在这个例子中是 $matrix
。调用 $matrix.$ync(websocket)
将使该同步变量对该 WebSocket 可见。
我们在同步变量名中使用 $
是因为这是一种约定。您不必这样做,但在代码中有一些指示,表明对服务器上这个变量的更改会导致网络通信,这是很好的做法。
现在数据正在从服务器发送,让我们关注客户端。在浏览器中,可以通过以下两种方式之一访问 jsynchronous 客户端:
<script src="/jsynchronous-client.js"></script>
import jsynchronous from 'jsynchronous/jsynchronous-client.js';
第一种方法需要作为静态资源提供,或者从 CDN 提供。第二种方法需要使用像 webpack 这样的打包工具。
// 浏览器端
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (data) => jsynchronous.onmessage(data.data);
就这么简单!在客户端查看同步变量的内容:
// 浏览器端
console.log(jsynchronous());
查看示例设置中的示例代码以获取指导。
还有一个可选步骤是启用浏览器到服务器的通信,这有助于从网络中断中恢复,我们将在下面讨论。
替代变量
在服务器上调用一次 jsynchronous() 而不提供名称将创建一个主变量。客户端可以通过在建立通信后调用一次 jsynchronous() 来查看主同步变量。
**在客户端上变量尚未同步时调用 jsynchronous() 将导致错误!**为了避免这种情况,请在第一个参数中指定预期的类型 'array' 或 'object':
// 浏览器端
const $matrix = jsynchronous('array');
如果客户端尚未完成同步,$matrix 将返回一个提供的类型的替代变量。替代变量最初只是空数组或对象。当客户端同步并更新后,替代变量将相应更新。
替代变量的替代方案是等待调用 jsynchronous()。您可以随时使用 jsynchronous.list() 查看哪些同步变量可用。
多个同步变量
在服务器上多次调用 jsynchronous() 将创建额外的同步变量,但您必须通过传入名称作为第二个参数来命名它们:
// 服务器端
const $greetings = jsynchronous({text: 'hello world'}, 'greetings');
在客户端上,通过在第二个参数中引用其名称来检索它:
// 客户端
const $greetings = jsynchronous('object', 'greetings');
您可以使用 jsynchronous.list() 查看名称列表。
连接中断和重新同步
TCP/IP 连接重置的原因有很多。失去服务、关闭笔记本电脑或计算机进入睡眠状态、用手机进入地下或电梯、在以太网、Wi-Fi 或蜂窝数据之间切换。有时路由器或网络本身会出现故障。许多 WebSocket 库会在 TCP/IP 中断后恢复会话,但不保证在 TCP/IP 连接断开时发送的消息能够送达。
无论用户断开连接多长时间,Jsynchronous 都会确保用户重新连接时进行重新同步。它通过对所有消息进行编号并重新请求缺失的范围来实现这一点 - 这通常是 TCP/IP 处理的... 当它没有中断时。
为了支持重新同步请求,需要客户端到服务器的通信。您需要在客户端添加 jsynchronous.send,并在服务器上调用 jsynchronous.onmessage(websocket, data) 来接收发送的数据。类似于之前的设置,但在客户端和服务器上都有 .send 和 .onmessage。还要注意服务器端函数需要传入 websocket。
// 服务器端
jsynchronous.send = (websocket, data) => websocket.send(data);
const $ynchronized = jsynchronous(['Quoth', 'the', 'raven', '"Nevermore."']);
const wss = new WebSocket.Server({port: 8080});
wss.on('connection', (websocket) => {
$ynchronized.$ync(websocket);
websocket.on('message', (data) => jsynchronous.onmessage(websocket, data));
websocket.on('close', () => $ynchronized.$unsync(websocket));
});
// 浏览器端
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (data) => jsynchronous.onmessage(data.data);
jsynchronous.send = (data) => ws.send(data);
const $ynchronized = jsynchronous('object');
设置客户端到服务器的通信可以使您的同步变量能够抵抗数据丢失和不同步。默认情况下,如果您没有在客户端提供.send或在服务器上提供.onmessage,客户端将给出警告,如果消息丢失且无法重新同步,则会停止。
并非所有应用程序都需要这种级别的保护,可以依赖TCP/IP提供的保证。通过在服务器上调用jsynchronous()时将{one_way: true}作为选项来完全禁用客户端到服务器的通信。如果您保持同步变量的初始结构不变,只更改原始值(字符串、数字、布尔值),而不给对象或数组分配任何新引用,单向模式效果很好。丢失的消息将被忽略,jsynchronous将尽最大努力继续更新已同步对象/数组的属性。
# 回溯模式
回溯模式是jsynchronous的一个强大功能。回溯模式允许您"回溯"到数据的先前快照。
想象一场国际象棋比赛。在普通模式下,不可能回退几步来查看过去棋盘的样子。使用回溯模式,您可以看到游戏中任何一步棋盘的样子。使用回溯模式,暂停、回溯和向前播放更改都成为可能。
通常,客户端一旦更新就会丢弃更改历史以节省内存。使用回溯模式,客户端从一开始就获得完整的历史记录。可以将历史应用于初始状态,以重建历史中的任何时刻。这被称为事件溯源。
通过在服务器上调用同步变量的.$napshot()来创建快照:
```javascript
// 服务器端
const $ynced = jsynchronous([], 'name', {rewind: true});
$ynced.push(Math.random());
$ynced.$napshot('one');
$ynced.push(Math.random());
$ynced.$napshot('two');
您可以通过调用.$rewind(name)回溯到快照
const previous = $ynced.$rewind('one');
如果您的客户端期望按照更改发生的顺序接收更改,而不考虑客户端何时连接到活跃变化的状态,回溯模式也可能很有用。
对于大型历史记录,重建当前状态可能在网络和计算上都很密集,因此请注意不要对启用回溯模式的变量应用无限数量的更改。
安全和权限
Jsynchronous中的权限设置很简单。如果您不希望某个websocket看到同步变量中包含的数据,就不要调用.$ync(websocket)。
建议为应用程序中不同级别的权限和可见性创建额外的同步变量。
事件
通过在同步变量上使用.$on('changes', callback)来观察、监听、触发更改事件:
$ynchronized.$on('changes', () => {})
目前这只在客户端可用。未来的版本将会看到更多类型的事件和更多跟踪更改的方式。敬请期待!
故障排除
最常见的错误是对传入jsynchronous()的数据进行赋值,而不是对jsynchronous()返回的同步变量进行赋值,这会让您疑惑为什么数据没有被传输。
// 服务器端
const physics = {x: 0, y: 0, z: 0}
const $ynced = jsynchronous(physics);
physics.x += 10; // 不会同步!
要在客户端看到更改,上述代码必须修改$ynced
,而不是physics
。Jsynchronous会创建您传入的变量的深度副本,完全忽略原始数据。在将对象或数组分配给同步变量时也是如此:
const quaternion = {w: 1, i: 0, j: 0, k: 0}
$ynced.orientation = quaternion; // 会同步
$ynced.orientation.w = 0; // 会同步
quaternion.i = 1; // 不会同步
在将对象和数组分配给同步变量时要小心。所有内容都将对.$ync()中的客户端可见。
另一方面,您可以从应用程序的其他部分引用同步变量。对这些引用的更改将会同步:
const $orientation = $ynced.orientation;
$orientation.i = 0; // 会同步
$orientation.j = 1; // 会同步
我们建议您在引用同步变量时使用前缀'$'或其他约定,以表明该变量是响应式的,对该变量的赋值将通过网络发送。
文档参考
Jsynchronous()函数调用
jsynchronous(data, name, options);
创建并返回一个同步变量。
在服务器上,data必须是一个对象或数组。 在客户端,data必须是匹配'array'或'object'的字符串。
Jsynchronous方法
send
// 服务器端
jsynchronous.send = (websocket, data) => {}
// 客户端
jsynchronous.send = (data) => {}
默认未定义,您必须将其分配给一个传输数据的函数。Websocket将匹配您在调用同步变量的$ync(websocket)方法时提供的值。
onmessage
// 服务器端
jsynchronous.onmessage(websocket, data);
// 浏览器端
jsynchronous.onmessage(data);
一个函数。您需要在传输的数据到达时调用onmessage。
list
// 服务器或浏览器端
jsynchronous.list();
返回变量名称的数组。
variables
// 服务器或浏览器端
jsynchronous.variables();
返回一个对象,键值对应名称->同步变量。
垃圾回收
jsynchronous.pausegc(); // 暂停jsynchronous垃圾回收
jsynchronous.resumegc(); // 恢复暂停的垃圾回收
jsynchronous.rungc(); // 同步运行垃圾回收器,忽略暂停
传递给jsynchronous(data, name, options)的选项
{send: <function>}
用特定于同步变量的send函数覆盖jsynchronous.send。默认未定义。
{rewind: true}
开启回溯模式。参见上文关于回溯模式的文档。默认为false。
{one_way: true}
指示客户端只读取数据,不在应用程序级别传输任何握手、心跳或重新同步请求。如果网络连接断开导致不同步,尽可能继续处理更改。参见上文关于连接中断和重新同步的文档。默认为false。
{wait: true}
告诉jsynchronous延迟同步,直到在同步变量上调用$tart()。默认为false。
{buffer_time: <number>}
在向客户端传输同步数据之前等待的毫秒数。默认为0。
{history_limit: <number>}
历史记录的最大大小。回溯模式忽略这个数字,因为回溯模式保存所有历史记录。默认为100000。
保留字
将同步变量可用的任何方法名作为选项键传递,以将该保留字覆盖为您提供的字符串值。
例如,要将方法名.$on()
更改为__on__()
,在调用jsynchronous()时传入{$on: '__on__'}
作为选项。这将在服务器和客户端上覆盖保留字。如果您预期根变量包含与现有保留字匹配的键,这很有用。
同步变量根级可用的方法
.$info()
客户端或服务器。返回一个对象,详细说明此同步变量的信息。对调试有用。
.$ync(websocket)
仅服务器。将客户端添加到监听器列表中。Websocket可以是字符串、数字或由您的websocket库提供的对象。Websocket必须是唯一标识客户端的值或引用。
.$unsync(websocket)
仅服务器。从客户端列表中移除websocket。客户端将不再接收任何更新。
.$copy()
服务器和客户端。返回同步变量的深度副本。返回的变量不同步。
.$on('changes', callback)
仅限客户端。创建一个事件监听器,在每批更改后触发回调。服务器事件将在未来版本中提供。
.$on('snapshot', callback)
仅限客户端。创建一个事件,在创建快照时触发回调,用于回溯模式。服务器事件将在未来版本中提供。
.$tart()
仅限服务器。与调用jsynchronous()时的{wait: true}选项一起使用,告诉jsynchronous开始将变量同步到已$ync的客户端。
.$listeners()
仅限服务器。您传入.$ync()调用的websocket列表。
.$napshot(name)
仅限服务器。创建快照,用于回溯模式。名称可以是数字或字符串。
.$rewind(name, [counter])
仅限客户端。名称可以是数字或字符串。返回一个非同步变量,其数据与创建该名称快照时同步变量的数据相匹配。如果未定义名称,将回溯到同步变量的编号变更计数器。服务器端回溯将在未来版本中提供。
常见问题
如何在React/Vue/Angular/Svelte中使用我的同步变量?
在同步变量的根上使用.$copy()来填充初始状态。
使用.$on('change')事件监听器来更新状态。
jsynchronous可以用来表示图形或复杂数据结构吗?
当然可以。无论是稀疏或密集的图形、树、链表、双向链表、循环和自引用数据结构,甚至可能是神经网络。如果JavaScript可以表示它,jsynchronous就可以轻松地同步它。
在笨重的传输栈表现力有限的世界中,jsynchronous旨在成为一股清新的空气,不限制您可以用它做什么。
jsynchronous真的可以用于游戏吗?
所有浏览器依赖的TCP/IP在丢包严重的情况下可能会因为队头阻塞而增加延迟。
对90%的游戏来说,基于TCP/IP的Jsynchronous是非常理想的。对于快速反应的射击游戏或格斗游戏可能不太适合。如果您的游戏不需要毫秒级精度,jsynchronous将以网络所能承载的最快速度将您的数据完美同步到每个客户端,您的websocket可以处理。
UDP可能会进入浏览器,这对快节奏游戏来说非常令人兴奋。虽然UDP不适合精确的数据同步,因为它不能确保传输或排序,但Jsynchronous的one_way模式将非常适合快速的尽力而为传输。
我可以进行浏览器->服务器同步吗?
保护应用程序安全的最佳方法是构建API或使用websocket进行浏览器->服务器通信。
可以在浏览器中导入jsynchronous.js,在服务器上导入jsynchronous-client.js。每个浏览器都必须为其同步变量唯一命名,以便服务器能够区分它们。
对于视频游戏,游戏状态可以从服务器->浏览器同步,用户输入可以从浏览器->服务器同步。这样,您可以轻松地让游戏循环响应用户输入的变化并更新所有客户端可见的游戏状态。
任何好奇的脚本编写者都可以打开浏览器控制台并随意修改您的同步变量。jsynchronous服务器设计为在受信任的环境中运行,而浏览器不是。您的websocket服务器必须进行速率限制和大小限制(maxHttpBufferSize/maxPayload),如果数据结构与您的预期不完全匹配,应该断开连接。
双向同步和客户端的更改呢?
客户端数据结构的更改不会反映在服务器或其他客户端上,并可能导致错误。
Jsynchronous是单向同步,而不是双向同步。这可能在未来得到实验性支持,但对于生产工作负载,强烈建议使用API或websocket命令,让服务器从其端更改jsynchronous变量。
这样做的原因是,相比服务器API/接口,保护客户端数据结构免受篡改、注入、DDoS或放大攻击要困难得多。代理和getter/setter是相对较新的JavaScript规范,通过不接受来自客户端的更改,这个库可以更容易地支持非常旧的浏览器。
使用像jsynchronous这样的响应式数据结构来管理双向请求以更改数据存在局限性。即使是像多个客户端同时增加这样简单的操作,在最后写入胜出的启发式方法中也可能丢失,因为i++在getter/setter看来与i=常量相同。某些数据类型可能需要比"设置"和"删除"更具表现力的操作。操作转换需要针对应用程序、意图和数据类型来处理合并冲突,即使是服务器><客户端冲突,通过API也比通过响应式数据结构更容易推理。
支持Jsynchronous
想要提供帮助吗?考虑捐赠,所有收益都用于jsynchronous的开发。
如果您想为这个开源项目贡献代码,请联系我们。