基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(二)

我们上一篇《基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(一)》主要讲解了如何搭建一个实时数据通讯服务器,客户端与服务端是如何通讯的,相信通过上一篇的讲解,再配合上数据库的数据储存,我们就可以实现一个简易版的 Web 聊天工具了,有空的朋友可以自己尝试下实现,那么我们今天的主要内容真的是实现 WebGL 3D 拓扑图实时数据通讯了,请大家接着往下看。

基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(二)

有了前面的知识储备,我们就可以来真正实现我们 3D 拓扑图组件上节点位置信息的实时数据同步了,毋庸置疑,节点的位置信息必须是在服务端统筹控制,才能达到实时数据同步,也就是说,我们必须在服务端创建 DataModel 来管理节点,创建 ForceLayout 弹力布局节点位置,并在节点位置改变的过程中,实时地将位置信息推送到客户端,让每个客户端都更新各自页面上面的节点位置。

在服务端我们该如何创建 HT 的 DataModel 和 ForceLayout 呢?其实也很简单,我们可以看看下面的代码:

var ht = global.ht = this.ht = require('../../../build/ht-debug.js').ht,
 dataModel = new ht.DataModel(),
 reloadModel = require("../util.js").reloadModel;
reloadModel(dataModel, { A: 3, B: 5 });
require("../../../build/ht-forcelayout-debug.js");
var forceLayout = new ht.layout.Force3dLayout(dataModel);
forceLayout.onRelaxed = function() {
 var result = {};
 dataModel.each(function(data) {
 if (data instanceof ht.Node) {
 result[data.getTag()] = data.p3();
 }
 });
 io.emit('result', result);
};
forceLayout.start();

我们通过 require 将非 Node.js 模块包引入到程序中,并加以使用。在上面的代码中,我们确实创建了 HT 的拓扑节点,是通过 util.js 文件中的 relowdModel 方法创建的节点,那这个文件中到底是怎么实现创建 HT 拓扑节点的呢?接下来就来看看具体的实现:

function createNode(dataModel, id){
 var node = new ht.Node();
 node.setId(id);
 node.setTag(id);
 node.s3(40, 40, 40);
 node.s({
 'shape3d': 'sphere',
 'note': id,
 'note.position': 17,
 'note.background': 'yellow',
 'note.color': 'black',
 'note.autorotate': true,
 'note.face': 'top'
 });
 dataModel.add(node);
 return node;
}
function createEdge(dataModel, source, target){
 var edge = new ht.Edge(source, target);
 edge.s({
 'edge.width': 10,
 'shape3d.color': '#E74C3C',
 'edge.3d': true
 });
 dataModel.add(edge);
 return edge;
}
function reloadModel(dataModel, info){
 dataModel.clear();
 var ip = "192.168.1.";
 var count = 0;
 var root = createNode(dataModel, ip + count++);
 for (var i = 0; i < info.A; i++) {
 var iNode = createNode(dataModel, ip + count++);
 createEdge(dataModel, root, iNode);
 for (var j = 0; j < info.B; j++) {
 var jNode = createNode(dataModel, ip + count++);
 createEdge(dataModel, iNode, jNode);
 }
 }
}
this.reloadModel = reloadModel;

在这个文件中,封装了创建节点的方法 createNode,和创建连线的方法 createEdge,最后是通过 reloadModel 方法将前面的两个方法连接起来,在这个文件的最后,我们可以看到,只公开了 reloadModel 的函数接口。

当然光这些是不够的,这些还不能够达成实时数据通讯的功能,我们还需要监听和派发一些事件才能够达到效果,那么我们都监听了什么借口,派发了什么事件呢?

io.on('connection', function(socket) {
 socket.emit('ready', dataModel.serialize(0));
 console.log('a user connected');
 socket.on('disconnect', function() {
 console.log('user disconnected');
 });
 socket.on('moveMap', function(moveMap) {
 dataModel.sm().cs();
 for (var id in moveMap) {
 var data = dataModel.getDataByTag(id);
 if (data) {
 data.p3(moveMap[id]);
 dataModel.sm().as(data);
 }
 }
 });
});

上面那串代码是我们的事件监听,我们通过监听 moveMap 的事件,并获取从客户端传递上来的移动的节点坐标信息,根据参数的内容,我们将其改变服务端的 DataModel 中对应节点的坐标,改变后 ForceLayout 就会根据当前的状态去调整整个拓扑上所有节点的位置。那么在调节的过程中,我们是怎么知道 ForceLayout 是正在调整的呢?在前面介绍如何在 Node.js 上面创建 HT 相关的组件时贴出来的代码中就告诉我么怎么做了。

在创建 ForceLayout 组件的代码后面,紧跟着就是重载 ForceLayout 组件的 onRelaxed 方法,每次布局玩后,都会调用这个方法,这样我们就可以在这个方法中,编辑获取到 DataModel 中的所有节点的当前位置,并通过 io.emit 方法通知给所有的客户端,让客户端去实时更新对应节点的坐标位置。

基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(二)

但是还有一个问题,我们要怎么样让客户端显示的节点和服务端上的节点一一对应呢?首先不能让客户端自己创建节点,我们的做法其实也很简单,虽然不能保证客户端的节点 ID 会和服务端的节点 ID 一模一样,但是我们可以保证其他关键属性是一样,因为我们利用了 HT 的序列化功能,当有客户端连接到服务器时,就会向客户端派发 ready 事件,将 DataModel 序列化的结果返回到客户端,让客户端反序列化,从而达到数据基本一致的效果。

那么客户端和服务端的节点是如何保持一一对应的呢?首先我们得了解 HT 在获取节点对象上提供了几个方法,熟悉的朋友应该知道,有 getDataById 和 getDataByTag 两个方法,其中 ID 是 HT 系统自己维护的属性,Tag 是提供给用户自己维护其唯一性的属性,一般不建议使用 ID 作为业务上面的唯一标识,因为在序列化和反序列化时候可能会有细微的差别,很难保证反序列话后的节点 ID 和序列化前的 ID 是一样的。因此在本文中,我们是通过 Tag 属性来控制服务器和客户端的节点一一对应的。

接下来我们来看看客户端的实现吧:

<!DOCTYPE html>
<html>
 <head>
 <meta charset="utf-8">
 <title></title>
 <script src="/socket.io/socket.io.js"></script>
 <script src="/build/ht-debug.js"></script>
 <script>
 var socket = io();
 var init = function() {
 var dm = window.dataModel = new ht.DataModel(),
 sm = dm.sm(),
 g3d = new ht.graph3d.Graph3dView(dm);
 g3d.setGridSize(100);
 g3d.setGridGap(100);
 g3d.setGridVisible(true);
 g3d.addToDOM();
 var moveNodes = null;
 g3d.mi(function(evt){
 if ( evt.kind === 'beginMove'){
 moveNodes = sm.getSelection();
 }
 else if (evt.kind === 'betweenMove'){
 moveMap = {};
 g3d.sm().each(function(data){
 if(data instanceof ht.Node){
 moveMap[data.getTag()] = data.p3();
 console.info(data.p3());
 }
 });
 socket.emit('moveMap', moveMap);
 }
 else if (evt.kind === 'endMove') {
 moveNodes = null;
 }
 });
 socket.on('ready', function(json) {
 dm.clear();
 dm.deserialize(json);
 });
 socket.on('result', function (result) {
 for(var id in result){
 var data = dm.getDataByTag([id]);
 if (!data)
 continue;
 if (moveNodes && moveNodes.indexOf(data) >= 0)
 continue;
 data.p3(result[id]);
 }
 });
 };
 </script>
 </head>
 <body onload="init();">
 </body>
</html>

代码并不长,我来介绍下具体的实现。首先是创建 3D 拓扑图组件,并做一些设置,让场景上出现线条,然后就是监听拓扑图上面的操作,当监听到 betweenMove 时,或许当前被移动的节点位置信息,向服务器派发该信息;接下来是监听服务器的 ready 事件,在事件回调中做了反序列化的操作,但是在反序列化之前,为什么要将场景中的所有节点 Clear 掉呢?是因为页面有可能是断线重连,如果是断线重连的话,没有将场景中的节点都 Clear 掉的话,反序列化后就会有节点重叠了,而且 Tag 属性也不再是唯一的了,所以这时候操作节点的话,将会很混乱;最后呢,就是监听服务器的 result 事件,在事件的回调中,跟新回调参数中对应节点的位置信息,但是其中做了些过滤,这是过滤正在移动的节点,因为正在移动的节点位置是认为控制的,所有不需要更新其节点位置信息。

基于 WebSocket 实现 WebGL 3D 拓扑图实时数据通讯同步(二)

那么实时数据通讯系列到这里就介绍完了,如有什么问题,欢迎批评指正。

相关推荐