tech.guitarrapc.cóm

Technical updates

リアルタイム通信で利用されるプロトコルと手法

NOTE: 本記事はすでに内容が古く、今読んでも役に立つ度合いはほぼないです。

本記事は社内勉強会で準備した、Webサービスのリアルタイム通信周りのまとめシリーズ の1つを転載して公開するものです。

まだまだわかっていないことが多いので、ぜひぜひ間違っている点などにご指摘いただければと思い公開します。

ぜひぜひ優しくマサカリをいただけると泣いて喜びます!

はじめに

ここでは、Webサービスのリアルタイム通信に利用されるプロトコルと手法をざっくり説明します。ライブラリについては別項目で!

リアルタイム通信に利用される手法がいくつかありますが、それぞれ意図していることが違います。

プロトコルと手法

ざっくり見てみましょう。

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
Polling HTTP X X X 容易
ロングポーリング(Comet) HTTP RFC6202 O 容易
Server Sent Events HTTP W3C CR 不要 O O O ブラウザ互換依存あり
Forever-frame HTTP(iframe) - 不要 O O 容易
WebSocket WebSocket RFC 6455 不要 O O 面倒
RUDP RUDP IETF Draft (標準化なし) O ? Photon使えば

前世代のやり方であるComet について

従来のPolling (後述) やAjaxでは、サーバーからクライアントにデータを送る(PUSH) するのが困難でした。そこで、提唱されたのがCometで、「サーバーでイベント更新された時、クライアントにデータをプッシュするアプリケーションモデル」を指します。

とはいえ、Cometではサーバーからレスポンスが返されたら接続を切る(この文脈は主にロングポーリングを指してます) ため、真の意味でリアルタイム性にはまだ一歩足りません。そしてCometはServer Sent Events、 Forever frame、 WebSocketが出てきてオワコン感とかいわれてる...。

とりあえずPolling系とStreaming系があるのでざくっと。

Polling 系

「サーバーに更新がきたら教えて」と、クライアントからリクエストなげておいて、サーバーは更新があったらレスポンスを返す手法

  • ロングポーリング

Streaming 系

サーバーは更新イベントがあったら、「接続されているクライアントに更新を通知」する手法

  • XMLHttpRequest (ここでは扱いません)
  • XHR streaming (ここでは扱いません)

過渡期といわれてる手法

  • Forever-frame (Cometの一部として言われることもある)
  • Server Sent Events (ロングポーリングの発展版感覚。 Chunked Transfer Codingをまとも使えるようにした感)

将来有望といわれてる手法

各種ライブラリで広く用いられており、名前を良く知られているのがWebSocketです。

  • WebSocket
  • RUDP (Reliable User Datagram Protocol)

WebSocketは、WebSocketプロトコルを用いることで、これまでのHTTPの発展ではできない双方向通信を実現しているのが特徴です。

では、リアルタイム通信で利用されるプロトコル/手法を順に見ていきましょう。

Polling

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
Polling HTTP X X X 容易

クライアントからサーバーへのPULL型通信です。

Pollingは、クライアントが主導してサーバーに情報を送信する時、真っ先に思いつく汎用的な手法です。仕組みは単純で、クライアントからサーバーに定期的に問い合わせると、サーバー上に更新が発生していないか確認し更新があれば取得します。

メリット

単純なため、実装が容易で応用範囲も広いです。HTTPの基本的な機能を使用しているのみのため、Web、デスクトップ、ネイティブと利用シーンも問いません。

デメリット

サーバーに更新があるか毎回確認しにいくため、通信コストの大半は無駄になります。ネットワーク通信は様々な処理の中でもコストが大きいため、バッテリー消費、帯域幅、通信の両面で代償が大きいため好ましくありません。

特に、接続を生成されるタイミングはサーバーにとって馬鹿にならないコストがかかります。これが多数のクライアントからとすると死亡一直線です!

リアルタイム性は、ポーリング間隔を短くすれば改善します。それは更なるクライアント、サーバーのリソース圧迫をもたらします。

対策として、アダプティブインターバルピギーバッキング という緩和策があります。いずれもリソースを効率良くすることを主眼においた手法です。

向いているシーン

以下のシーンでは妥当といえます。

  • あまり更新が頻繁ではない
  • サーバーからクライアントにプッシュ通信する必要がない

Long Polling (Comet)

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
ロングポーリング HTTP RFC6202 Comet(Polling) 容易

ポーリングと同様に、クライアントからサーバーへのPULL型通信です。

Polling の発展版

Pollingでは、サーバーはクライアントからリクエストがあったら速やかにレスポンスを返していました。

ロングポーリングでは、「クライアントからのリクエストを、サーバーがPUSHしたい時まで返さず保持する」ことで、リアルタイムに近い応答ができるようにします。

クライアントがサーバーにリクエストをしに行くとサーバーはそのリクエストを保持します。サーバーは、何かサーバー側で変更が発生したらクライアントにレスポンスを返ります。

リクエストをポーリングに比べて長く保持することで、接続、切断の回数大きく省いてリアルタイムに近い挙動を実現しています。

メリット

ポーリングに比べて、リアルタイミング性が圧倒的に高くなります。また、ポーリングが圧倒的に長く少なくなります。やったね!

さらに、サーバーに更新があったらレスポンスを返すので、「疑似的」にサーバーからクライアントへのイベントを発信(PUSH型の通信)ができます。 そのためロングポーリングは双方向通信の手段の1つに数えられます。

デメリット

一度PUSHしたら、クライアントからのリクエストを待たないといけないので、極小間隔での連続PUSH通信はできません。

データ送信のたびに接続を再生成するのでメモリ消費が少し大目です。また、他のリアルタイム通信と比較すると、接続が若干遅いです。繋ぎっぱなしじゃないので仕方ない..。

接続が遅いということは、通知と通知で遅延が気になるケースもありえます。特に、サーバーがレスポンスを返している最中に発生したイベントは、次にクライアントがリクエストしてくるまで遅延します。

対策として、ロングポーリングはHTTPが提供する機能だけ使用しているので、同じタイミングのメッセージは1つのHTTPレスポンスにまとめるなどが有効です。

ロングポーリング 自体は双方向通信ではない

みてのとおりロングポーリングはサーバーからクライアントへの接続に利用されるため、クライアントからサーバーへの通信したい場合は、別途HTTP(S)接続を作る必要があります。

  • サーバーからクライアントへの通信にはロングポーリング
  • クライアントからサーバーへの通信には通常のHTTP(S)

接続が閉じられるケース

接続はクライアントから常に開かれ、接続が閉じられるのはいずれかのケースです。どちらのケースでもすぐに新しい接続が確立され、データ更新を待ち受けます。

  • サーバーがその接続でクライアントにデータを送信する
  • 接続がアイドル状態になりタイムアウトが発生する

向いているシーン

以下のシーンでは妥当といえます。

  • Pollingではちょっと頻繁に接続ありすぎる場合
  • Server Sent Eventsに対応していないブラウザでリアルタイム性のある通信をしたい場合

Server Sent Events

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
Server Sent Events HTTP W3C CR 不要 Commet(Streaming) O O ブラウザ互換依存あり

ロングポーリングの延長で考えるとイメージしやすいでしょう。

ロングポーリング の発展版

まず、クライアントはサーバーと接続時、Content-Type: text/event-streamを指定します。すると、サーバーはイベント更新が発生したら、クライアントにChunked (断片) データとしてレスポンスを送信します。ロングポーリングとの違いはここで、 Chunkedとしてレスポンスの一部を送信することでクライアントとの通信は切れず、再リクエストをする必要がありません。

これにより、ロングポーリングまでの「クライアント契機のPULL型通信」から、「サーバー契機のPUSH型通信」に発展しています。リアルタイム通信はPUSH型でないと遅延を回避できないのでダイジダイジ。

メリット

ロングポーリングに比べて、よりリアルタイム性が高まります。ロングポーリングにあった、レスポンス後の再リクエストというコストがなく遅延も少なくなっています。

再接続コストもないので、メモリ消費も優し目。

サーバー側の実装追加がほぼ不要。WebSocketの場合は、従来の実装から変更が必要になる。

リトライやKeep-aliveも仕様に含まれてるのでお任せできる。

デメリット

クライアント側のブラウザなどが、Server Sent Eventsに対応していないと使えない! (IE10でもだめぽよ。Android 4.4未満の標準ブラウザもダメぽよ)

WebSocketと違いHTTPなのでちょっと効率悪い。

セキュリティモデルもHTTP引き継いじゃってるので、場合によってはめんどー。

ブラウザ側の実装に引きずられることもままあります。 (kyo_agoさんがバグ多いっていうレベル)

\r\n区切りで送信するので、バイナリはむりぽよ。

ロングポーリングもそうですが、サーバープッシュ用途でコネクションを貼り続けるため、クライアントからサーバーにデータを送るためには別コネクションを張らないとだめ。

向いてるシーン

ブラウザが対応していて、既存の実装をあまり変更せずにやりたいならWebSocketより簡単。基本はHTTP、XMLHTTPRequestの改善がメインなので、WebSocketが使えない時の選択に。

WebSocketと違い、HTTP 1.1なのでProxyも通りやすい。(一部chunked stream通してくれないプロキシもあるけど! )

HTTPでの開発経験を生かせる。

死んでく技術扱いされてる(要出典) ので、WebSocket使えるならムシ!

備考

Android 2系で使うなら、1回の送信が4K超えるようにpadding入れるなりしましょう。

Forever-frame

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
Forever-frame HTTP(iframe) - 不要 O O 容易

HTMLのiframeタグを使って常時接続を実現する方法です。ざっくりクライアントが開くHTMLに、サーバーのURLをソースとしたiframeタグを埋め込みます。あとはこの接続を開いたままにして、クライアント側で定義された関数を呼びだすことでデータの送信します。

メリット

接続が開きっぱなしなので、リソースの無駄は小さい。 HTML + JavaScript + HTTPを駆使すれば、だいたいどのようなシーンでも使えます。ただ、ブラウザとかみたいに標準でサポートしていない環境では、あまり利用されない。

デメリット

1つの接続は片方向通信のため、サーバーからクライアントへのForever-frame接続を張ったら、クライアントからサーバーへの通信には別にHTTP接続を作る必要があります。

また、HTML + JavaScript + HTTPを駆使しているため、デスクトップアプリケーション向きかというとちょっとつらさあるです。

クライアント内部のインメモリにレスポンスをためていくので、メモリ消費が優しくない + 定期的なリサイクルを考慮します。

向いてるシーン

あれ、なんかつらさが多い。。。。ブラウザ依存が小さいので、まぁ使えるシーンで。

WebSocket

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
WebSocket WebSocket RFC 6455 不要 O O 面倒

サーバーとクライアント間で、双方向通信をおこなうために提唱された通信規格の1つです。

クライアントはサーバーと一度HTTPで接続を確立(ハンドシェイク)させたら、Upgradeヘッダを用いてHTTP1.1からWebSocketに切り変えて以降は繋ぎっぱなしします。これにより、「クライアントからサーバーへの送信」「サーバーからクライアントへの送信」と双方向に通信が可能となります。他のが、サーバーからの送信と、サーバーへの送信でそれぞれ通信チャンネルを作っていたのと異なります。

メリット

HTTPに比べて軽量なヘッダで、通信コストは低いです。

通信コストが低いということは、よりリアルタイム性が高く負荷も低めです。

バイナリも送れるので、Server Sent Eventsと違ってこの辺は扱いが楽。

サーバー とクライアントで同じ通信経路を使うため、これまで紹介したようなクライアントからサーバーへ投げる時は別経路を用意する必要はありません。(あくまでそのWebSocket接続で担保する通信に限るのは当然ですが)

クライアントからサーバー側の関数呼び出し、サーバー側からクライアント側の関数呼び出しを実装するライブラリが多いのも特徴です。例えば、SignalRでは、サーバー側のHubメソッドをクライアントのJavaScriptから呼びだしたり、サーバー側からクライアント側のJavaScriptメソッドを呼び出したりできます。

デメリット

サーバー、クライアント共にWebSocket対応が必要です。(IE10未満、Android 4.4未満の標準ブラウザとか)

既存の通信機器がUpgradeヘッダを通せない可能性がある。(httpsでくるむと、中身見えないのでいけることがおおいにゃ! )

向いてるシーン

ブラウザや環境がサポートしてるなら、Server Sent EventsやForever-frameよりリアルタイム性が高く、低負荷です。

RUDP

名称 プロトコル RFC データ取得時の再接続 サーバープッシュ メモリ消費 遅延の少なさ デスクトップでの利用
RUDP RUDP IETF Draft (標準化なし) O ? O Photon使えば

HTTPやWebSocketで利用されているTCPは、信頼性が高い代わりにオーバーヘッドが大きいです。一方で、UDPはオーバーヘッドが小さく、軽量、高速な代わりに信頼性が低いです。

プロトコル メリット デメリット
UDP 高速、軽量 確実にパケットが届けられた保証、信頼性がない
TCP パケット到達の保障、信頼性が高い 低速、重い

そこでベル研が独自OSで利用するために、UDPを改良したプロトコルがRUDP (Reliable User Datagram Protocol) です。RUDPはReliableを足した別実装は多いのですが、ここではPhotonで利用されてるのに限定しようかな。

いずれもTCP同様に信頼性を持たせる実装を、TCPよりも低いオーバーヘッドで満たしています。

  1. 受信パケットの確認応答
  2. ウィンドーイングとフロー制御
  3. 欠損パケットの再送
  4. リアルタイムストリーミングより速いオーバーバッファリング
プロトコル メリット デメリット
RUDP 高速、軽量、パケット送信の保証 標準化されておらず一般的じゃない + 専用フレームワークで扱うことになる

逆にいうと、これまで見てきたWebSocketのような「サーバーとクライアントが繋ぎっぱなし」というよりは、RUDPで次々にパケットを送りつけあう仕組みといえる。

ぶっちゃけRUDP自体に関しては詳細ふめーです。ドキュメントみつけられない.... 公開情報足りなすぎ...。

メリット

TCPよりも圧倒的に高速、コスト低い。

デメリット

RUDPをハンドリングするライブラリ、というよりもはやフレームワークないと低レイヤすぎ。(セッション層扱いたくないぉ)

向いてるシーン

Photonつかってくれ~。おわり。