nodejs一直以異步io著稱,其語言特性尤其擅長于在realtime應(yīng)用中潘明,如聊天室等。在進(jìn)行實(shí)時(shí)應(yīng)用開發(fā)時(shí)秕噪,必不可少的需要用到 socket.io庫钳降,可以說,nodejs+socket.io在實(shí)時(shí)應(yīng)用中具有較好的表現(xiàn)能力腌巾。
??本文既然選擇以實(shí)時(shí)地圖應(yīng)用做個(gè)小例子遂填,那么選擇經(jīng)典的PostgreSQL/PostGIS作為地圖的數(shù)據(jù)庫。希望實(shí)現(xiàn)的是模擬數(shù)據(jù)庫數(shù)據(jù)插入了新的GPS坐標(biāo)澈蝙,而一旦數(shù)據(jù)發(fā)生改變吓坚,立刻將插入的GPS坐標(biāo)廣播到服務(wù)端,服務(wù)端廣播到所有的客戶端地圖上灯荧,進(jìn)行定位展示礁击。早期作者使用的是redis的廣播/訂閱機(jī)制,最近發(fā)現(xiàn)Pg數(shù)據(jù)庫的listen/notify也具備這種消息傳遞機(jī)制逗载。
??本文主要的socke.io廣播/訂閱參考官網(wǎng)哆窿,Pg的listen/notify自行谷歌,作者僅簡述一下自己如何考慮應(yīng)用的厉斟。
一 服務(wù)器端
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);//數(shù)據(jù)庫連接
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'; //監(jiān)聽數(shù)據(jù)庫的gps消息
var query = pgClient.query(sql);//開始數(shù)據(jù)庫消息監(jiān)聽
//數(shù)據(jù)庫一旦獲取通知挚躯,將通知消息通過socket.io發(fā)送到各個(gè)客戶端展示。
pgClient.on('notification', function (data) {
console.log(data.payload);
//socketio.sockets.emit('GPSCoor', data.payload); //與下面的等價(jià)
socketio.emit('GPSCoor', data.payload);//廣播給所有的客戶端
});
pgClient.connect();
二 數(shù)據(jù)庫端
建立一個(gè)測試表如下:
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);
對表的增刪改建立一個(gè)觸發(fā)器捏膨,觸發(fā)器中發(fā)送變化數(shù)據(jù)出去:
CREATE OR REPLACE FUNCTION process_t_gps() RETURNS TRIGGER AS $body$
DECLARE
rec record;
BEGIN
IF (TG_OP = 'DELETE') THEN
--插入的GPS都是4326的經(jīng)緯度,我們將在3857的谷歌底圖上顯示數(shù)據(jù)食侮,發(fā)送轉(zhuǎn)換后的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>實(shí)時(shí)地圖應(yīng)用</title>
<link rel="stylesheet" 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();
//接受服務(wù)端消息
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);//地圖新增點(diǎn)
break;
case 'UPDATE':
var geom=wktform.readGeometry(data.geom);
var feature=gpsSource.getFeatureById(data.id);
if(feature)
feature.setGeometry(geom);//修改已有點(diǎn)
break;
case 'DELETE':
var feature=gpsSource.getFeatureById(data.id);
if(feature)
gpsSource.removeFeature(feature);//刪除點(diǎn)
break;
}
});
}
</script>
</head>
<body onload="init()">
<div id="map"></div>
</body>
</html>
客戶端接收到消息后号涯,改變當(dāng)前地圖上的圖標(biāo)gps坐標(biāo)位置。
四 測試與結(jié)果
連開三個(gè)客戶端連接如下:
4.1 數(shù)據(jù)庫新增GPS坐標(biāo)
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));
頁面自動(dòng)響應(yīng)效果如下:
4.2 數(shù)據(jù)庫修改GPS坐標(biāo)
查看下當(dāng)前的數(shù)據(jù)如下:
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的坐標(biāo)改成 150,40:
Test=# update t_gps set geom=st_geomfromtext('Point(150 40)',4326) where id=25;
UPDATE 1
服務(wù)器端打印如下:
4.3 數(shù)據(jù)庫刪除GPS坐標(biāo)
Test=# delete from t_gps where id=25;
DELETE 1
所有以上操作锯七,只是數(shù)據(jù)的增刪改指令链快,服務(wù)器和客戶端都是自動(dòng)響應(yīng)的。
結(jié)論:本文實(shí)現(xiàn)了眉尸,數(shù)據(jù)庫一旦廣播了消息域蜗,服務(wù)器端監(jiān)聽,并繼續(xù)以sockeio廣播到客戶端噪猾。全部過程霉祸,只是數(shù)據(jù)庫發(fā)送了一個(gè)坐標(biāo)消息無任何其他操作。pg的notify和listen消息機(jī)制钓株,真實(shí)應(yīng)用一般比如寫在觸發(fā)器中颤枪,觸發(fā)器監(jiān)聽是否有數(shù)據(jù)采集終端將新坐標(biāo)寫入或者更新汉操,然后在觸發(fā)器中notify消息余素,這樣奔穿,前端實(shí)時(shí)響應(yīng)镜沽。可以做到將終端應(yīng)用位置無任何操作的一波流發(fā)送到全部客戶端實(shí)時(shí)展示贱田。