@nikezono
中園 翔
慶應大学増井研究室4年
-
最近の悩み
-
最近やったダメな例
-
WebSocket時代のデータフロー設計
- Rest APIとWebsocketについて
- WebSocketっぽいデータフロー
間違いだらけだと思うのでツッコミください
- Socket.ioをフルに使いこなせていない
- データをPartialに分割してemit出来ない
- そもそも、サーバがクライアントにデータをpush出来ることを設計に落とし込めていない
- 結局REST APIっぽい設計になってしまう
- よくあるNewsサービスを開発する
- Pageオブジェクトをサーバと同期して、常に最新の記事データを表示したい
- リロード無しで最新記事がどんどん追加されてくると良い
- Pageオブジェクトの元データはサーバサイドでクローラを走らせる
- サーバが定期的にバッチ処理でPageを更新する
サーバが定期的に全クライアントにデータを送信し、クライアントで新しいかを判別する
コード例
### 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万件あったらどうするの
クライアントの最新データの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できることを設計に落とし込もう
- クライアントでデータ取得フラグを計算
- リクエスト
- サーバサイドで最新データを計算
- レスポンス
このデータフローだと、HTTPだろうがWebSocketだろうがXHRLongPollingだろうが大差ない.
こうしたい
- 一つのデータについてのフローは、一方向にする
- サーバからクライアントのどちらかがひたすらPushし続ける
- DBやその他のプロセスも全部ひとつなぎのフローとしてObserver Patternを適用したい
- MVCアーキテクチャをデータベース/サーバ/クライアントに適用したい
- プロセスやプロトコルを超えてMVCを構築できる
- サーバにもクライアントにもMVCがある(たとえばBackbone + Railsとか)という歪みを是正できる
- サーバからクライアントにデータを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(MongoDB ODM)なら pre/post hookがある
- そのために、 DataのSave/DestroyにTriggerがあるDB/ODM/ORMを使う。
- 複雑なプログラムになると、データフローの発端が複数になったりする
- クローラがたくさんあったり、時間によって値が変更されたり
- フローの終端にあるクライアントもデータを操作したかったり
こんな感じにすると良いと思う。
- どこかでフローをひとつのイベントにまとめる
- 前図では、実データの変更をMongooseの'post save'イベントにまとめている
- データベースの値を変更できるプロセスと、取得するプロセスを分けている
- 前図では、実データの変更をMongooseの'post save'イベントにまとめている
- とにかくデータの依存関係を明確にする
- クライアントからの操作を受け付けるイベントハンドラが直接、プロセス内の変数を弄ったりしない
- DB(Model)のデータ変更を行えば、そこからフローが発生してクライアントの再描画まで行けるはず
- クライアントからの操作を受け付けるイベントハンドラが直接、プロセス内の変数を弄ったりしない
- 要するにイベントドリブンで全部書きましょうということ
- データフローの発端となるEvent/Behaviorの更新頻度はどれくらいか?
- 更新頻度が激しいと、落ちるのでは/トラフィックがヤバいのでは
- 連続的データなのか、離散的データなのか?
- 連続的データをEventEmitterで取り扱うと凄まじい発火回数になるのでは?
- ヘビーなデータ変更がひとつのEventにまとめられてしまうと辛いのでは?
- ファイルアップロードとか、離散的に一つのイベントにされるとヘビーすぎる
- 部分変更をどれだけ切り出せるか?コードが冗長にならないか?
- node.jsのStream API
- socket.io
- 連続的データ(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);
};
https://github.com/nkzawa/socket.io-stream
- socket.ioのコネクションから来るデータをStream APIっぽく扱える
- サーバがPartialにさばいたデータをクライアントもPartialに表示させたりできる
ことリアルタイムWebアプリ開発においては、
- サーバがPushできることを念頭において設計すると良い
- MVCアーキテクチャをプロセス/プロトコルをまたいで適用しよう
- モデル(DB)+コントローラ(バックエンドサーバ)+ビュー(クライアントアプリ/html)
- この仕組みで通信させる
- ユーザによるビューの変更は、Controllerを通してModel(DB)に通知する
- Model(DB)に変更があったとき、Viewに画面描画を通知する
- モデル(DB)+コントローラ(バックエンドサーバ)+ビュー(クライアントアプリ/html)
- nodeのStream APIでPartialにデータを扱うと、よりスケールしそう
- pipeしてWebsocketコネクションでそのままクライアントに流し込むと良い
ありがとございました



