Edited at
ClassiDay 21

WebSocket使ってリアルタイムにグラフを描画するやり方

More than 1 year has passed since last update.

Classi Advent Calendar 2016 21日目です。

WebSocketの話をします。

実際のサービスでのご利用や、やろうと思ってる方にお役を立てればと思います。


WebSocket仕様

Webサーバとブラウザとの間の双方法通信規格である

プロトコル仕様

https://tools.ietf.org/html/rfc6455


XMLHttpRequestとの違い

ajax利用して、サーバとクライアントのデータやりとりができるが、頻繁にコネクションの接続切断が必要。サーバからクライアントに情報をプッシュすることが難しい。

WebSocketでは、サーバとクライアントがコネクッションを張った後は必要な通信全てはそのコネクション上で行う。


URIスキーム

ws

wss(SSL通信)


コネクションの成立手順

HTTP要求でハンドシィクした後、WebSocketへ切り替える事でWebSocketコネクションされる。

ほとんど問題ないですが、対応していないLBも稀にあるそうなので、独自でLB仕入れてる場合は気をつけたほうがいい。

スクリーンショット 2016-12-20 14.13.46.png


スケールアウトの話

実際のサービス運用では、WebSocketサーバは複数台で運用されることが多い。

しかし、ただ単にWebSocketサーバを複数台に並べるだけだと、期待通りにスケールできない問題があります。

グループチャット(nodejs+socket.io)を例にすると、

WebSocketサーバAに接続するユーザAとサーバBに接続するユーザBは同じチャットルームにいます。

ユーザAからのメッセージはWebSocketサーバBのプロセスに送れないため、メッセージはユーザBに届きません。

将来的にWebSocketクラスターや、クラスター機能もつミドルウェアなどが出てくればこれ解決しますが、というかすでにあるかもしれない(調べてません)。

解決方法として、下記のような構成でよく取られるのが現状

あるプロセスから発生したsocketイベントを別のプロセスに送れるような中継サーバ(pub/sub)を立てる

上記グループチャットを例にすると

ユーザA接続プロセスとユーザB接続プロセスは、同じチャットルーム(channel)をsubscribしておく

メッセージ送信が行われた場合は、チャットルームにpublishする

という手順


リアルタイムにグラフ描画の例

数行レベルでリアルタイムにグラフ描画のアプリができます(実用レベルではないですが


構成

node.js

socket.io

redis

d3.js


プログラム説明


app.js

WebSocket処理

バックエンドからメッセージ送信API

var app = require('express')();

var server = require('http').Server(app);
var io = require('socket.io')(server);
var cors = require('cors');

var redis = require("redis");
var pub = redis.createClient();
var sub = redis.createClient();

app.use(cors());
app.options('*', cors());

app.post('/p', function (req, res) {
pub.publish("test_channel", JSON.stringify([5,5,6,3,5,8,13]));
res.json({ message: 'success.' });
});

app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});

io.on('connection', function (socket) {
socket.emit('msg_from_back', { data: JSON.stringify([13,8,5,3,2,1,1]) });

socket.on('msg_from_front', function (data) {
console.log(data);
});

sub.on('message', function(chan, msg) {
console.log("redis channel msg : " + chan + ": " + msg);
socket.emit('msg_from_back', {data: msg});
});

sub.subscribe('test_channel');
});

server.listen(3000, function(){
console.log('listening on :3000');
});


index.html

パイグラフ描画

<!DOCTYPE html>

<meta charset="utf-8">
<div id="container">
</div>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="/socket.io/socket.io.js"></script>

<script>
var socket = io.connect('http://localhost:3000');
function draw_graph(data) {
$('#container').html('<canvas id="chart" height="500" width="960"></canvas>');
console.log(data);
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d");
var width = canvas.width,
height = canvas.height,
radius = Math.min(width, height) / 2;
var colors = [
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
];
var outerRadius = radius - 10,
cornerRadius = 12;
var arc = d3.arc()
.outerRadius(outerRadius)
.innerRadius(0)
.cornerRadius(cornerRadius)
.context(context);
var pie = d3.pie();
var arcs = pie(data);
context.translate(width / 2, height / 2);
context.globalAlpha = 0.5;
arcs.forEach(function(d, i) {
context.beginPath();
arc(d);
context.fillStyle = colors[i];
context.fill();
});
context.globalAlpha = 1;
context.beginPath();
arcs.forEach(arc);
context.lineWidth = 1.5;
context.strokeStyle = "#fff";
context.stroke();
context.beginPath();
context.strokeStyle = "#000";
context.stroke();
}
socket.on('msg_from_back', function (msg) {
$('#container').html('');
console.log(msg.data);
draw_graph(JSON.parse(msg.data));
socket.emit('msg_from_front', { msg: 'from front.' });
});
</script>


データ送信してみる

Redisクライアント

RestAPI(http://localhost:3000/p)

からデータ送ると、index.htmlのパイグラフがリアルタイムに描画される

スクリーンショット 2016-12-20 15.04.28.png


ソースコード

https://github.com/zhenghailong/pub-sub-graph


最後に

Classiではエンジニアを募集しています。

日本の教育に興味のある方は是非お越しください!