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

 [Classi Advent Calendar 2016](http://qiita.com/advent-calendar/2016/classi) 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](https://qiita-image-store.s3.amazonaws.com/0/15092/b490a119-39c0-3636-bcd6-6bd2295d5cc5.png "スクリーンショット 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

```javascript
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

パイグラフ描画

```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://qiita-image-store.s3.amazonaws.com/0/15092/fc0bc6fe-bcd6-9710-2315-7b6e2d1a08cc.png "スクリーンショット 2016-12-20 15.04.28.png")

### ソースコード
https://github.com/zhenghailong/pub-sub-graph

## 最後に
[Classiではエンジニアを募集しています。](https://www.wantedly.com/companies/classi/projects)
日本の教育に興味のある方は是非お越しください！