在简单的 3D 立方体模型上使用 Three.JS 服务器发送的事件 (SSE) 很慢
Server sent events (SSE) is slow using Three.JS on a simple 3D cube model
我目前正在开发一个客户端-服务器应用程序,它使用 SSE 从数据中获取加速度计数据并将它们传递给 three.js 模型,然后在浏览器上呈现结果。
从技术上讲,应用程序应该在浏览器上仅在一个方向上实时显示加速度计数据,因此延迟很重要。
因为我只传递一个数据(即只有 accelerometerX),这个过程应该是快速和即时的;但是,将 accX 值传递给 object.position.x 甚至 console.log("Accelerometer X-Axis Data: " + (sensor value / 16384)) 需要很长时间;偶尔显示数据。
有时,整个浏览器崩溃显示 “WebGL 场景由于丢失上下文而无法呈现” 错误和非常长的“精度”错误。
到目前为止,我已经尝试了每一种方法,但我始终无法解决这个长期滞后的问题。
这是我从服务器得到的,一个 JSON 具有这种特定格式的文件:
"DMObjectsCompleteObject": [
{
"DataMapAddress": 1,
"DataType": 9,
"DefaultValue": 0,
"Description": "Accelerometer X Axis Data",
"MaxValue": 66,
"MinValue": 18446744073709552000,
"ReadOnly": false,
"Value": -18706.4
},
{
"DataMapAddress": 2,
"DataType": 9,
"DefaultValue": 0,
"Description": "Accelerometer Y Axis Data",
"MaxValue": 66,
"MinValue": 18446744073709552000,
"ReadOnly": false,
"Value": 128
}
]
这是我客户端的代码:
// Importing libraries and data
import * as THREE from "three";
if (!!window.EventSource) {
var source = new EventSource("/sse");
source.addEventListener('message', function (event) {
// Parameters initialization
const canvas = document.querySelector('#canvas');
const accelPanel = document.querySelector('#accelPanel');
const renderer = new THREE.WebGLRenderer({ canvas });
const fov = 70;
const aspect = 2; // the canvas default
const near = 20;
const far = 500;
// Initialize camera perspective
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
// The camera FOV when the model starts to move
camera.position.z = 25;
camera.up.set(0, 0, 1);
camera.lookAt(0, 0, 0);
// Add background grid and light
const scene = new THREE.Scene();
{
const color = 0x00afaf;
const intensity = 10;
const light = new THREE.PointLight(color, intensity);
scene.add(light);
}
// Make the 3D cube model with the XYZ axis
const boxGeometry = new THREE.BoxGeometry();
const boxMaterial = new THREE.MeshBasicMaterial({ color: "green", wireframe: false });
const object = new THREE.Mesh(boxGeometry, boxMaterial);
var cubeAxis = new THREE.AxesHelper(3);
object.add(cubeAxis);
object.scale.set(5, 5, 5)
scene.add(object);
scene.background = new THREE.Color(0.22, 0.23, 0.22);
let currentIndex = 0
let time = 0
let velocity = new THREE.Vector3()
requestAnimationFrame(render);
// Rendering function responsible of creating the translation motion
function render(dt) {
dt *= 0.0001 // in seconds
time += dt
document.querySelector("#time").textContent = time.toFixed(2)
// JSON.parse twice due to over-stringified string from SSE
var obj = JSON.parse(JSON.parse(event.data));
if (obj !== null) {
// Sensor data
if (
obj.hasOwnProperty("DataMapChangedObjectsAddressValue") &&
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"] !==
undefined
) {
let sensorAddr =
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"];
let sensorValue =
obj["DataMapChangedObjectsAddressValue"][0]["Value"];
//Accelerometer X Axis
//if(sensorAddr === this.despToAddrMap.get("Accelerometer X Axis Data")){
if (sensorAddr === 1) {
// console.log(obj["DataMapChangedObjectsAddressValue"][2]["Value"])
console.log("Accelerometer X Axis Data: " + (sensorValue / 16384));
}
object.position.x = (sensorValue / 16384) * 500;
document.querySelector("#accX").textContent = (sensorValue / 16384) * 500;
object.rotation.y = -70.68;
var relativeCameraOffset = new THREE.Vector3(5, 10, 1);
var cameraOffset = relativeCameraOffset.applyMatrix4(object.matrixWorld);
camera.position.x = cameraOffset.x;
// camera.position.y = cameraOffset.y;
// camera.position.z = cameraOffset.z;
camera.lookAt(object.position);
}
}
// // Find datapoint matching current time
// while (data[currentIndex].time < time) {
// currentIndex++
// if (currentIndex >= data.length) return
// }
// const { rotX, rotY, rotZ, accX, accY, accZ } = data[currentIndex]
// document.querySelector("#accX").textContent = accX;
// const acceleration = new THREE.Vector3(accX, accY, accZ)
// object.position.x = accX * 30;
// object.rotation.y = -70.68;
resizeToClient();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
function resizeToClient() {
const needResize = resizeRendererToDisplaySize()
if (needResize) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
function resizeRendererToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
}, false)
source.addEventListener('open', function (e) {
// document.getElementById('state').innerHTML = "Connected"
}, false)
source.addEventListener('error', function (e) {
const id_state = document.getElementById('state')
if (e.eventPhase == EventSource.CLOSED)
source.close()
if (e.target.readyState == EventSource.CLOSED) {
id_state.innerHTML = "Disconnected"
}
else if (e.target.readyState == EventSource.CONNECTING) {
id_state.innerHTML = "Connecting..."
}
}, false)
} else {
console.log("Your browser doesn't support SSE")
}
这是客户端呈现 3D 立方体的方式:
但它几乎不动,而且太迟钝了。
有人可以建议解决这个问题吗?将不胜感激。
这里活动部件太多,不知道是什么问题。
用类似的东西替换您的消息处理程序:
source.addEventListener('message', function (event) {
console.log(event)
}
现在确认您的加速度计数据实时到达控制台日志。 (这也是确认数据结构是否完全符合预期的好时机。)
如果它确实快速可靠地工作,那么 SSE 工作正常,你的问题只是关于 ThreeJS。 (一开始我会将您的静态函数移到消息处理程序之外 - 如果只是为了让代码更易于阅读。)
但是,如果您的数据流非常安静,然后一大块数据到达,然后在另一大块数据到达之前再次安静,这可能意味着您的服务器正在缓冲。您没有描述服务器端系统,但研究了刷新数据或关闭输出缓冲的选项。
如果数据不规律地到达,有时有效,有时无效,那么我会调查网络问题。浏览器中的开发人员工具可以帮助诊断此类问题。或者从等式中删除浏览器,并使用 curl(或类似工具)调试 SSE 流。
SSE 服务器的实现方式也很重要。
如果你使用 expressjs 风格,就像这个(抱歉代码不是很干净,它来自实时项目)
'use strict';
const util = require('util');
// const config = require('../lib/config');
const requireAuthorization = require('../middlewares/requireAuthorization');
const router = require('express').Router();
// const thinky = require('../lib/thinky');
// const r = thinky.r;
// const Errors = thinky.Errors;
const logger = require('../lib/logger');
const helpers = require('../lib/helpers');
// redis feed for events
const subscriber = require('../lib/redis').createClient();
const EventEmitter = require('events');
const spine = new EventEmitter();
subscriber.on('message', function (channel, message) {
spine.emit(channel, message);
});
// setInterval(function () {
// spine.emit('scubamailer_feed', JSON.stringify({time: Date.now()}));
// }, 500);
subscriber.subscribe('scubamailer_feed');
router.use(requireAuthorization);
router.get('/subscribe', function (req, res, next) {
// Good read -
// https://learn.javascript.ru/server-sent-events#tipy-sobytiy
// https://www.terlici.com/2015/12/04/realtime-node-expressjs-with-sse.html
// http://whosebug.com/questions/27898622/server-sent-events-stopped-work-after-enabling-ssl-on-proxy
// http://whosebug.com/a/33414096/1885921
// https://github.com/expressjs/compression/issues/17
// how to remove listeners
// https://odetocode.com/blogs/scott/archive/2013/07/16/angularjs-listening-for-destroy.aspx
logger.info('User %s subscribed to event feed from IP %s.', req.user.email, helpers.extractIPfromReq(req), {
user: req.user.email,
type: 'user/unsubFromEvents'
});
req.isSSE = true; // PLEASE, DO NOT TOUCH IT, OK???
req.socket.setTimeout(24 * 60 * 60 * 1000);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
// redis feed events
const listen = function (message) {
res.write(util.format('event: notification\ndata: %s\n\n', message));
// res.flush(); // https://github.com/expressjs/compression#server-sent-events
};
const tickerInterval = setInterval(function () {
res.write(util.format('event: time\ndata: %s\n\n', Date.now()));
}, 500);
const stopListening = function () {
logger.info('User %s [%s] unsubscribed from event feed...', req.user.email, helpers.extractIPfromReq(req), {
user: req.user.email,
type: 'user/unsubFromEvents'
});
clearInterval(tickerInterval);
spine.removeListener('scubamailer_feed', listen);
};
res.once('close', stopListening);
res.once('finish', stopListening);
spine.on('scubamailer_feed', listen);
const entitiesToMonitor = [
'Campaign',
'EmailImport',
'EmailExport',
'User',
'Server'
];
return Promise.all(entitiesToMonitor.map(function (entity) {
return req.model[entity].changes()
.then(function (feed) {
feed.each(function (error, doc) {
if (error) {
throw error;
}
if (doc.isSaved()) { // send updates only if document is persisted in database
if (entity === 'User') {
res.write(util.format('event: %s\ndata: %s\n\n', entity, JSON.stringify(doc.formatToJSON())));
} else {
res.write(util.format('event: %s\ndata: %s\n\n', entity, JSON.stringify(doc)));
}
// res.flush(); // https://github.com/expressjs/compression#server-sent-events
}
});
return Promise.resolve();
});
}))
.catch(next);
});
module.exports = exports = router;
如您所见,您需要提供所有 headers 所需
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
X-Accel-Buffering 需要 nginx 才能正确处理 SSE 提要。
此外,如果您使用 nodejs,确保压缩中间件不会压缩 SSE 流的端点可能很重要,因为它会进行事件传递 laggy
我目前正在开发一个客户端-服务器应用程序,它使用 SSE 从数据中获取加速度计数据并将它们传递给 three.js 模型,然后在浏览器上呈现结果。
从技术上讲,应用程序应该在浏览器上仅在一个方向上实时显示加速度计数据,因此延迟很重要。
因为我只传递一个数据(即只有 accelerometerX),这个过程应该是快速和即时的;但是,将 accX 值传递给 object.position.x 甚至 console.log("Accelerometer X-Axis Data: " + (sensor value / 16384)) 需要很长时间;偶尔显示数据。 有时,整个浏览器崩溃显示 “WebGL 场景由于丢失上下文而无法呈现” 错误和非常长的“精度”错误。 到目前为止,我已经尝试了每一种方法,但我始终无法解决这个长期滞后的问题。
这是我从服务器得到的,一个 JSON 具有这种特定格式的文件:
"DMObjectsCompleteObject": [
{
"DataMapAddress": 1,
"DataType": 9,
"DefaultValue": 0,
"Description": "Accelerometer X Axis Data",
"MaxValue": 66,
"MinValue": 18446744073709552000,
"ReadOnly": false,
"Value": -18706.4
},
{
"DataMapAddress": 2,
"DataType": 9,
"DefaultValue": 0,
"Description": "Accelerometer Y Axis Data",
"MaxValue": 66,
"MinValue": 18446744073709552000,
"ReadOnly": false,
"Value": 128
}
]
这是我客户端的代码:
// Importing libraries and data
import * as THREE from "three";
if (!!window.EventSource) {
var source = new EventSource("/sse");
source.addEventListener('message', function (event) {
// Parameters initialization
const canvas = document.querySelector('#canvas');
const accelPanel = document.querySelector('#accelPanel');
const renderer = new THREE.WebGLRenderer({ canvas });
const fov = 70;
const aspect = 2; // the canvas default
const near = 20;
const far = 500;
// Initialize camera perspective
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
// The camera FOV when the model starts to move
camera.position.z = 25;
camera.up.set(0, 0, 1);
camera.lookAt(0, 0, 0);
// Add background grid and light
const scene = new THREE.Scene();
{
const color = 0x00afaf;
const intensity = 10;
const light = new THREE.PointLight(color, intensity);
scene.add(light);
}
// Make the 3D cube model with the XYZ axis
const boxGeometry = new THREE.BoxGeometry();
const boxMaterial = new THREE.MeshBasicMaterial({ color: "green", wireframe: false });
const object = new THREE.Mesh(boxGeometry, boxMaterial);
var cubeAxis = new THREE.AxesHelper(3);
object.add(cubeAxis);
object.scale.set(5, 5, 5)
scene.add(object);
scene.background = new THREE.Color(0.22, 0.23, 0.22);
let currentIndex = 0
let time = 0
let velocity = new THREE.Vector3()
requestAnimationFrame(render);
// Rendering function responsible of creating the translation motion
function render(dt) {
dt *= 0.0001 // in seconds
time += dt
document.querySelector("#time").textContent = time.toFixed(2)
// JSON.parse twice due to over-stringified string from SSE
var obj = JSON.parse(JSON.parse(event.data));
if (obj !== null) {
// Sensor data
if (
obj.hasOwnProperty("DataMapChangedObjectsAddressValue") &&
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"] !==
undefined
) {
let sensorAddr =
obj["DataMapChangedObjectsAddressValue"][0]["DataMapAddress"];
let sensorValue =
obj["DataMapChangedObjectsAddressValue"][0]["Value"];
//Accelerometer X Axis
//if(sensorAddr === this.despToAddrMap.get("Accelerometer X Axis Data")){
if (sensorAddr === 1) {
// console.log(obj["DataMapChangedObjectsAddressValue"][2]["Value"])
console.log("Accelerometer X Axis Data: " + (sensorValue / 16384));
}
object.position.x = (sensorValue / 16384) * 500;
document.querySelector("#accX").textContent = (sensorValue / 16384) * 500;
object.rotation.y = -70.68;
var relativeCameraOffset = new THREE.Vector3(5, 10, 1);
var cameraOffset = relativeCameraOffset.applyMatrix4(object.matrixWorld);
camera.position.x = cameraOffset.x;
// camera.position.y = cameraOffset.y;
// camera.position.z = cameraOffset.z;
camera.lookAt(object.position);
}
}
// // Find datapoint matching current time
// while (data[currentIndex].time < time) {
// currentIndex++
// if (currentIndex >= data.length) return
// }
// const { rotX, rotY, rotZ, accX, accY, accZ } = data[currentIndex]
// document.querySelector("#accX").textContent = accX;
// const acceleration = new THREE.Vector3(accX, accY, accZ)
// object.position.x = accX * 30;
// object.rotation.y = -70.68;
resizeToClient();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
function resizeToClient() {
const needResize = resizeRendererToDisplaySize()
if (needResize) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
function resizeRendererToDisplaySize() {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
}, false)
source.addEventListener('open', function (e) {
// document.getElementById('state').innerHTML = "Connected"
}, false)
source.addEventListener('error', function (e) {
const id_state = document.getElementById('state')
if (e.eventPhase == EventSource.CLOSED)
source.close()
if (e.target.readyState == EventSource.CLOSED) {
id_state.innerHTML = "Disconnected"
}
else if (e.target.readyState == EventSource.CONNECTING) {
id_state.innerHTML = "Connecting..."
}
}, false)
} else {
console.log("Your browser doesn't support SSE")
}
这是客户端呈现 3D 立方体的方式:
但它几乎不动,而且太迟钝了。 有人可以建议解决这个问题吗?将不胜感激。
这里活动部件太多,不知道是什么问题。
用类似的东西替换您的消息处理程序:
source.addEventListener('message', function (event) {
console.log(event)
}
现在确认您的加速度计数据实时到达控制台日志。 (这也是确认数据结构是否完全符合预期的好时机。)
如果它确实快速可靠地工作,那么 SSE 工作正常,你的问题只是关于 ThreeJS。 (一开始我会将您的静态函数移到消息处理程序之外 - 如果只是为了让代码更易于阅读。)
但是,如果您的数据流非常安静,然后一大块数据到达,然后在另一大块数据到达之前再次安静,这可能意味着您的服务器正在缓冲。您没有描述服务器端系统,但研究了刷新数据或关闭输出缓冲的选项。
如果数据不规律地到达,有时有效,有时无效,那么我会调查网络问题。浏览器中的开发人员工具可以帮助诊断此类问题。或者从等式中删除浏览器,并使用 curl(或类似工具)调试 SSE 流。
SSE 服务器的实现方式也很重要。 如果你使用 expressjs 风格,就像这个(抱歉代码不是很干净,它来自实时项目)
'use strict';
const util = require('util');
// const config = require('../lib/config');
const requireAuthorization = require('../middlewares/requireAuthorization');
const router = require('express').Router();
// const thinky = require('../lib/thinky');
// const r = thinky.r;
// const Errors = thinky.Errors;
const logger = require('../lib/logger');
const helpers = require('../lib/helpers');
// redis feed for events
const subscriber = require('../lib/redis').createClient();
const EventEmitter = require('events');
const spine = new EventEmitter();
subscriber.on('message', function (channel, message) {
spine.emit(channel, message);
});
// setInterval(function () {
// spine.emit('scubamailer_feed', JSON.stringify({time: Date.now()}));
// }, 500);
subscriber.subscribe('scubamailer_feed');
router.use(requireAuthorization);
router.get('/subscribe', function (req, res, next) {
// Good read -
// https://learn.javascript.ru/server-sent-events#tipy-sobytiy
// https://www.terlici.com/2015/12/04/realtime-node-expressjs-with-sse.html
// http://whosebug.com/questions/27898622/server-sent-events-stopped-work-after-enabling-ssl-on-proxy
// http://whosebug.com/a/33414096/1885921
// https://github.com/expressjs/compression/issues/17
// how to remove listeners
// https://odetocode.com/blogs/scott/archive/2013/07/16/angularjs-listening-for-destroy.aspx
logger.info('User %s subscribed to event feed from IP %s.', req.user.email, helpers.extractIPfromReq(req), {
user: req.user.email,
type: 'user/unsubFromEvents'
});
req.isSSE = true; // PLEASE, DO NOT TOUCH IT, OK???
req.socket.setTimeout(24 * 60 * 60 * 1000);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
// redis feed events
const listen = function (message) {
res.write(util.format('event: notification\ndata: %s\n\n', message));
// res.flush(); // https://github.com/expressjs/compression#server-sent-events
};
const tickerInterval = setInterval(function () {
res.write(util.format('event: time\ndata: %s\n\n', Date.now()));
}, 500);
const stopListening = function () {
logger.info('User %s [%s] unsubscribed from event feed...', req.user.email, helpers.extractIPfromReq(req), {
user: req.user.email,
type: 'user/unsubFromEvents'
});
clearInterval(tickerInterval);
spine.removeListener('scubamailer_feed', listen);
};
res.once('close', stopListening);
res.once('finish', stopListening);
spine.on('scubamailer_feed', listen);
const entitiesToMonitor = [
'Campaign',
'EmailImport',
'EmailExport',
'User',
'Server'
];
return Promise.all(entitiesToMonitor.map(function (entity) {
return req.model[entity].changes()
.then(function (feed) {
feed.each(function (error, doc) {
if (error) {
throw error;
}
if (doc.isSaved()) { // send updates only if document is persisted in database
if (entity === 'User') {
res.write(util.format('event: %s\ndata: %s\n\n', entity, JSON.stringify(doc.formatToJSON())));
} else {
res.write(util.format('event: %s\ndata: %s\n\n', entity, JSON.stringify(doc)));
}
// res.flush(); // https://github.com/expressjs/compression#server-sent-events
}
});
return Promise.resolve();
});
}))
.catch(next);
});
module.exports = exports = router;
如您所见,您需要提供所有 headers 所需
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
X-Accel-Buffering 需要 nginx 才能正确处理 SSE 提要。
此外,如果您使用 nodejs,确保压缩中间件不会压缩 SSE 流的端点可能很重要,因为它会进行事件传递 laggy