はじめに
こんにちは! これはフィヨルドブートキャンプ Part 1 Advent Calendar 2023 - Adventarの4日目の記事です。 昨日のエントリーは、suzuka_horiさんの「「FBCビブリオバトル」で発表した感想📚 - すずかのプログラミング勉強記」でした!
本日のエントリーでは、WebSocketについて具体的なコードを交えながら世界一わかりやすく解説していきたいと思います!
WebSocketって何?
WebSocketとは、サーバーとクライアント(ブラウザ)との間で双方向通信を行うための通信プロトコルです。
どんなところで使えるの?
一番わかりやすい例では、チャットアプリが挙げられます。
相手が送ったメッセージをリアルタイムに眺めるためには、クライアントA → サーバー → クライアントBへと即座に送る必要があります。
WebSocketなら双方向通信が可能なので、この機能を簡単に実装できます。
また、多人数対応のオンラインゲームを作るときにも役立ちます。
Node.js+Socket.IOで作る、通信対戦ができるHTML5ゲームシステムの作り方 - Yahoo! JAPAN Tech Blog
何が便利なの?
この技術を使うことで、サーバーから送信されたメッセージをクライアントがいつでも受け取れるようになります(逆もしかり)。
HTTPの場合、クライアントがサーバーに「自分宛てのメッセージが届いていませんか?」と一定間隔で問い合わせる必要があります。この方法だと、リアルタイム性が失われてしまいます。通信ごとに、一回一回コネクションが切れています。
WebSocketの場合、接続を切るまでずっとコネクションが繋がったままです。専用の軽量なプロトコルを用いて、安定した通信を維持できます。
どうやって使うの?
ここからが本題です。
WebSocketという名称を聞いたことがあっても、実際にどんなふうにコードを書けば良いのか迷ってしまうかもしれません。
そのため本記事では、自分が作成した「世界一わかりやすいWebSocketのサンプルコード」を用いつつ、そのコードを解説していきます。この記事とリポジトリのコードを読めば、基本をガッチリ押さえられるはずです。
リポジトリ
lef237/simple-websocket-typescript: シンプルなWebSocketのTypeScriptコード
こちらのリポジトリを用意しました。
このコードを用いつつ、どのような流れでメッセージが送受信されているか解説します。
デモ
まず、上記のコードを実行しましょう。実際に動く様子を見ることで、その後の解説が分かりやすくなるはずです。
実行方法はREADMEに書きました。📝
本当は試しに動かして頂きたいですが「手元で動かすのはちょっと面倒……」という方向けに、録画した動画を添付致します。
https://youtu.be/j9_ARlt6X2w?si=IpVTTzuazrSY6V18
左のウィンドウがサーバーで、右の2つのウィンドウがクライアントです。
aaa
, bbb
といった文字列がリアルタイムに送受信されたのをご確認頂けたと思います。
クライアントのコード
まず、クライアント側のコードから説明していきます。
リポジトリにはNodeとBunのディレクトリがありますが、Nodeのほうで解説していきます。
simple-websocket-typescript/node-websocket/client.ts at main · lef237/simple-websocket-typescript
http://
ではなくws://
を使おう
WebSocketの通信にはws://
またはwss://
から始まるURLスキーマが必要です。
それぞれhttp://
がws://
, https://
がwss://
に対応している感じです。
今回はローカル環境内での通信のため、ws://
のほうを使うことにします。
(本番環境ではwss://
を使いましょう!)
const url = "ws://localhost:8080";
イベントハンドラ
先ほどのurl
を使ってws
インスタンスを生成しています。
const ws = new WebSocket(url);
このws
インスタンスに対してサーバーからイベントが送られます。
下記の4つのイベントごとに、クライアントで処理を実装できます。
onopen
→ WebSocketコネクションが確立したときに実行するよonmessage
→ サーバーからデータを受け取ったときに実行するよ。受け取ったデータの値を確認できるよonerror
→ 何か問題が起こったときに実行するよonclose
→WebSocketコネクションが閉じたときに実行するよ
ちなみに自分はonmessage
のような書き方(イベントハンドラープロパティ)にしましたが、addEventListener()
メソッドを使うこともできます。
ws.addEventListener("message", (e) => { console.log(`Received: ${e.data}`); promptInput(); });
この書き方でも問題なく動きます!
詳しくはMDN Web Docsを参照ください。
WebSocket: message イベント - Web API | MDN
インスタンスメソッド
今度はクライアントからサーバーへと指示を送るときです。
具体的には次の2つのメソッドが使えます。
send()
→ サーバーへとメッセージを送りたいときclose()
→ WebSocketコネクションを切断したいとき
リポジトリのコードでは、次のようになっています。
function promptInput() { rl.question("Enter message to send: ", (message) => { if (message === "exit") { // 'exit'と入力した場合、プログラムを終了 rl.close(); ws.close(); } else { ws.send(message); } }); } ws.onmessage = (e) => { console.log(`Received: ${e.data}`); promptInput(); };
まず、onmessage
でサーバーからのデータを受け取っています。そして、データを受け取ったらpromptInput()
関数を呼んでいます。
promptInput()
関数では、readline
を使ってターミナルでの入力を受け付けています。
question()
関数では、エンターキーを押したときにコールバック関数(アロー関数の部分)が呼び出されて、直前に入力した文字がmessage
変数へと渡されます。
そして、このmessage
変数(入力された文字列)が"exit"
のとき、WebSocketのコネクションを切断します。それ以外のときは、そのmessage
変数に格納された文字列をサーバーへと送るようになっています。
一見複雑に見えますが、単純にws.send(message);
でサーバーへとメッセージを送ってるよ〜と理解すると分かりやすいと思います。
サーバーのコード
次にサーバー側のコードを解説していきます。こちらもNodeで解説します。
simple-websocket-typescript/node-websocket/server.ts at main · lef237/simple-websocket-typescript
サーバー側の詳しい書き方については、wsライブラリのREADMEをご参考ください。
シンプルサーバー
こちらのリンク先のSimple serverを元に説明します。
import { WebSocketServer } from 'ws'; const server = new WebSocketServer({ port: 8080 }); server.on('connection', function connection(ws) { ws.on('error', console.error); ws.on('message', function message(data) { console.log('received: %s', data); }); ws.send('something'); });
このコードのように、on
とsend
を使うだけでほぼ目的は達成できます。
server.on()
を使って、第一引数に"connection"
、第二引数にコールバック関数を渡しています。これで、サーバーへのコネクションが確立されたとき、クライアント側から送られたメッセージの処理をおこなえます。
あとはws.on()
を使って、message
が送られたときの処理を実装します。
この中でconsole.log()
を使ってログを出力したり、send()
を使ってクライアント側にメッセージを送ったりします。
ws.on('message', function message(data) { ws.send(`Client said: ${data}`) });
このように書けば、サーバーが受け取ったデータをそのままクライアントに投げ返すことができます。
これで、双方向通信が可能になりました!
ブロードキャスト機能
しかし、ちょっとだけ物足りないです。
WebSocketを使えば、一つのサーバーに対して複数のクライアントが接続できます。
複数のクライアントを接続できるのだから、クライアントAが送信したメッセージを、クライアントBも受け取れるようにしたいです。
【現在】
- 複数のクライアントが存在する → クライアントAが送信 → サーバーが受け取ってクライアントAに投げ返す → クライアントAだけがキャッチする
【理想】
- 複数のクライアントが存在する → クライアントAが送信 → サーバーが受け取って全てのクライアントに送信する → クライアントAにもクライアントBにもクライアントCにも届く → 複数のクライアントが同時に動く
理想形のほうが、実際のチャットアプリに近いです。
この挙動を実現するために、サーバー接続時に生成されたws
(WebSocketのインスタンス)を、Set
を使って保存しましょう。
そして、それらのインスタンスの集まりをclients
とし、forEach()
関数でその全てにメッセージを送ればOKです。
const clients = new Set<WebSocket>(); // 接続中のクライアントを格納するSet server.on("connection", (ws) => { clients.add(ws); // クライアントを追加 ws.on("message", (message) => { // すべてのクライアントにメッセージをブロードキャスト clients.forEach((client) => { client.send(`Client said: ${message}`); }); }); });
クライアントにNode.jsを使わずにブラウザを使う場合
まずターミナルでサーバー側のコードを動かします。
それからChromeのDevToolsでコンソールを開きます*1。
ブラウザから送信する場合、Node.jsとは違ってクライアント側でWebSocketのライブラリを用意する必要はありません。
Setを使わない方法
ちなみにライブラリを使った場合、Set
を使わなくてもBroadcastすることはできます。
https://github.com/websockets/ws#server-broadcast
今回はBunのコードとなるべく形を揃えるため、Set
を使って実装しました。
(Bunの場合はpub/subで実現できるようです)
応用的な使い方
今回は文字列を送受信しているだけですが、オブジェクトやJSONを使うことでもっと複雑なdataを受け渡すこともできます。
例えばオンラインゲームで、キャラクターに対して指示を出すdataをイメージしてみましょう。
const data = JSON.stringify({ target: "lefzou", action: "move", direction: "right", ... });
サーバーを介して複数のクライアントがこのdataを送受信すれば、お互いのブラウザに描画されたキャラクターをリアルタイムに動かせるでしょう。
まとめ
WebSocketの簡単な使い方について今回ご紹介致しました。
必要最小限の機能を満たすように、コードをなるべくシンプルに保つように気をつけました。記事の説明で分からないところがあっても、GitHub上のコードを読めば、大方のところは理解できるようになっていると思います。
Bunで実装したコードもリポジトリに上げています。そちらについても解説しようか迷ったのですが、この記事の主旨から外れそうなので今回は断念しました。ぜひNodeのコードと見比べてみてください。
参考記事
MDN Web Docs以外では、JavaScript Infoさんの記事がオススメです。
https://ja.javascript.info/websocket
明日のアドベントカレンダー
明日のエントリーは Sochi419 さんの記事です! 楽しみ✨
*1:ブラウザを右クリックして「検証(Inspect)」を選択します