第4話: WebSocket の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第4話です。

WebSocket

WebSocketとは、サーバー/クライアント間で、オーバーヘッドの小さい双方向通信を実現する仕組みです。

WebSocketはフレーム探知で送受信しますが、相手が決まっているために送信先の情報などはもちません。 HTTPの基本要素のうち、ボディのみを送っているようなものです。 フレームはデータサイズなどを持っているだけで、オーバーヘッドも2から14バイトしかありません。 一度コネクションを確立した後は、サーバーとクライアントのどちらからも通信を行うことが可能です。

WebSocketは何がうれしいのか

Ajax, Commet, WebSocketの通信の比較を電話で表現してみたの図です。

f:id:yagi_suke:20180128175545p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.34
(備考: 一回返事をもらうごとに通信が切れる気がするけど、)

f:id:yagi_suke:20180128175547p:plain f:id:yagi_suke:20180128175551p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.35
画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.36

f:id:yagi_suke:20180128175555p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.37

通信方式 特徴
Ajax 負荷が高い
サーバー側の情報取得は定期的
クライアント側の情報送信は別接続で行うことが多い
Commet 負荷が非常に高い
サーバー側の情報取得はリアルタイム
クライアント側の情報送信は別接続のAjax
WebSocket 負荷が小さい
サーバー側の情報取得はリアルタイム
クライアント側の情報送信も同じ接続内(双方向通信)

WebSocketはステートフル

WebSocketが他のHTTPベースのプロトコル異なるのは「ステートフルな通信である」という点です。
ステートフルとかステートレスについて: http://yohei-y.blogspot.jp/2007/10/blog-post.html

HTTPは高速性のためにKeep-Aliveなどの複雑な機構も備えるようになってきていますが、 基本的にリクエスト単位で接続が切れてもセマンティクス上は問題ありません。 ロードバランサーを使ってサーバーを複数台に分散しておき、リクエストのたびに別のサーバーが 応答していても問題ありません。

ちなみに、Server-Sent Eventsも送信済みIDを一元管理して保証できれば、 リクエストを捌くサーバーが入れ替わっても問題がないように設計されています。

JavaScriptのクライアントAPI

WebSocketは、HTTPの下のレイヤーであるTCPソケットに近い機能を提供するAPIです。 JavaScriptAPITCPソケットのAPIに近い形態になっています。 通信はサーバーが受信を受けている状態で、必ずクライアントから接続します。 (クライアント起点の通信)

  1. サーバーが特定のIPアドレス、ポート番号でサーバーを起動(Listen)
  2. クライアント(ブラウザ)がサーバーに通信開始の宣言をする(Connect)
  3. サーバーにクライアントから接続依頼がくるので、それを受け入れる(Accept)
  4. サーバーにはソケットクラスのインスタンスが渡される
  5. サーバーが受理すると、クライアントのソケットのインスタンスの送受信機能が有効になる

このListen/Connect/Acceptは筆者が参考のために追加したもので、 システムプログラミングの文脈でソケットについて説明するときに使われる関数名です。

JavaScriptのWebSocketのAPI名はこれと異なりますが、基本的な考え方に差はありません。 実際には一旦HTTPで接続してからアップグレードするため、内部の手順はやや複雑ですが、 外部から見たシーケンスは同じです。 基本的な接続と送信のコードを示します。 WebSocketクラスのコンストラクタで接続先のURLを指定して、send()メソッドでデータを送信します。

上記の手順でクライアントが接続に当たってすべきことは2つだけです。 このコンストラクタと、onopenイベントリスナの裏でこれだけのことが行われています。

const socket = new WebSocket('ws://game.example.com:12010/updates')

socket.onopen = () => { // データの送受信可能となる
  setInterval(() => {
    if (socket.bufferedAmount === 0) {
      socket.send(getUpdateData())
    }
  })
}

接続後のソケットに対して、クライアント側で行う操作は次の3つです。 - send([データ]): WebSocket接続を利用して、データをサーバーに送信する - onmessage: サーバーからメッセージを受信した時に呼ばれるイベントリスナ - close([コード [, 理由]]): WebSocket接続を切断

データとしては文字列、Blob, ArrayBufferなどを使うことができます。 受信には、onmessageメソッドを使います。 onmessageイベントの使い方はServer-Sent Eventsと同じです。

参考: https://developer.mozilla.org/ja/docs/Web/API/WebSocket

接続

まず通常のHTTPとして接続をスタートし、その中でプロトコルのアップグレードを行い、 WebSocketにアップグレードすることを要請します。

通常開始のリクエス
GET /chat HTTP/1.1             // 普通のHTTPリクエスト
Host: server.example.com
Origin: http://example.com
Upgrade: websocket             // この2つで、HTTPからWebSocketへの
Connection: Upgrade            // プロトコルのアップグレードを表現している
Sec-WebSocket-Key: A4QSEcsepWr4m2PLS2PJHA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
  • Sec-WebSocket-Key クライアントとのコネクションの確立を立証する為に使われる。 ランダムに選ばれた16バイトの値をBASE64エンコードした文字列
  • Sec-WebSocket-Versions 接続のプロトコルバージョンを指示するためにクライアントからサーバへ送信され、 現在のWebSocketの最新バージョンは13なので13を固定する
  • Sec-WebSocket-Protocol WebSocketは単にソケット通信の機能だけを提供する。 その中でどのような形式を使うかはアプリケーションで決める必要がある。 コンテントネゴシエーションのように複数のプロトコルが選択できるように使われる。 このヘッダーはオプション
サーバーレスポンス
HTTP/1.1 101 Switching Protocols   // HTTPがUpgrade!!
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 7eQChgCtQMnVILefJAO6dK5JwPc=
Sec-WebSocket-Protocol: chat
  • Sec-WebSocket-Accept Sec-WebSocket-Keyを決まったルールで変換した文字列。 これにより、クライアントはサーバーとの通信確立を検証できる。
  • Sec-WebSocket-Protocol クライアントからサブプロトコルのリストを受け取ったときに、その中野一つを選択して返す。 クライアントは送信したプロトコル以外を受け取ったら接続を拒否しなければならない

ランダムな文字列からSec-WebSocket-Acceptの値は特定の文字列を接合した後にSHA1ハッシュを計算し、 それをBASE64エンコードした内容になります。

この後は双方向通信を開始します。

Socket.IO

WebSocketは強力なAPIですが、多くの場合、それをさらに使いやすくするSocket.IOという ライブラリを経由して使う方法に人気がありました。

Socket.IOのメリットは次の3点 - WebSocketが使用できないときは、XMLHttpRequestによるロングポーリングでエミュレーションし、 サーバー側からの送信を実現する機能がある - WebSocketの再接続を自動で行う - クライアントだけではなく、サーバー側で使える実装もあり、クライアントが期待する手順で フォールバックのXMLHttpRequest通信をハンドシェイクしたりできる - ロビー機能

WebSocketが使われ始めた当初は後方互換性を広く維持できるということで、 WebSocketといえばSocket.IOでした。

しかし、現在ではWebSocketが使えないブラウザは殆どありません。 企業内のセキュリティ管理のためのウェブプロキシなど、WebSocketが使えなくなる要因はいくつか ありますが、後方互換性目的で他のライブラリを使う理由はかなり減ってきています。 再接続処理などメリットがゼロではありませんが、今後は直接使われるか、 使うにしてもXMLHttpRequestフォールバックをオフにされることが増えるでしょう。

引用元や参考情報