2015年4月24日金曜日

サーバサイドで WebSocket を実装するための基礎

比較的シンプルな仕様である WebSocket ですが、サーバサイドフレームワークからの視点で見ると留意しなければならない点が幾つかあります。


多数の同時接続

一旦接続が成立すると、どちらかから切断しないかぎり、そのまま保持されます。WebSocket を使うサーバサイドは、同時に接続するクライアントの数が膨大になることを想定しなければなりません。いわゆる C10K 問題(クライアント1万台問題)です。

これを軽減するには、Linux では epoll システムコール、BSD の場合は kqueue システムコールなどの多重I/Oを使ったソケットの監視が必須となります。


また、リスエストを処理する部分については、1つの重い処理に引きずられて他のリクエストのレスポンスが遅くなっていはいけませんので、1リクエストを処理するのに1スレッド(または1プロセス)を割り当てることを考えなければなりません。スレッドもリソースを消費するので、無尽蔵に生成されないよう注意する必要があります。

 ※ 1つの接続に対し常時1つのスレッドが必要という意味ではありません


このような理由で、シングルスレッドのイベント駆動型モデルの実装(node.jsなど)には問題があると言えるでしょう ※1。但し、アプリが実行する処理が単純で、重い処理が全くないという場合はこの問題は当てはまりませんが、どのような要求にも対応すべき必要がある開発フレームワークとしてはあまり考えられないアーキテクチャだと思います。

 ※1   サーバをマルチプロセス化するという軽減策もでてきているが根本的な解決策ではないでしょう

キープアライブ

通信相手がダウンしていないかどうか、または通信路が確立されているかどうかをチェックするための定期的な通信。

  TCPにもキープアライブの機能がありますが、ここはアプリケーションレイヤーの話です。

WebSocket では長時間接続し続けることが想定されています。通信相手のプログラムがダウンしていなくとも、無通信状態が長く続くと通信路の途中にあるルータなどの機器がポートを閉じてしまうケースがあります。

これを回避するため、無通信時間があまり長くならないようピン/ポンフレームを送受信しなければなりませんが、2015年4月現在、ブラウザ上のスクリプトからこれらのフレームを送る仕組み(API)はありません。

従って、サーバサイドからピン/ポンフレームを定期的または任意のタイミングで送る仕組みが必要となります。


クライアントへのブロードキャスト

チャットアプリを想像すると分かりやすいですが、ある人が書いたコメントはそのグループ全員に直ちに通知されます。このようにメッセージを多数のクライアントへ一括送信できる「ブロードキャスト」の仕組みが必要です。

解決策として「出版-購読型モデル」のアーキテクチャは考えられます。アプリケーションサーバ自体にこの仕組があることが望まれます。

規模が巨大なシステムの場合は、負荷分散のためにアプリケーションサーバのホストを複数設置することになるので、別のプロダクト(Redisなど)と組み合わせて実現することになるでしょう。

データベースへのアクセス

敢えて書くまでもないかもしれませんが、HTTP/HTML のWebアプリと同様に、データベースとのデータの受け渡しができなければなりません。トランザクションについても機能が実装されているかどうかチェックしましょう。







WebSocket プロトコルの話


HTML5 で標準化された WebSocket は、Webサーバ(アプリケーションサーバ)とクライアント(主にブラウザ)との双方向通信を実現するものです。一度接続をするとサーバサイドからも好きなタイミングでデータを送ることができます。近頃のブラウザであればほぼ実装されています。(実装されていないブラウザはあるのか。。)

      プロトコルの仕様は RFC6455 として標準化されています。
 
WebSocket は 基本 80 番ポートの TCP 通信であるので、一度接続ができてしまえば任意のタイミングで NATを越えて の送受信が可能であることを意味します。プラグインを使わずに、ブラウザだけで実現できるようになったことで Web アプリの可能性が広がりました。

データフレームはヘッダとペイロードで構成され、可能な限り小さくまとめられています。ヘッダ長は最小の場合2バイト、最大でも14バイトであるので、HTTP のものと比べると劇的に小さいと言えます。

ヘッダが小さい上に、データ更新のための無駄なポーリングを抑えられるので、WebSocket を利用することでサーバサイドの負荷を大幅に抑えることができます。


データフレーム仕様:

  ※ 単位: ビット



オペコード  ( opcode )

operation code の略で、何のフレームなのかを示すコードが指定される。次の種類があります。

オペコード 意味 備考
0 継続フレーム   大きなペイロードを分割して送る際に使用
1 テキストフレーム   ペイロードがテキスト(UTF-8)
2 バイナリフレーム   ペイロードがバイナリデータ
8 クローズフレーム   コネクションを切断する際にお互いに投げ合う
9 ピンフレーム   キープアライブ用
10ポンフレーム   ピンフレームの応答


ピンフレーム/ポンフレーム  ( Ping-Pong )

通信相手が応答可能な状態なのかを確かめるために使用されます。ピンフレームを受信したら、できるだけ早くポンフレームを返信しなければなりません。

 ※ このやりとりが卓球(ピンポン)をイメージすることにその名が由来します

もし応答が不要な場合(一方向の確認)は、ポンフレームの送信だけで完結することができます。


クローズフレーム

切断する際はクローズフレームを送信しなければなりません。これを受信した側もレスポンスとして、同じクローズフレームを返信しなければなりません。

ブラウザについては、JavaScript から WebSocket の接続をしますが、そのページから離れてしまうと、あるいはページリロードをしたタイミングで、自動的にクローズフレームを投げて切断されるように実装されていました。

モバイル端末やPCがスリープモードに入ると、コネクションは切断されます。

ペイロード長 (9ビット目から最大79ビット目まで)

ペイロードの長さを設定する。ペイロードの長さによって、「ペイロード長」を設定する領域やその長さが変わるのが少しややこしい(詳細は省略)。

ヘッダの長さを抑えるためにそのような仕様になったと考えられます。

ペイロード

実際のデータ本体。
ペイロードにどんなデータを載せるかは、アプリケーション次第。フォーマットはプレーンテキスト、JSON、XML、MessagePack、独自仕様など、なんでもあり。サーバサイドとクライアントサイドで仕様を合わせれば良いです。

ペイロードがテキストかバイナリか、適切な値をオペコードに設定しましょう。

マスク処理

マスクフラグが1の場合に、マスキングキー(Masking-key)でペイロードデータを変換します。クライアントからサーバに送る場合はマスク処理を施さなければなりません。マスキングキーにはランダムな値が使われます。

ちなみに、ブラウザではこれらが実装されているので、開発者はほとんど気にする必要はありません。


なぜ、わざわざ負荷のかかるマスク処理をするのか

マスキングキーが同じフレームに載っているので、暗号化が目的でないことは分かります。

マスク処理がないと、WebSocket の中継点にあるプロキシのキャッシュを汚染させることで、悪意のあるスクリプトを実行させられる攻撃が成立する可能性があるようです。この攻撃を困難にするために、クライアントが送信する場合にマスク処理が行われます。

要は、WebSocket がネット攻撃のベースにならないようにするためなのです。