adish intelligence

アディッシュ株式会社のエンジニアブログです。

hitoboにおけるWebSocketとAPIの使い分け

こんにちは。2年目新卒エンジニアの坪井(@ufo_ocha)です。普段はhitoboの主にフロント周りを開発しています。この記事は Gaiax Advent Calendar 2016 の12日目の記事です。

最近WebSocketを使って、わざわざブラウザの更新しなくても、リアルタイムに画面が更新されるWebアプリも増えてきましたね。チャットサポートを提供するhitoboでもメイン機能のチャット機能を提供するためにWebSocketを使用しています。その際に、 Server => Clientへの通信はWebSocketを使うしかないのですが、悩むのが、Client => Serverの通信を、 「API経由で行うか?」 それとも、 「WebSocket経由で行うか?」 ということです。この記事では、WebSocketとAPIの使い分けについて、hitoboでの方針をお話します。

Socket.io

まずはWebSocketの実装ですが、定番のSocket.ioを使っています。Socket.ioはWebSocketを対応していないブラウザに対して、ポーリングで対応してくれたりと互換性を担保してくれます。また、それだけではなくnamespaceやroomの概念を提供してくれるので、チャットのルームなどの実装を楽に行うことができます。

hitoboではこれにスケールアウトできるように、socket.io-redisを用いています。socket.io-redisを用いて、redisを挟むと以下のようにソケットサーバが増えても対応することができます。

f:id:ufo_ocha:20161213150017p:plain

このあたりは色々な記事で説明されているので、詳細には触れません。が、これが本題のWebSocketとAPIの使い分けに役に立ちます。

WebSocketとAPIの使い分け

さて、本題ですが、結論から言うとhitoboではなるべくWebSocketではなく、APIに寄せるようにしています。

理由としては、 Socketサーバーが複雑になりメンテしづらい というのがあります。例えば、チャットを実装するとすると以下のようなコードになります。

const io = require('socket.io');

io.on('connection', socket => {
  // ここにコードを書いていく必要がある
  
  // ユーザからの投稿イベントを受け取る
  socket.on('post', message => {
    // 受け取ったメッセージを他のユーザに配信する
    socket.emit('display', message);
  });
});

クライアントサイドからのユーザの投稿イベントをemitし、ソケットサーバでそれを受け取る。受け取ったメッセージを他のユーザにemitする。といった流れです。これぐらいだと複雑に思えないかもしれませんが、ここに他のイベントも記述されていったり、受け取ったメッセージをDBに保存するなどと言った処理が増えると、かなりメンテしづらくなってしまいます。

こういった投稿をWebSocket経由ではなく、ユーザ投稿APIを用意することで、今までのサーバーサイドと同じように実装することができます。となると、 Client => Server はAPIで通信可能ですが、受け取ったメッセージを Server => Client に送るのはどうしたらいいのでしょう。ここでsocket.io-emitterを使用します。

socket.io-emitterとは

socket.io-emitterとは、socket.ioに先程少し触れたsocket.io-redisを組み込んだ実装をした場合に、直接redisに同じ形式でpublishすることで、socket.ioの外部のプロセスからイベントをemitすることができます。

f:id:ufo_ocha:20161213150111p:plain

これによって、API経由でユーザの投稿を受取り、socket.io-emitterを通じてemitすることで、DBに保存するなどはいつもどおり、サーバーサイドで実装することが可能になります。

こうすることによって、実装が複雑になりがちなソケットサーバはルームへのjoinやdisconnect時の処理など、ソケットサーバにしかできないような処理のみを記述するだけでよく、今までと同じようなAPIでのやり取りで実装することができます。もちろん、実際はユーザの投稿が失敗した時のリトライできるような処理が必要になったりと、こんな単純ではありませんが、APIの実装に集中できるのはメリットだと思います。

socket.io-emitterの注意

===2016/01/10 追記===
先日、socket.io-redisの3.0.0がリリースされました。
これによって依存パッケージが msgpack-lite に変更されたので、以下の注意は必要ないでしょう。
逆に今まで正常に動いていた場合、v3.0.0にアップデートする際は注意してください。
=== ===

socket.io-emitterはJavaScriptにかぎらず、RubyやPerl, PHPと様々な言語での実装がOSSとして公開されています。ここで少し注意なのが、(2016/12/12の時点で)socket.io-redisの内部で使っている msgpack-js が、msgpackの最新の仕様ではないという罠があります。JS版を利用するときはsocket.io-emitter内部で同じsocket.io-redisを使用しているので問題ないのですが、Ruby版を使う場合は少し注意が必要で gem install socket.io-emitter でインストールできるRuby実装のemitterではmsgpackが最新の仕様となっており、emitが上手くできません。

ただ、socket.io-redisも先日内部で使ってるパッケージを msgpack-js から msgpack-lite に変更するコミットがなされており、これによってmsgpackが最新の仕様となったため、正常に動作するようになりました。ただ、このコミットはまだリリースされていないため、現時点でRubyのsocket.io-emitterを使う場合は

$ npm install -S git+https://github.com/socketio/socket.io-redis.git

と、リポジトリを指定して、直接masterリポジトリをインストールすると良いでしょう。他の言語実装でどうなっているかまでは調べきれていませんが、socket.io-emitterを使用するときや、socket.io-redisのアップデートの際は気をつけた方がいいと思います。

まとめ

  • hitoboでの開発ではsocket.io-emitterを用いて、APIに寄せた開発をしている
  • APIに寄せると今までと同じようにサーバーサイドの開発ができ、ソケットサーバが複雑にならないというメリットがある
  • socket.io-emitterを使う場合は内部で使われているmsgpackの仕様に注意する必要がある

さて、hitoboでのWebSocketとAPIの使い分けのお話をさせていただきました。もちろん、APIに比べて、WebSocketの方が通信量が少なくて済むため、それを優先し、WebSocketに寄せてしまうこともあると思います。しかしながら、今までの資産をそのまま使いつつ、WebSocketを導入することができるので、既存のプロダクトに導入する際や、やり取りが多くない画面を実装する際は、APIに寄せて実装することをオススメします。とはいえ、hitoboでもこれがベストプラクティスだとは思っていません。「自分のチームではこうしてるよ!」などあればぜひ教えてください!

また、hitoboチームでは効率のよいチャットサポートを実現するフロントの設計や、大量のメッセージをさばくインフラ設計がしたいエンジニアを募集しております!少しでも興味のある方はお気軽にご連絡ください!

P.S. 12日担当なのに投稿遅れてしまい申し訳ありません!もうちょっと、計画的に準備すればよかったと反省しております...