1. 程式人生 > >postgres+socket.io+nodejs實時地圖應用實踐

postgres+socket.io+nodejs實時地圖應用實踐

nodejs一直以非同步io著稱,其語言特性尤其擅長於在realtime應用中,如聊天室等。在進行實時應用開發時,必不可少的需要用到 socket.io庫,可以說,nodejs+socket.io在實時應用中具有較好的表現能力。
  本文既然選擇以實時地圖應用做個小例子,那麼選擇經典的PostgreSQL/PostGIS作為地圖的資料庫。希望實現的是模擬資料庫資料插入了新的GPS座標,而一旦資料發生改變,立刻將插入的GPS座標廣播到服務端,服務端廣播到所有的客戶端地圖上,進行定位展示。早期作者使用的是redis的廣播/訂閱機制,最近發現Pg資料庫的listen/notify也具備這種訊息傳遞機制。
  本文主要的socke.io廣播/訂閱參考官網,Pg的listen/notify自行谷歌,作者僅簡述一下自己如何考慮應用的。

一 伺服器端

        var fs = require('fs');
    var http = require('http');
    var socket = require('socket.io');
    var pg = require('pg');
    var util=require('util');
    
    var constr=util.format('%s://%s:%s@%s:%s/%s', 'postgres','postgres','123456','192.168.43.125',5432,'Test');
    var server = http.createServer(function(req, res) {
        res.writeHead(200, { 'Content-type': 'text/html'});
        res.end(fs.readFileSync(__dirname + '/index.html'));
    }).listen(8081, function() {
        console.log('Listening at: http://localhost:8081');
    });

    var pgClient = new pg.Client(constr);//資料庫連線
    var socketio=socket.listen(server);//socketio
    socketio.on('connection', function (socketclient) {
        console.log('已連線socket:');
        //socketclient.broadcast.emit('GPSCoor', data.payload);//廣播給別人
        //socketclient.emit('GPSCoor', data.payload);//廣播給自己
    
    });
    var sql = 'LISTEN gps'; //監聽資料庫的gps訊息
    var query = pgClient.query(sql);//開始資料庫訊息監聽
        //資料庫一旦獲取通知,將通知訊息通過socket.io傳送到各個客戶端展示。
    pgClient.on('notification', function (data) {
        console.log(data.payload);
        //socketio.sockets.emit('GPSCoor', data.payload); //與下面的等價
        socketio.emit('GPSCoor', data.payload);//廣播給所有的客戶端
            
    });
    pgClient.connect();
    

二 資料庫端

建立一個測試表如下:

create table t_gps(
          id serial not null,
          geom geometry(Point,4326),
          constraint t_gps_pkey primary key (id)
);
--建立索引
create index t_gps_geom_idx on t_gps using gist(geom);

對錶的增刪改建立一個觸發器,觸發器中傳送變化資料出去:

CREATE OR REPLACE FUNCTION process_t_gps() RETURNS TRIGGER AS $body$
    DECLARE
        rec record;
    BEGIN
        IF (TG_OP = 'DELETE') THEN
            --插入的GPS都是4326的經緯度,我們將在3857的谷歌底圖上顯示資料,傳送轉換後的3857出去
            select TG_OP TG_OP,OLD.id,ST_AsText(ST_Transform(OLD.geom,3857)) geom into rec;
            perform pg_notify('gps',row_to_json(rec)::text);
            RETURN OLD;
        ELSIF (TG_OP = 'UPDATE') THEN 
            select TG_OP TG_OP,NEW.id,ST_AsText(ST_Transform(NEW.geom,3857)) geom into rec;
            perform pg_notify('gps',row_to_json(rec)::text);
            RETURN NEW;
        ELSIF (TG_OP = 'INSERT') THEN
            select TG_OP TG_OP,NEW.id,ST_AsText(ST_Transform(NEW.geom,3857)) geom into rec;
            perform pg_notify('gps',row_to_json(rec)::text);
            RETURN NEW;
        END IF;
        RETURN NULL;
    END;
$body$ LANGUAGE plpgsql;

CREATE TRIGGER T_GPS_TRIGGER
AFTER INSERT OR UPDATE OR DELETE ON T_GPS
    FOR EACH ROW EXECUTE PROCEDURE process_t_gps();

三 客戶端

<html>
<head>
    <meta charset='utf-8'>
    <title>實時地圖應用</title>
    <link rel="stylesheet" href="http://openlayers.org/en/v3.18.2/css/ol.css" type="text/css">
    <script src="http://openlayers.org/en/v3.18.2/build/ol.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script>
            var wktform=new ol.format.WKT();//wkt解析
            var gpsSource=new ol.source.Vector();
            function init(){
                var gpsLayer=new ol.layer.Vector({
                    source:gpsSource,
                    style:new ol.style.Style({
                        image: new ol.style.Icon(({
                            anchor: [0.5, 1],
                            src: 'http://openlayers.org/en/v3.18.2/examples/data/icon.png'
                        }))
                    })
                });
                var map = new ol.Map({
                    layers : [
                        new ol.layer.Tile({
                            title : '街道圖',
                            visible : true,
                            source : new ol.source.XYZ({
                                url : 'http://www.google.cn/maps/vt?pb=!1m5!1m4!1i{z}!2i{x}!3i{y}!4i256!2m3!1e0!2sm!3i342009817!3m9!2szh-CN!3sCN!5e18!12m1!1e47!12m3!1e37!2m1!1ssmartmaps!4e0&token=32965'
                            })
                        }),
                        gpsLayer
                    ],
                    target : 'map',
                    controls : ol.control.defaults({
                        attributionOptions : 
                        ({
                            collapsible : false
                        })
                    }),
                    view : new ol.View({
                        center : [0, 0],
                        zoom : 2
                    })
                });
                var iosocket = io.connect();
                //接受服務端訊息
                iosocket.on('GPSCoor', function(data) {
                    data=JSON.parse(data);
                    switch(data.tg_op){
                        case 'INSERT':
                            var feature=new ol.Feature({
                                geometry:wktform.readGeometry(data.geom)
                            });
                            feature.setId(data.id);
                            gpsSource.addFeature(feature);//地圖新增點
                            break;
                        case 'UPDATE':
                            var geom=wktform.readGeometry(data.geom);
                            var feature=gpsSource.getFeatureById(data.id);
                            if(feature)
                                feature.setGeometry(geom);//修改已有點
                            break;
                        case 'DELETE':
                            var feature=gpsSource.getFeatureById(data.id);
                            if(feature)
                                gpsSource.removeFeature(feature);//刪除點
                            break;
                    }
                });
            }
            
           
    </script>
</head>
<body onload="init()">
    <div id="map"></div>
</body>
</html>

客戶端接收到訊息後,改變當前地圖上的圖示gps座標位置。

四 測試與結果

連開三個客戶端連線如下:

 

初始化三個客戶端.png

伺服器端socket連線成功.png

4.1 資料庫新增GPS座標

insert into t_gps(geom) values (st_geomfromtext('Point(0 0)',4326));
insert into t_gps(geom) values (st_geomfromtext('Point(118 32)',4326));
insert into t_gps(geom) values (st_geomfromtext('Point(-118 -32)',4326));

頁面自動響應效果如下:

伺服器端監聽到的資料庫訊息.png

伺服器端socket到客戶端的效果.png

4.2 資料庫修改GPS座標

檢視下當前的資料如下:

Test=# select id,st_astext(geom) from t_gps;
 id |    st_astext    
----+-----------------
 24 | POINT(0 0)
 25 | POINT(118 32)
 26 | POINT(-118 -32)
(3 rows)

將id=25的座標改成 150,40:

Test=# update t_gps set geom=st_geomfromtext('Point(150 40)',4326) where id=25;
UPDATE 1

伺服器端列印如下:

顯示一條更新語句.png

更新效果.png

4.3 資料庫刪除GPS座標

Test=# delete from t_gps where id=25;
DELETE 1

顯示一條刪除.png

刪除效果.png

所有以上操作,只是資料的增刪改指令,伺服器和客戶端都是自動響應的。

結論:本文實現了,資料庫一旦廣播了訊息,伺服器端監聽,並繼續以sockeio廣播到客戶端。全部過程,只是資料庫傳送了一個座標訊息無任何其他操作。pg的notify和listen訊息機制,真實應用一般比如寫在觸發器中,觸發器監聽是否有資料採集終端將新座標寫入或者更新,然後在觸發器中notify訊息,這樣,前端實時響應。可以做到將終端應用位置無任何操作的一波流傳送到全部客戶端實時展示