基於 WebSocket 實現 WebGL 3D 拓撲圖實時資料通訊同步(二)

圖撲軟體發表於2016-07-26

我們上一篇《基於 WebSocket 實現 WebGL 3D 拓撲圖實時資料通訊同步(一)》主要講解了如何搭建一個實時資料通訊伺服器,客戶端與服務端是如何通訊的,相信通過上一篇的講解,再配合上資料庫的資料儲存,我們就可以實現一個簡易版的 Web 聊天工具了,有空的朋友可以自己嘗試下實現,那麼我們今天的主要內容真的是實現 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 方法通知給所有的客戶端,讓客戶端去實時更新對應節點的座標位置。

但是還有一個問題,我們要怎麼樣讓客戶端顯示的節點和服務端上的節點一一對應呢?首先不能讓客戶端自己建立節點,我們的做法其實也很簡單,雖然不能保證客戶端的節點 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 事件,在事件的回撥中,跟新回撥引數中對應節點的位置資訊,但是其中做了些過濾,這是過濾正在移動的節點,因為正在移動的節點位置是認為控制的,所有不需要更新其節點位置資訊。

那麼實時資料通訊系列到這裡就介紹完了,如有什麼問題,歡迎批評指正。

相關文章