Skip to content

Instantly share code, notes, and snippets.

@nikezono
Last active December 24, 2015 06:59
Show Gist options
  • Select an option

  • Save nikezono/6761059 to your computer and use it in GitHub Desktop.

Select an option

Save nikezono/6761059 to your computer and use it in GitHub Desktop.

WebSocketについて最近思ったこと

@nikezono
中園 翔
慶應大学増井研究室4年

話すこと

  1. 最近の悩み

  2. 最近やったダメな例

  3. WebSocket時代のデータフロー設計

    • Rest APIとWebsocketについて
    • WebSocketっぽいデータフロー

間違いだらけだと思うのでツッコミください

最近の悩み

  • Socket.ioをフルに使いこなせていない
  • データをPartialに分割してemit出来ない
  • そもそも、サーバがクライアントにデータをpush出来ることを設計に落とし込めていない
    • 結局REST APIっぽい設計になってしまう

まず、データフローについての話

だめな例をあげる

目標
  • よくあるNewsサービスを開発する
    • Pageオブジェクトをサーバと同期して、常に最新の記事データを表示したい
    • リロード無しで最新記事がどんどん追加されてくると良い
実装
  • Pageオブジェクトの元データはサーバサイドでクローラを走らせる
    • サーバが定期的にバッチ処理でPageを更新する

だめな実装例1

サーバが定期的に全クライアントにデータを送信し、クライアントで新しいかを判別する

コード例

### Client ###
 
# 差分があったら表示する
socket.on ‘全データ’,(pages)->
  latestPage = Pages.latest
  pages.each{|page| displayPage(page) if page.id > latestPage.id }
  
### Server ###

# 定期的に全データ渡す
setInterval ->
  io.sockets.emit '全データ',Page.all
,10000 # 10秒毎

なぜだめか

  • 通信環境やマシンスペックに依存しすぎる
  • トラフィックが無駄に多い
  • クライアントが1000人いてPageのレコードが10万件あったらどうするの

だめな実装例2

クライアントの最新データのIdをサーバに渡して、サーバが最新のデータを渡す

コード例

### Client ###

# 1000ミリ秒ごとに新しいデータをもらいにいく
setInterval ->
  socket.emit("データくれ",Pages.latest)
,1000
 
# 差分があったら表示する
socket.on ‘データあったわ’,(pages)->
  pages.each{|page| displayPage(page)}
  
### Server ###

# データくれと言われたら探す
socket.on ‘データくれ’, (page)->
  pages = Page.where(:id > page.id)
  socket.emit("データあったわ",pages) if pages isnt null

なぜだめか

  • REST APIを作る上では定石の実装だが・・・
  • SetIntervalしてたら本当の意味でリアルタイムではない
    • しかしIntervalを縮めるほど計算量が増大する
  • 「新しいレコードが無いのにDBにデータ取りに行く」ケースが存在する
  • Websocketコネクションの利点を活かしきれていない

サーバがデータをPushできることを設計に落とし込もう

リクエスト&レスポンスからの脱却

  1. クライアントでデータ取得フラグを計算
  2. リクエスト
  3. サーバサイドで最新データを計算
  4. レスポンス

このデータフローだと、HTTPだろうがWebSocketだろうがXHRLongPollingだろうが大差ない.

よくないデータフローの例

データフロー

そこで

こうしたい

  • 一つのデータについてのフローは、一方向にする
    • サーバからクライアントのどちらかがひたすらPushし続ける
    • DBやその他のプロセスも全部ひとつなぎのフローとしてObserver Patternを適用したい
  • MVCアーキテクチャをデータベース/サーバ/クライアントに適用したい
    • プロセスやプロトコルを超えてMVCを構築できる
    • サーバにもクライアントにもMVCがある(たとえばBackbone + Railsとか)という歪みを是正できる

これが

データフロー

こうなるとよい

Push

実装

  • サーバからクライアントにデータをpushする仕組みが必要
  • WebSocketやXHR Long Pollingを使う
  • サーバがDBに要所要所データを取りに行くのではない
  • DBがサーバにイベントを発行する
    • そのために、 DataのSave/DestroyにTriggerがあるDB/ODM/ORMを使う。
      • Mongoose(MongoDB ODM)なら pre/post hookがある
        • mongoDBの変更をnodeからPollingしてる
      • Oracleにもある
      • DBの変更をEventとして扱えればいい

データフローの分割

  • 複雑なプログラムになると、データフローの発端が複数になったりする
  • クローラがたくさんあったり、時間によって値が変更されたり
  • フローの終端にあるクライアントもデータを操作したかったり

そういうときは

プロセス

こんな感じにすると良いと思う。

要点

  • どこかでフローをひとつのイベントにまとめる
    • 前図では、実データの変更をMongooseの'post save'イベントにまとめている
      • データベースの値を変更できるプロセスと、取得するプロセスを分けている
  • とにかくデータの依存関係を明確にする
    • クライアントからの操作を受け付けるイベントハンドラが直接、プロセス内の変数を弄ったりしない
      • DB(Model)のデータ変更を行えば、そこからフローが発生してクライアントの再描画まで行けるはず
  • 要するにイベントドリブンで全部書きましょうということ

問題

  • データフローの発端となるEvent/Behaviorの更新頻度はどれくらいか?
    • 更新頻度が激しいと、落ちるのでは/トラフィックがヤバいのでは
  • 連続的データなのか、離散的データなのか?
    • 連続的データをEventEmitterで取り扱うと凄まじい発火回数になるのでは?
  • ヘビーなデータ変更がひとつのEventにまとめられてしまうと辛いのでは?
    • ファイルアップロードとか、離散的に一つのイベントにされるとヘビーすぎる
  • 部分変更をどれだけ切り出せるか?コードが冗長にならないか?

便利なライブラリで解決しよう

  • node.jsのStream API
  • socket.io

nodeのStream API

  • 連続的データ(Stream)を扱うためのAPI
  • デカいデータをPartialに扱うコードが簡単に書ける #これ重要
  • httpRequestとかもpartialに流せる
  • Pipeが使える
  • Reactive Programmingとマジ相性いい
  • 実装はEventEmitter

コード例

引用:http://d.hatena.ne.jp/Jxck/20111204/1322966453

// 本来は 'data', 'end', 'error', 'close' イベントが必要
function TimerStream() {
  this.readable = true;
  this.t = 0;
  this.timer = null;
  this.piped = false;
}

// 継承、詳細は util.inherits を参照
util.inherits(TimerStream, stream.Stream);

TimerStream.prototype.resume = function() {
  this.timer = setInterval(function() {
    this.t++;
    if (this.t > 4) {
      return this.emit('end');
    }
    this.emit('data', this.t.toString());
  }.bind(this), 1000);
};

Socket.io

socket.io

socket.io-stream

https://github.com/nkzawa/socket.io-stream

  • socket.ioのコネクションから来るデータをStream APIっぽく扱える
  • サーバがPartialにさばいたデータをクライアントもPartialに表示させたりできる

まとめ

ことリアルタイムWebアプリ開発においては、

  • サーバがPushできることを念頭において設計すると良い
  • MVCアーキテクチャをプロセス/プロトコルをまたいで適用しよう
    • モデル(DB)+コントローラ(バックエンドサーバ)+ビュー(クライアントアプリ/html)
      • この仕組みで通信させる
      • ユーザによるビューの変更は、Controllerを通してModel(DB)に通知する
      • Model(DB)に変更があったとき、Viewに画面描画を通知する
  • nodeのStream APIでPartialにデータを扱うと、よりスケールしそう
    • pipeしてWebsocketコネクションでそのままクライアントに流し込むと良い

ありがとございました

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment