Socket.IOでWebSocket通信

2018.11.07

Socket.IOでWebSocket通信

今回はNode.jsのSocket.IOを使用して、クライアントPCとサーバ間でWebSocket通信をしてみたいと思います。

WebSocket通信とは

コンピュータネットワーク用のプロトコルのひとつで、リアルタイムかつ双方向な通信を実現するためのプロトコルです。
コネクション確立時に従来のHTTP通信からWebSocketへのプロトコルのupgradeを要求し、1度コネクションが確立されると、「ws://」あるいは「wss://」から始まるURIスキーム上でクライアント・サーバ間のデータのやりとりを連続して行うことができます。

HTTP通信との違い

HTTP通信がリクエストを出す度に新しくコネクションを確立しなければならないのに対し、WebSocket通信は一度コネクションを確立させると、クライアント・サーバ間のデータのやり取りを連続して行うことができます。
また、HTTPよりも軽量なヘッダを扱うことから、通信コストが低く、よりリアルタイム性の高いプロトコルであると言えます。
そのため、利用例としては、複数の人と同時に対戦するゲームやSNSなどリアルタイム性が求められるアプリケーションに多く見られます。

Socket.IOとは

WebSocket通信を可能にするNode.jsライブラリとブラウザ用ライブラリのセットです。機能としては下記が挙げられます。

  • 全クライアントへのデータ送信
  • ブロードキャスト送信
    (送信者以外の全クライアントへのデータ送信)
  • 送達確認
    (送信したデータが送信相手にきちんと到達したかを確認できる。)
  • ネームスペース
    (Socket.IOの実装を機能単位で分割するための仕組み)
  • ルーム
    (双方向・リアルタイムデータ送受信を任意の範囲で行うための仕組み)
  • データの紐づけ
    (接続しているクライアントに対して任意のデータを紐づけることができる。)
  • 揮発性メッセージ
    (送信に失敗したら、リトライせずにそのメッセージを捨てる揮発性のメーセージを送信することができる。)
  • 認証
    (ソケット全体とネームスペースごとで認証ロジックを挟むことができる。)
  • セッションIDの取得
  • ハンドシェイク
    (接続に紐づけたハンドシェイクデータを最初に保持しておくことで、通信開始後もこのデータから Cookie 等の情報を取得できる。)
  • 自動再接続のサポート
    (切断されたクライアントは、サーバが再び利用可能になるまで永久に再接続を試みる。)
  • 断線検出
    (サーバとクライアントの両方がもう一方の応答がなくなったことを知ることができる。)

今回はSocket.IOを利用してWebSocket通信を使用した下記のようなチャットプログラムを作りたいと思います。

機能一覧

  • メッセージ送信機能
    (宛先選択で指定した宛先にメッセージを送信できるようにする。)
  • 宛先選択
    (to_allclient〈全クライアント宛て〉かto_roomA〈roomAに属しているクライアント宛て〉かto_roomB〈roomBに属しているクライアント宛て〉)
  • 部屋選択
    (roomAかroomB〈未選択の場合はどちらの部屋にも属していないものとする。〉)
  • 退室ボタン
    (属しているroomを退室する。)

通信図

チャットプログラムの実装

環境について

下記環境で実装を進めていきます。

  • Node.js 8.11.4
  • WebサーバはKoaで作成

Socket.IO実装準備

ファイル構成

  • app.js(サーバ側の設定・イベント実装用ファイル)
  • manager.js(クライアント側の設定・イベント実装用ファイル)
  • layout.pug(クライアント側のjs・css読み込み用ファイル)
  • soc.html(クライアント側に表示されるチャットプログラムの表示用ファイル)

サーバ側の実装

[app.js]

 

クライアント側の実装

[layout.pug]
[manager.js]

これで、Socket.IOを使用する準備が整いました。
ここから機能に応じてサーバ側・クライアント側にそれぞれイベントを設置し、WebSocket通信でイベントの送受信をさせながら機能を作成していきます。

メッセージの送受信機能

チャットプログラムのメッセージの送受信機能を作っていきます。
下記図のように送信したメッセージがサーバに接続しているすべてのクライアントに送信されるようにします。
今回はニックネームの代わりにWebSocket通信時に各クライアントに割り当てられるセッションIDを使用してクライアントを識別しようと思います。

クライアント側の実装

[soc.html]
[manager.js]

サーバ側の実装

[app.js]

同時に接続しているクライアントにも同じメッセージが届きました。
自分の発言が青色、他人の発言が灰色の吹き出しになっています。

特定の範囲内でのメッセージの送受信

先ほどは同じサーバに接続しているすべてのクライアントにメッセージが届くようにしていましたが、Socket.IOでは特定の範囲のクライアントに送信することもできます。

roomとnamespaceについて

roomはWebSocket通信のデータ送受信を任意の範囲内で行うためのSocket.IOの機能です。
roomを使用すると、下記図のように、文字通り、そのroomに所属するクライアントの間のみでデータのやりとりをすることができます。
デフォルトでは、接続の際に各クライアントに割り当てられるセッションID名と同じroom名に属しています。なので、roomを応用して特定のセッションIDのクライアントにデータを送信することも可能です。

namespaceはroomと異なり、クライアント側からio(“namespace名”)で接続することができ、接続時の認証機能も設置することができることから、Socket.IOの実装を機能単位で行いたいときに使用されます。
例えば、今回作成しているチャットプログラムに誰でも使用できるチャットと、パスワードを使用して特定の人しか使用できないチャットを設置する場合は、だれでも使用できるチャットと特定の人しか使用できないチャットを下記図のようにnamespaceを分けて設置することができます。


デフォルトの接続はnamespace「/」を使用しています。roomもとくに指定していない現在のチャットプログラムの記述においては、namespace「/」の中に各クライアントに割り当てられるセッションID名のroomがあるという状態であり、roomはnamespaceの一部であると言えます。

room選択機能と特定のroom宛てのメッセージの送受信

先ほど作成したチャットプログラムに部屋選択機能を実装し、選択した部屋に入室できるようにします。
また、その部屋に入室しているクライアントか全クライアントのどちらにメッセージを送るか選択できる宛先選択機能を実装し、特定の範囲にメッセージを送れるようにします。

クライアント側の実装

[soc.html]
[manager.js]

サーバ側の実装

[app.js]

roomへの入室と、指定roomのクライアント宛てのメッセージを送ることができました。

退室機能の実装

続いて、入室したroomを退室する機能を設置します。
roomに入室したら部屋選択のセレクトボックスを非表示にし、代わりに退室ボタンを設置します。
退室ボタンを押したら、roomの退室・退室通知を退室roomのクライアントと自分に送信するようにします。

クライアント側の実装

[soc.html]
[manager.js]

サーバ側の実装

[app.js]

入室後に退室ボタンが表示され、左上のタブのクライアントの退室通知が退室クライアント(左上タブ)と退室roomのクライアント(右上タブ)に送信されました。

namespaceの実装

今まで記述してきたソースコードを、デフォルトのnamespace「/」からnamespace「/chat」を使用した記述に変更します。
通常なら、機能ごとにnamespaceを分けて複数接続を記述するものですが、今回はSocket.IOを使用した主な機能がチャットしかないので、現在の記述を変更するのみにします。

サーバ側の実装

[app.js]

クライアント側の実装

[manager.js]

WebSocket通信のSSL対応

「ws://」と「wss://」の違い

「ws://」はWebSocket通信接続用のURIスキームです。
「wss://」はSSLを使ってセキュリティを担保しているWebSocket接続用のURIスキームです。
SSLにおいて通信はすべて暗号化されるため、通信の途中でデータを盗み見られることがあっても、解読は困難です。
安全な通信のためにもチャットプログラムの通信もSSL対応したいと思います。

SSL対応に必要なファイル

尚、今回使用するSSLの証明書は、OpenSSLで用意した自己証明書とします。必要なファイルは下記のとおりです。

  • 秘密鍵(拡張子:.key)
  • 証明書署名要求(拡張子:.csr)
  • サーバ証明書(拡張子:.crt)

設置場所は読み込めればどこでも大丈夫です。今回はapp.jsと同じフォルダ・同じ階層に設置します。

SSL対応実装

サーバ側の実装

[app.js]

まず、httpモジュールでなく、httpsモジュールを読み込ませるようにします。
次に先ほど用意した秘密鍵、証明書署名要求、サーバ証明書を読み込ませ、サーバをhttps化します。
最後に、Socket.IOでも秘密鍵、証明書署名要求、サーバ証明書を読み込ませて起動させることで、SSL対応したWebSocket通信をすることができます。

クライアント側の実装

[manager.js]

引数にhttpsで接続先URLを指定します。

ブラウザで動作確認をしてみます。
WebSocket通信のヘッダの中身を確認してみると、Request URLは「wss://」で始まっており、SSL対応ができたことが確認できました。

懸念点

以上のようにWebSocket通信はクライアント・サーバ間にて双方向通信を可能にしてくれますが、同時にこれは攻撃者にとっても好都合なことであると言えます。
双方向通信が可能になることで、攻撃者にとってもクライアントPCの不正な遠隔操作やサーバへの攻撃を以前より行いやすくなってしまいます。
攻撃を防ぐためにもサーバ側においては、扱う情報の重要度に見合ったユーザ認証やアクセス認証の仕組みを構築する必要があります。
信頼のできるクライアントかを判断するためにOrigin要求ヘッダの妥当性の検証をすることも必要です。
先ほど挙げたように、WebSocket通信を通す情報が他人に傍受・干渉されては困る場合は、「wss://」を使用する必要もあります。

また、HTTP通信はステートレスであるため、どのサーバに接続しに行っても同じレスポンスを得ることができますが、WebSocket通信はステートフルであるため、サーバに負荷が増えた際他のサーバに切り替えようとしても、個々のサーバにおいて独自の仕組みが必要となってくるため、スケールアウトが簡単にできません。
したがって、大規模なアクセス数をもつサービスにおいて活用は難しいかもしれません。
しかし、Socket.IOの公式ドキュメントには、スケールアウトを可能にするために、sticky-sessionモジュールを使用したスケールアウト方法を紹介しているので、工夫次第では可能であるようです。

まとめ

今回はWebSocket通信を使用してチャットプログラムを作成しましたが、WebSocket通信はチャットプログラム以外にも、リアルタイム解析ツールやバイナリデータのストリーミング、ドキュメントの共同編集など、様々なものを作ることが可能で、アイディア次第で面白いことができそうです。
Socket.IOにも、今回紹介したroom・namespace以外にも便利な機能があるので、そこを押さえることでできることの幅が広がりそうです。