11
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ClassiAdvent Calendar 2016

Day 21

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

Last updated at Posted at 2016-12-21

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

ソースコード

最後に

Classiではエンジニアを募集しています。
日本の教育に興味のある方は是非お越しください!

11
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?