この記事は OUCC アドベントカレンダー 2015の10日目の記事です.
昨日は@E_Rubikさんによる診療内科へ行こうでした.明日は@spring_rainingさんです.
空いていたので埋めたは良いものの,特に書けるようなことがなかったので,最近調べていたことをまとめました. Djangoでリアルタイムにアンケート集計をするアプリの機能改善をしていて,Python+Ajaxではどうしても速度がでないので,node.jsでWebSocketを使って一部を高速化をしようといった内容です.
間違っている,こうした方が良い等ありましたら,私(@okwrtdsh)までお願いします.
- Django+Ajaxで0.5秒に1回jsonを取得してflotr2.jsで描画
- 数が多くなると遅延が無視できないレベル
- その他いろいろな改善点(この記事では触れない)
- AjaxをやめてWebSocketを使う
- 0.5秒毎ではなくDBに変更のある毎に
- node.js触ってみたい
ということで,WebSocketとPostgreSQLのNOTIFY/LISTENについて調べてみました.
HTTPはリアルタイムに適したプロトコルではなく,HTTPにリアルタイム性を持たせるための多様な迂回方法が開発され,サーバ・クライアント間の双方向で途切れのない連続した通信が実現しました. WebScketはこれら既存の仕様を利用,迂回して双方向の通信を実現するのではなく,双方向通信を提供することを目的に新たに策定されたプロタコルです. WebScketはHTTPを取り去り永続的にTCPのような通信を行うことによって,HTTPよりもオーバーヘッドや機能制限を取り除いています.
![seqdiag](http://utils.uci-sys.jp/seqdiag?seqdiag{ クライアント -> サーバ [label = "HTTP GET Upgrade Request"]; クライアント <- サーバ [label = "HTTP 101 Switching Protocols Response"]; クライアント <- サーバ [diagonal, label = "WebSocket"]; クライアント <- サーバ [diagonal, label = "WebSocket"]; クライアント -> サーバ [diagonal, label = "WebSocket"]; クライアント <- サーバ [diagonal, label = "WebSocket"]; })
クライアントから,WebSocketへのアップグレードを求めるGETリクエストをサーバに送り,サーバがWebSocketに対応しているか判断し,両方ともに対応している場合はHTTPは取り去られWebSocketでの通信に切り替わります.以降,双方向から任意のデータを送ることができるようになります.
詳しくはこちらをご覧下さい.
古いブラウザはWebSocketをサポートしていません.socket.ioは,実行中のブラウザが対応している中で最も適切なリアルタイム通信技術を使って,WebSocketもしくはWebSocketライクな双方向通信APIをサーバとクライアントに提供しています.古いブラウザ(IE5.5以降)やモバイルブラウザ(iOS Safari, Android)にも対応しています.これに加えて,socket.ioは切断検出,自動再接続や,カスタムイベント,ネームスペース,などなど便利な機能がそろっています.
詳しくはこちらをご覧下さい.
LISTEN channel_name;
で監視を開始し,NOTIFY channel_name;
で監視しているセッションに配信を行います.また,通知を送信する際にはpg_notify(channel_name, payload)
関数を利用することもできます.pg_notify関数は、NOTIFY文が関数になったもので、チャンネル名やペイロード(通知の際に使用できる任意の文字列)が決まっていない場合に便利とされています.
詳しくはこちらをご覧下さい.
![seqdiag](http://utils.uci-sys.jp/actdiag?actdiag{ 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 ->10 lane browser { label = "browser" 1 [label = "登録・変更"]; 9 [label = "データを受け取る"]; 10 [label = "描画"]; } lane django { label = "django server" 2 [label = "保存処理"]; } lane postgresql { label = "postgresql server" 3 [label = "INSERT OR UPDATE"]; 4 [label = "NOTIFY"]; 7 [label = "SQL VIEW"]; } lane nodejs { label = "node.js server" 5 [label = "LISTEN"]; 6 [label = "viewに問い合わせ"]; 8 [label = "結果をwebsocketでデータを送る"]; } })
通知を送るためにトリガ関数を作ります.listen_notification
チャンネルに通知を送るようにします.
CREATE OR REPLACE FUNCTION notify_trigger()
RETURNS trigger AS
$BODY$
DECLARE
BEGIN
PERFORM pg_notify('listen_notification', TG_TABLE_NAME || ',id,' || NEW.id );
RETURN new;
END;
$BODY$
LANGUAGE plpgsql;
トリガを作ります.staff_schedule
のテーブルに挿入,変更,削除があった後に先ほど作成したnotify_trigger
関数を呼びます.※アンケートのプロジェクトが手元になかったので適当なプロジェクトを使っています.
CREATE TRIGGER staff_schedule_trigger
AFTER INSERT OR UPDATE OR DELETE
ON staff_schedule
FOR EACH ROW
EXECUTE PROCEDURE notify_trigger();
これで通知を送る側の設定は終わりです.node.js側でlisten_notification
チャンネルを監視するようにします.通知があると通知内容をconsoleに表示します.
var pg = require('pg');
var pgConString = "protocol://user:password@host:port/dbname";
pg.connect(pgConString, function(err, client){
if(err){
console.log(err);
process.exit();
}
client.on('notification', function(msg){
console.log(msg);
});
client.query("LISTEN listen_notification");
});
サーバ側の作成をします.カスタムイベントを作成してデータを送るようにします.クライアント(index.html)は後で説明します.
var fs = require("fs");
var http = require("http");
var socketio = require("socket.io");
var io;
var server = http.createServer(function(req, res){
res.writeHead(200, {"Content-Type": "text/html"});
var output = fs.readFileSync("./index.html", "utf-8");
res.end(output);
}).listen(8080);
io = socketio.listen(server);
io.sockets.on("connection", function(socket){
socket.on("change", function(dict_list){
io.sockets.emit("change", dict_list);
});
});
集計結果を返すためにviewを作ります.※集計するようなものが無かった為,適当にstaff毎の登録数を返しています.
create view staff_attend_days as
select staff_id as id, count(*) as total from staff_schedule
group by staff_id;
viewに問い合わせて結果を返す処理を追加します.
var pg = require('pg');
var pgConString = "protocol://user:password@host:port/dbname";
pg.connect(pgConString, function(err, client){
if(err){
console.log(err);
process.exit();
}
client.on('notification', function(msg){
console.log(msg);
//追加 ここから
client.query("SELECT * FROM staff_attend_days", function(err, result){
if(err){
console.log(err);
}
else{
console.log(result.rows);
io.sockets.emit("change", result.rows);
}
});
//ここまで
});
client.query("LISTEN listen_notification");
});
これでサーバ側の設定は終わりです.クライアントで結果を受け取れるようにします.※とりあえず結果を表示するだけです.
<html>
<head>
<meta charset="UTF-8">
<title>log</title>
<script src="/socket.io/socket.io.js"></script>
<script type="text/javascript">
var socketio = io.connect('http://localhost:8080');
socketio.on("change", function (dict_list){
showLog(dict_list);
});
function showLog(dict_list) {
var logArea = document.getElementById('log');
var newDiv = document.createElement('div');
newDiv.innerHTML = ++i + " : " + JSON.stringify(dict_list);
logArea.appendChild(newDiv);
}
i = 0;
</script>
</head>
<body>
<h1>Log</h1>
<div id="log"></div>
</body>
</html>
これでDBに変更がある毎に,クライアント側でデータを受け取れるようになりました.
- consoleへの出力
{ name: 'notification',
length: 49,
processId: 83770,
channel: 'listen_notification',
payload: 'staff_schedule,id,88' }
[ { id: 2, total: '24' },
{ id: 1, total: '30' },
{ id: 3, total: '6' },
{ id: 4, total: '2' },
{ id: 5, total: '1' } ]
一人で試しているので実際のところ,速くなったとか全く分かりませんでした... オーバーヘッドが減ったことによりサーバへの負荷が減るのではないかと思います.(そもそもサーバ分けてるし...)
かなり雑ですが,以上です.今回作ったものはgithubにあげています.node.jsに関連は「Nodeクックブック(オライリージャパン)」を参考にしました.
OUCCではバイトが楽しくて仕方がないという奇特なバイト畜部員を募集しています.
以下,ホームページより抜粋.
OUCCは、複数のIT企業とつながりを持ち、プログラマのアルバイトなどをさせていただいています。
OUCCに入れば、プログラマのアルバイトを紹介してもらえるでしょう。
最近では、C#を用いたソフトの保守・修正やPHPによるWebアプリ製作などを行っています。
詳しくはこちら。