仕様RFC 6455で記述されているWebSocket
プロトコルは、永続的な接続を介してブラウザとサーバー間でデータ交換を行う方法を提供します。データは「パケット」として双方向に渡すことができ、接続を切断したり、追加のHTTPリクエストを必要とすることなく行えます。
WebSocketは、オンラインゲーム、リアルタイム取引システムなど、継続的なデータ交換を必要とするサービスに特に適しています。
簡単な例
WebSocket接続を開くには、URLに特別なプロトコルws
を使用してnew WebSocket
を作成する必要があります。
let socket = new WebSocket("ws://javascriptinfo.dokyumento.jp");
暗号化されたwss://
プロトコルもあります。これは、WebSocketのHTTPSのようなものです。
wss://
を優先してください。wss://
プロトコルは暗号化されているだけでなく、より信頼性も高いです。
これは、ws://
データは暗号化されておらず、中間者に対して可視化されるためです。古いプロキシサーバーはWebSocketについて認識しておらず、「奇妙な」ヘッダーを見て接続を中止することがあります。
一方、wss://
はTLS上のWebSocket(HTTPSがTLS上のHTTPであるのと同様)であり、トランスポートセキュリティレイヤーは送信者側でデータを暗号化し、受信者側で復号化します。そのため、データパケットはプロキシを介して暗号化されて渡されます。プロキシは内部の内容を見ることができず、通過させます。
ソケットが作成されたら、そのイベントをリッスンする必要があります。合計で4つのイベントがあります。
open
– 接続が確立されました。message
– データを受信しました。error
– WebSocketエラーが発生しました。close
– 接続が閉じられました。
…何かを送信したい場合は、socket.send(data)
を使用します。
例を以下に示します。
let socket = new WebSocket("wss://javascriptinfo.dokyumento.jp/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Connection established");
alert("Sending to server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error]`);
};
デモのために、上記の例を実行するためのNode.jsで記述された小さなサーバーserver.jsがあります。「Hello from server, John」で応答し、5秒間待ってから接続を閉じます。
そのため、open
→ message
→ close
イベントが表示されます。
実際にはこれで、すでにWebSocketで通信できます。非常に簡単ですね?
それでは、より詳細に見ていきましょう。
WebSocketを開く
new WebSocket(url)
が作成されると、すぐに接続を開始します。
接続中は、ブラウザ(ヘッダーを使用して)サーバーに「WebSocketをサポートしていますか?」と問い合わせます。サーバーが「はい」と応答すると、会話はまったくHTTPではないWebSocketプロトコルで続行されます。
new WebSocket("wss://javascriptinfo.dokyumento.jp/chat")
によって行われたリクエストのブラウザヘッダーの例を次に示します。
GET /chat
Host: javascript.info
Origin: https://javascriptinfo.dokyumento.jp
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Origin
– クライアントページのオリジン(例:https://javascriptinfo.dokyumento.jp
)。WebSocketオブジェクトは本質的にクロスオリジンです。特別なヘッダーやその他の制限はありません。古いサーバーはとにかくWebSocketを処理できないため、互換性の問題は発生しません。しかし、Origin
ヘッダーは、サーバーがこのウェブサイトとWebSocketで通信するかどうかを決定できるようにするため重要です。Connection: Upgrade
– クライアントがプロトコルを変更したいことを示します。Upgrade: websocket
– 要求されたプロトコルは「websocket」です。Sec-WebSocket-Key
– サーバーがWebSocketプロトコルをサポートしていることを確認するために使用される、ブラウザによって生成されたランダムなキーです。プロキシが後続の通信をキャッシュするのを防ぐためにランダムです。Sec-WebSocket-Version
– WebSocketプロトコルバージョン。13が現在のバージョンです。
JavaScriptはこれらのヘッダーを設定できないため、XMLHttpRequest
またはfetch
を使用してこの種のHTTPリクエストを行うことはできません。
サーバーがWebSocketへの切り替えに同意した場合、コード101の応答を送信する必要があります。
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
ここで、Sec-WebSocket-Accept
は、特別なアルゴリズムを使用して再コード化されたSec-WebSocket-Key
です。これを見ると、ブラウザはサーバーが実際にWebSocketプロトコルをサポートしていることを理解します。
その後、データはWebSocketプロトコルを使用して転送されます。その構造(「フレーム」)はすぐにわかります。そしてそれはまったくHTTPではありません。
拡張機能とサブプロトコル
拡張機能とサブプロトコルを記述する追加ヘッダーSec-WebSocket-Extensions
およびSec-WebSocket-Protocol
がある場合があります。
例えば
-
Sec-WebSocket-Extensions: deflate-frame
は、ブラウザがデータ圧縮をサポートしていることを意味します。拡張機能は、データ転送に関連するものであり、WebSocketプロトコルを拡張する機能です。ヘッダーSec-WebSocket-Extensions
は、サポートされているすべての拡張機能のリストとともに、ブラウザによって自動的に送信されます。 -
Sec-WebSocket-Protocol: soap, wamp
は、任意のデータだけでなく、SOAPまたはWAMP(「WebSocket Application Messaging Protocol」)プロトコルでデータを転送したいことを意味します。WebSocketサブプロトコルは、IANAカタログに登録されています。そのため、このヘッダーは使用するデータ形式を記述しています。このオプションのヘッダーは、
new WebSocket
の2番目のパラメーターを使用して設定されます。これはサブプロトコルの配列です(例:SOAPまたはWAMPを使用する場合)。let socket = new WebSocket("wss://javascriptinfo.dokyumento.jp/chat", ["soap", "wamp"]);
サーバーは、使用するのに同意したプロトコルと拡張機能のリストで応答する必要があります。
たとえば、リクエスト
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascriptinfo.dokyumento.jp
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
レスポンス
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap
ここで、サーバーは「deflate-frame」拡張機能と、要求されたサブプロトコルのSOAPのみをサポートしていることを応答します。
データ転送
WebSocket通信は「フレーム」つまりデータフラグメントで構成され、どちらの側からも送信でき、いくつかの種類があります。
- 「テキストフレーム」– 相互に送信するテキストデータが含まれています。
- 「バイナリデータフレーム」– 相互に送信するバイナリデータが含まれています。
- 「ping/pongフレーム」は接続を確認するために使用され、サーバーから送信され、ブラウザはこれらに自動的に応答します。
- 「接続クローズフレーム」とその他のいくつかのサービスフレームもあります。
ブラウザでは、テキストフレームまたはバイナリフレームのみを直接操作します。
WebSocketの.send()
メソッドは、テキストデータまたはバイナリデータのいずれかを送信できます。
socket.send(body)
の呼び出しでは、Blob
、ArrayBuffer
などを含む文字列またはバイナリ形式でbody
を許可します。設定は必要ありません。どのような形式でも送信できます。
データを受信すると、テキストは常に文字列として表示されます。バイナリデータの場合、Blob
とArrayBuffer
の形式から選択できます。
これはsocket.binaryType
プロパティで設定され、デフォルトでは"blob"
であるため、バイナリデータはBlob
オブジェクトとして表示されます。
Blobは高レベルのバイナリオブジェクトであり、<a>
、<img>
などのタグと直接統合されているため、適切なデフォルトです。ただし、個々のデータバイトにアクセスするには、バイナリ処理のために"arraybuffer"
に変更できます。
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data is either a string (if text) or arraybuffer (if binary)
};
レート制限
アプリが大量のデータを送信していると想像してください。しかし、ユーザーのネットワーク接続が遅く、モバイルインターネットを使用していて、都市の外にいるかもしれません。
socket.send(data)
を何度も呼び出すことができます。しかし、データはメモリにバッファリング(保存)され、ネットワーク速度が許す限り速く送信されます。
socket.bufferedAmount
プロパティは、現在ネットワーク経由で送信されるのを待っているバッファリングされたバイト数を保存します。
これを調べて、ソケットが実際に送信可能かどうかを確認できます。
// every 100ms examine the socket and send more data
// only if all the existing data was sent out
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);
接続クローズ
通常、一方の当事者が接続を閉じたい場合(ブラウザとサーバーの両方に同等の権利があります)、数値コードとテキストの理由を含む「接続クローズフレーム」を送信します。
そのためのメソッドは次のとおりです。
socket.close([code], [reason]);
code
は特別なWebSocketクローズコード(オプション)です。reason
は、クローズの理由を説明する文字列です(オプション)。
その後、close
イベントハンドラー内のもう一方の当事者は、コードと理由を取得します(例:)。
// closing party:
socket.close(1000, "Work complete");
// the other party
socket.onclose = event => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};
最も一般的なコード値
1000
– デフォルトの通常のクローズ(code
が指定されていない場合に使用)。1006
– このようなコードを手動で設定する方法がなく、接続が失われたことを示します(クローズフレームはありません)。
他にも次のようなコードがあります。
1001
– 当事者が離れています(例:サーバーがシャットダウンしている、またはブラウザがページを離れている)。1009
– メッセージが大きすぎて処理できません。1011
– サーバーで予期しないエラーが発生しました。- …など。
完全なリストはRFC6455、§7.4.1にあります。
WebSocketコードはHTTPコードに似ていますが、異なります。特に、1000
未満のコードは予約されており、このようなコードを設定しようとするとエラーが発生します。
// in case connection is broken
socket.onclose = event => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no closing frame)
};
接続状態
接続状態を取得するために、値を持つsocket.readyState
プロパティもあります。
0
– “接続中”: 接続はまだ確立されていません。1
– “オープン”: 通信中2
– “クローズ中”: 接続を閉じている最中です。3
– “クローズ”: 接続が閉じられました。
チャットの例
ブラウザのWebSocket APIとNode.jsのWebSocketモジュールhttps://github.com/websockets/wsを使ったチャットの例を確認しましょう。クライアントサイドに焦点を当てますが、サーバーサイドも簡単です。
HTML: メッセージを送信するための<form>
と、受信メッセージを表示するための<div>
が必要です。
<!-- message form -->
<form name="publish">
<input type="text" name="message">
<input type="submit" value="Send">
</form>
<!-- div with messages -->
<div id="messages"></div>
JavaScriptでは、次の3つの処理を行います。
- 接続を開く。
- フォーム送信時に – メッセージに対して
socket.send(message)
を実行する。 - 受信メッセージ時に – それを
div#messages
に追加する。
コードはこちらです。
let socket = new WebSocket("wss://javascriptinfo.dokyumento.jp/article/websocket/chat/ws");
// send message from the form
document.forms.publish.onsubmit = function() {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
};
// message received - show the message in div#messages
socket.onmessage = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
サーバーサイドのコードは、今回の範囲を超えています。ここではNode.jsを使用しますが、必須ではありません。他のプラットフォームでもWebSocketを使用する手段があります。
サーバーサイドのアルゴリズムは以下のようになります。
clients = new Set()
– ソケットの集合を作成する。- 受け入れた各websocketを集合
clients.add(socket)
に追加し、メッセージを受信するためのmessage
イベントリスナーを設定する。 - メッセージを受信したら: クライアントを反復処理し、全員に送信する。
- 接続が閉じられたら:
clients.delete(socket)
を実行する。
const ws = new require('ws');
const wss = new ws.Server({noServer: true});
const clients = new Set();
http.createServer((req, res) => {
// here we only handle websocket connections
// in real project we'd have some other code here to handle non-websocket requests
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
});
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // max message length will be 50
for(let client of clients) {
client.send(message);
}
});
ws.on('close', function() {
clients.delete(ws);
});
}
動作例はこちらです。
iframe内の右上ボタンからダウンロードして、ローカルで実行することもできます。実行する前に、Node.jsをインストールし、npm install ws
を実行することを忘れないでください。
概要
WebSocketは、ブラウザとサーバー間の永続的な接続を確立するための現代的な方法です。
- WebSocketは、クロスオリジン制限がありません。
- ブラウザで広くサポートされています。
- 文字列とバイナリデータの送受信が可能です。
APIはシンプルです。
メソッド
socket.send(data)
,socket.close([code], [reason])
.
イベント
open
,message
,error
,close
.
WebSocket自体は、再接続、認証、その他の多くの高レベルなメカニズムを含んでいません。そのため、それらのためのクライアント/サーバーライブラリが存在し、これらの機能を手動で実装することも可能です。
既存のプロジェクトにWebSocketを統合するために、メインのHTTPサーバーと並列でWebSocketサーバーを実行し、単一のデータベースを共有することがあります。WebSocketへのリクエストはwss://ws.site.com
(WebSocketサーバーにつながるサブドメイン)を使用し、https://site.com
はメインのHTTPサーバーにつながります。
もちろん、他の統合方法も可能です。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合は、サンドボックス(plnkr、jsbin、codepen…)を使用してください。