LEFログ:学習記録ノート

leflog: 学習の記録をどんどんアップしていきます

世界一わかりやすいWebSocketのサンプルコード(とその解説)

差し込む側がプラグで、差し込まれる側がソケット(豆知識)

はじめに

こんにちは! これはフィヨルドブートキャンプ 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のサンプルコード」を用いつつ、そのコードを解説していきます。この記事とリポジトリのコードを読めば、基本をガッチリ押さえられるはずです。

リポジトリ

github.com

lef237/simple-websocket-typescript: シンプルなWebSocketのTypeScriptコード

こちらのリポジトリを用意しました。

このコードを用いつつ、どのような流れでメッセージが送受信されているか解説します。

デモ

まず、上記のコードを実行しましょう。実際に動く様子を見ることで、その後の解説が分かりやすくなるはずです。

実行方法はREADMEに書きました。📝

本当は試しに動かして頂きたいですが「手元で動かすのはちょっと面倒……」という方向けに、録画した動画を添付致します。

youtu.be

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をご参考ください。

websockets/ws: Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js

シンプルサーバー

こちらのリンク先の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');
});

このコードのように、onsendを使うだけでほぼ目的は達成できます。

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)」を選択します