読者です 読者をやめる 読者になる 読者になる

WebSocketで作る双方向通信へえボタン

HTML5 Node.js WebSocket フロントエンド技術

おもしろ関西人です。
もう一度書きます。おもしろ関西人です。
以前はとある後輩が面白さで競ってきてたのですが、最近こいつ面白くないんですよね・・・

さて、今回の記事では
社内向けのツールとして「へえボタン」を作ったときのことを書こうと思います。

対象読者

  1. WebSocketで何かを作ってみたい人
  2. へえボタンに興味がある人
  3. メンバー参加型のおもしろコンテンツを求めている人
  4. トリビアの泉を深夜時代から見ていた人
  5. ○ビる大木のへえボタンの押し方に納得がいかなかった人

へえボタンとは

トリビアの泉」という、2006年まで放送されていたバラエティ番組の中で使われていた小道具です。
役に立たないムダ知識(トリビア)に対する出演者からの評価の度合いを伝えるために使われていました。

今回作ったへえボタンはこういった物理的なものでなく、
Web上で動作するアプリケーションです。

なぜへえボタンを作ったのか

弊社には、毎朝オフィス内の社員が集まって
各種報告や社内施策を行う場である「朝会」があります。
メンバーの増加・オフィスの増床を経てもずっと継続されているSpeeeの施策のひとつです。

朝会は司会を行うメンバーによって独自のコンテンツをプラスすることがあります。
自分(ともう一人)が朝会の司会を担当したときに提案したのが
「毎日司会がトリビアを発表し、朝会参加者にへえボタンでリアクションをしてもらう」という
本日のトリビアコーナーでした。

司会メンバーが二人ともエンジニアということもあり、もちろんへえボタンは自作。
せっかく作るなら興味のある技術を使いたいので、
WebSocketを用いたへえボタンを作ることにしました。

ちなみに実際に朝会のトリビアコーナーで発表したトリビアにはこんなものがあります。

「ワタナベ」姓の人は節分に豆まきをする必要がない
ハーゲンダッツのアイスクリーム工場は世界に4箇所しかなく、そのうちの一つが日本の群馬県高崎市にある

WebSocketで作る双方向通信へえボタン

今回作成したへえボタンについて紹介します。

要件

・朝会の参加者は30〜50人程度
・参加者がへえボタンを押した回数はリアルタイムで集計される
・総へえ数は常に参加者側から確認できる

へえボタン外観

こちらが朝会参加者が使用するへえボタンです。

・ボタン部分をタッチすると「へえ〜」という音声と共にへえ数が増加
・画面上部に表示される全ユーザの総へえ数はリアルタイムで更新される

WebSocket通信の流れ

ブラウザ(へえボタン)側とWebSocketサーバ側との通信の流れを図で表すと以下のようになります。

・ブラウザ側が最新のステータスをリクエストした場合
・ブラウザでへえボタンを押してへえ数を増加させた場合
その時点での最新のステータス(総へえ数 & 対象ユーザのへえ数)をブラウザ側に返します。

・他クライアントがへえボタンを押して総へえ数が増えた場合
サーバ側から全クライアントに総へえ数の情報を送ります。
それにより各ユーザのブラウザで総へえ数がリアルタイムで更新されます。

へえボタンの実装

へえボタンアプリケーションのおおまかな実装方法です。

サーバ側

サーバ側のWebSocket実装にはNode.jsのwsモジュールを使っています。

まずアプリケーションを作成します。

var WebSocketServer = require('ws').Server;

HehhApplication = (function(){
  var wss;
  var actions = {};

  var _decodeData = function(data) {
    var decodedData = JSON.parse(data);
    if (!decodedData || !decodedData.action || !decodedData.data) {
      return false;
    }
    return decodedData;
  };

  var _encodeData = function(action, data) {
    if(!action) {
      return false;
    }
    var payload = {'action' : action, 'data' : data};
    return JSON.stringify(payload);
  }

  var _start = function(){
    if (!wss){
      wss = new WebSocketServer({port: 8000});

      wss.broadcast = function(data) {
        this.clients.forEach(function(client){
          if (client) {
            client.send(data);
          }
        });
      };
    }
    wss.on('connection', function(ws) {
      ws.on('message', function(message) {
        var decodedData = _decodeData(message);

        // message内で指定された任意のアクションを実行
        if (actions[decodedData.action]) {
          actions[decodedData.action](decodedData.data, ws);
        }
      });
    });
  };

  return {
    start : _start
  };
})();

HehhApplication.start();

次に、作ったアプリケーションにアクションを定義していきます。

今回実装したアクション

  1. statusアクション:対象ユーザのステータス(ユーザごとのへえ数、総へえ数)
  2. incrementアクション:へえ数を1増加

statusアクションの実装

actions.status = function(data, ws) {
  var user_id = data;
  var trivia_id = _createTriviaId(); // トリビアごとに振られたID.  今回は日付からIDを生成している

  // ユーザのへえ数, 総へえ数をリクエストしたユーザに送信
  var status = _getStatus(user_id, trivia_id);
  ws.send(_encodeData('status', status));
};

incrementアクションの実装

actions.increment = function(data, ws) {
  var user_id = data;
  var trivia_id = _createTriviaId();

  // 総へえ数に+1
  _increment(user_id, trivia_id);

  // ユーザのへえ数, 総へえ数をリクエストしたユーザに送信
  var status = _getStatus(user_id, trivia_id);
  ws.send(_encodeData('status', status));

  // 総へえ数をクライアント全てに送信
  wss.broadcast(_encodeData('total_status', status['total']));
};

クライアント側

クライアント側ではHTML5のWebSocket APIを利用します。

WebSocketサーバに接続

var ws = new WebSocket('ws://example.com:8000/hehh');

サーバ接続時イベント:現時点のステータスを要求

ws.onopen = function(){
  var data = {};
  data.action = "status";
  data.data = $.cookie("hehh_uid"); // Cookieに保存しているユーザID
  ws.send(JSON.stringify(data));
};

メッセージ受信イベント

ws.onmessage = function(event) {
  var data = JSON.parse(event.data);
  if (data.action === 'status') {
    // ユーザごとのへえ数と総へえ数の表示を更新
    _updateUserCount(data.count);
    _updateTotalCount(data.total);

  } else if (data.action === 'total_status') {
    // 総へえ数のみ表示を更新
    _updateTotalCount(data.total);

  }
};

へえボタンタップ時イベント

$("#hehh_btn").on("touchstart", function(e){
  // へえ音声の再生 + へえボタンアニメーション開始
  _startHehhAction();

  // WebSocketサーバにincrementメッセージ送信
  var data = {};
  data.action = "increment";
  data.data = $.cookie("hehh_uid");
  ws.send(JSON.stringify(data));
});

最小構成ですが、これでWebSocketサーバと通信する
へえボタンアプリケーションを作ることができました。

余談その1:へえボタンver1.0 (PHP製)について

実は今回紹介したへえボタンはver2.0です。
当初はWebSocketサーバをPHPで実装していました。

利用したライブラリ

しかし初回のトリビアコーナーでいきなり問題が発生!
司会がトリビアを発表し、参加者50人がへえボタンを連打した瞬間...

・合計へえ数が更新されない!
・WebSocketサーバが応答しない!

大量のへえ数+1リクエストをサーバが捌ききれなかったのです。
ネタ番組内のへえボタンユーザ数(5人)なら耐えられたかもしれませんが、今回はその10倍の規模。
○ビる大木のごとく絶え間なくへえボタンを連打していた参加者もいたので
その影響もあったのでしょう。

この問題はサーバの同時接続可能数をどれだけ増やしても解決しませんでした。

思考錯誤の末、トリビア発表時の急激なリクエスト増加に耐え切るためには
サーバ側がノンブロッキングな実装である必要がある
、と結論。
WebSocketサーバをNode.jsで作り直すことにしました。
それが今のへえボタンver2.0です。

ユーザ数や使用される状況から起こりうる問題を事前に想定し、
適切な実装方法を選ぶことが重要
だということを実感できた経験でした。

余談その2:トリビア収集システム

あなたが毎日トリビアを披露しなくてはいけない立場になったとして、
まず悩むことになるのが

・日々のトリビア収集が大変!
・披露するトリビアを選ぶのが大変!

ということでしょう。

そんなあなたにトリビア収集の自動化をお薦めします。

  1. Web上のトリビア情報をスクレイピングしてDBに格納
  2. トリビアDBからランダムに3〜5個選んだものを自分に通知
  3. 通知されたトリビアの中から良さそうなトリビアをピックアップ!

自分はこの仕組みで日々のトリビア収集を効率化していました。
極めればあなたも今日からトリビアマスター!
※収集するトリビアのクオリティが低いと結局選び直しになるので注意

最後に

今回はWebSocketを用いたへえボタンの実装について紹介しました。

へえボタンは作りこそシンプルなものの、
実際に運用してみると余談その1で書いたような
様々なトラブルが頻発すると思います。
もしこれから似たようなものを作ってみようという方がいるのなら、
この記事に書いてあることが少しでも参考になれば幸いです。

おもしろ関西人でした。