🌐

WebSocketを完全に理解できるコードを書いたので解説します

に公開

これはなに

こんにちは。もんたです。
最近WebSocketなるものについて勉強をしたので、そのアウトプットとして記事を書いてみました。

WebSocketの勉強としてはローカル環境にNext.jsとPythonを使ってWebSocketを用いて通信を行うコードを書いて、WebSocketって何ができるのか、どうやって実装するかについて勉強しました。

なぜWebSocketの勉強をすることになったのか

私、現在仕事で生成AIを用いたチャットシステムの開発をしておりまして、そのチャットシステムの通信でWebSocketを使う必要があるからになります。

ChatGPTとかの生成AIを用いたチャットサービスで何か質問を投げるとします。

ChatGPTのストリーミングについて [更新日:2024/10/30]

余談ですが、ChatGPTではWebSocketは使われておらず、Server Sent Eventsが使われていました🙇‍♂️

参考:https://zenn.dev/cybozu_frontend/articles/try-server-sent-events

詳しくはWebSocketはどのようにして双方向通信を実現しているのかという箇所を読んでもらいたいのですが、ChatGPTで会話している時のネットワークを検証ツールより見てみたら、request headerにwebsocketにupgradeするヘッダーが含まれていませんでした。

また、ChatGPTからのレスポンスもWebSocketの形式とは違っていることがわかりました…!!🙇‍♂️

image.png

質問を投げると、生成AIがぽこぽこと回答を生成していってくれるかと思います。
こういった実装ってHTTP通信だと結構難しいのです。
しかし、WebSocket通信を用いることでChatGPTが生成した内容をぽこぽことリアルタイムに表示していくことが可能になります。

そういった背景から、WebSocketの勉強をすることになったのです。はい。

WebSocketとは

ではWebSocketとは何かについて説明していこうかと思います。
参考程度にですが、私が個人のNotionにまとめたものがありますのでよかったらこちらもご参照ください。

  • Notionにメモしたやつ

https://monta-database.notion.site/WebSocket-118cca6509328018875ae558c0cc8708?pvs=4

WebSocketってなんやねん

まず、WebSocketとは通信規約のことを指します。HTTP通信とかと同じです。
双方向の通信を実現するために使われる通信規約、それがWebSocketなのです。

双方向通信とは

双方向通信とは、データ通信において受信と送信の二つの通信ができる方式のことを指します。
ちなみに、送信か受信しかできない通信方式のことを単方向通信と言います。

双方向通信とは、データ通信において受信・送信の双方向での通信ができる方式のこと。車両の料金収受を行うETCなどで採用されている仕組みで、双方向通信には2種類あり、電話のように送受信が同時にできる全2重通信と、トランシーバーのようなどちらかが送信中はもう片方は送信できず受信のみになる半2重通信とがある。

参考:https://www.goo-net.com/knowledge/17024/

めちゃくちゃわかりやすい資料を作っておきました。
双方向通信と単方向通信の違いはこれです。

image.png

image.png

HTTP通信は単方向通信であり、クライアントがサーバーにリクエストを送り、サーバーがそのリクエストに応じてレスポンスを返します。
また、HTTP通信はレスポンスを返すと接続が切断されるので、常時接続されているわけではありません。

WebSocketではどんなことができるのか?

では続いて、WebSocketを用いることでどんなことができるようになるのかを説明していきます。

まず、わかりやすいのが『チャットアプリケーション』です。
今回私がWebSocketの学習をするきっかけにもなったやつですね。

WebSocketの双方向通信を活用することで、ユーザー同士のメッセージが即時反映することができます。
LINEやSlackのようなチャットアプリケーションでは相手がメッセージを送信した瞬間に即座にメッセージが表示されるようになっています。
また、そのほかにもInstagramやTwitterなどのSNSも他のユーザーにコメントなどを即座に投稿に反映する必要がありますし、実際表示されるようになっています。こういったSNSにもWebSocketによる双方向通信が使われています。

ChatGPTに生成してもらった内容ではありますが、以下(↓)のようなユースケースがあります。

まとめると、リアルタイム性が重視されるようなサービスではWebSocketを用いた双方向通信が使われる傾向にあることがわかります。

WebSocketはどのようにして双方向通信を実現しているのか

では、続いてWebSocketとはどのようにして双方向通信を実現しているかを説明していきます。

以下の画像を用いて説明していきます。

image.png

WebSocketは以下のステップで双方向通信を実現しています。

  1. WebSocketのためのコネクションの要求とコネクションの確立
  2. 確立したコネクションを用いて双方向通信を実現

WebSocketのためのコネクションの要求とコネクションの確立

コネクションの要求と確立について詳しく説明します。

WebSocketではまず、コネクションの確立をする必要があります。

以下のように、ブラウザからWebSocketの要求をサーバに送ります。サーバはその要求を受け、コネクションの確立のためのレスポンスを行い、ブラウザがそれを受け取ればコネクションが確立します。

image.png

まず、コネクションの確立のためには以下のようなリクエストをサーバに送ります。

GET /resource HTTP/1.1                # 通信を開始するリソースのパス
Host: example.com                     # 通信先のホスト名(ここでは "example.com")
Upgrade: websocket                    # HTTPからWebSocketへの「プロトコルのアップグレード」を要求するヘッダー
Connection: upgrade                   # 通信を「アップグレード」するための指示
Sec-WebSocket-Version: 13             # WebSocketのバージョン(現在は "13" が一般的)
Sec-WebSocket-Key: Zr3zHlPIsibpUnLOeHt1Tg==  # セキュリティのための一時的な鍵(サーバが認証に使用)

その後、以下のようなレスポンスを受け取り、 OKとなっていればコネクションが確立されます。

HTTP/1.1 101 OK                       # ステータスコード 101: プロトコルの切り替えを示す
Upgrade: websocket                    # WebSocketへのアップグレードを承認
Connection: upgrade                   # 通信のアップグレードを確認
Sec-WebSocket-Accept: eVAKLzRGo+WymzzGFJPdRsPIPA0=  # クライアントからのキーを基にサーバが生成した認証キー

参考:https://qiita.com/south37/items/6f92d4268fe676347160

注目して欲しいのがUpgradeConnectionヘッダーです。
UpgradewebsocketConnectionupgradeとなっていることから、websocketのアップグレードが承認され、『こっからWebSocketを用いた双方向通信だよ』ということになる。

image.png

上の画像はブラウザのコンソールのネットワークタブで確認したものです。
websocketの通信を行なっているネットワークのヘッダーを見てみたところ、同じような内容をリクエストし、レスポンスとして受け取っていることがわかります。

このような流れでWebSocketにアップグレードできたら、あとはそのコネクションを用いて双方向通信を実現することができるようになります。

ハンズオン

WebSocketってどんなもんなのかは理解してもらえたかと思います。

ここからは私がWebSocketの学習の過程で作ってみたWebSocketって何なのかを完全に理解できるコードセットを使って、実際にコードを用いて説明していこうかと思います。

  • 成果物イメージ

2024-10-2814.37.19-ezgif.com-video-to-gif-converter (1).gif

成果物の内容は以下のとおりです。

  1. WebSocketサーバから0.05秒間隔でtest text No.{i}という内容のテキストを受け取る処理(iは0から99までの数字)
  2. クライアント側でtextareaに入力した内容を送信したらWebSocketサーバに送信する処理

このコードでは、WebSocketを用いた双方向通信を用いて、WebSocketサーバからの受信とクライアント側からの送信の2つのタスクを実装しています。

上記のデモでは、『test, aiueo, testtest, こんにちは, おわり』というテキストをWebSocketサーバに送信しました。
サーバでは受け取ったテキストをログに出力しています。ログを見てみると、確かにクライアントからテキストを受け取っていることがわかります。

image.png

上記より、確かに双方向通信ができていることがわかるかと思います!

🐶<イエイ!!

  • Notionにメモしたやつ

https://monta-database.notion.site/WebSocket-12acca650932802abd51fa0bfc149029?pvs=74

  • 学習に使ったコードたち

https://github.com/Hiroto0706/WebSocket-Hands-on

環境構築に必要なコードたち

今回は以下のような構成でローカル環境を構築しました。

.
├── docker-compose.yml
├── nextjs-app
│   ├── Dockerfile
│   └── app
│       ├── globals.css
│       ├── layout.js
│       └── page.js
└── python-server
    ├── Dockerfile
    ├── main.py
    └── requirements.txt

このハンズオンではバックエンドはPythonのFastAPI、フロントエンドはNext.jsを用いて開発しています。

1. インフラ周り

インフラ周りのコードです。
これぺたぺたして環境構築してもらえればと思います。

version: '3.8'

services:
  python-server:
    build:
      context: ./python-server
    ports:
      - "8000:8000"
    volumes:
      - ./python-server:/app

  nextjs-app:
    build:
      context: ./nextjs-app
    ports:
      - "3000:3000"
    volumes:
      - ./nextjs-app:/app
  • python-server/Dockerfile
FROM python:3.9

WORKDIR /app

COPY . /app

RUN pip install -r requirements.txt

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

  • nextjs-app/Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY . /app

RUN npm install

CMD ["npm", "run", "dev"]

2. python-server周り

  • main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
import asyncio
import logging
from starlette.websockets import WebSocketState

app = FastAPI()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("websocket_server")


@app.get("/")
async def get():
    return HTMLResponse("<h1>WebSocket Server is running</h1>")


@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    logger.info("クライアントが接続されました")

    try:
        send_task = asyncio.create_task(send_messages_periodically(websocket))

        while True:
            data = await websocket.receive_text()
            logger.info(f"クライアントからメッセージを受け取りました: {data}")

    except WebSocketDisconnect:
        logger.warning("クライアントが切断されました")
    except Exception as e:
        logger.error(f"エラーが発生しました: {e}")
    finally:
        send_task.cancel()
        await websocket.close()
        logger.info("コネクションが終了しました")


async def send_messages_periodically(websocket: WebSocket):
    for i in range(100):
        if websocket.application_state != WebSocketState.CONNECTED:
            break
        await websocket.send_text(f"test text No.{i}")
        await asyncio.sleep(0.05)
  • requirements.txt
annotated-types==0.7.0
anyio==4.6.2.post1
click==8.1.7
fastapi==0.115.3
h11==0.14.0
idna==3.10
pydantic==2.9.2
pydantic_core==2.23.4
sniffio==1.3.1
starlette==0.41.0
typing_extensions==4.12.2
uvicorn==0.32.0
websockets==13.1

3. nextjs-app周り

  • layout.js
import "./globals.css";

export const metadata = {
  title: "WebSocket Hands-on",
};

export default function RootLayout({ children }) {
  return (
    <html lang="jp">
      <body>{children}</body>
    </html>
  );
}
  • page.js
"use client";

import { useEffect, useState, useRef } from "react";

export default function Home() {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  const [input, setInput] = useState("");
  const socketRef = useRef(null);

  useEffect(() => {
    const connectWebSocket = () => {
      socketRef.current = new WebSocket("ws://localhost:8000/ws");

      socketRef.current.onopen = () => {
        console.log("コネクションを確立しました");
        setIsConnected(true);
      };

      socketRef.current.onmessage = (event) => {
        console.log("サーバーからメッセージを受け取りました:", event.data);
        setMessages((prev) => [...prev, event.data]);
      };

      socketRef.current.onerror = (error) => {
        console.error("エラーが発生しました:", error);
      };

      socketRef.current.onclose = () => {
        console.log("サーバとのコネクションをクローズしました");
        setIsConnected(false);
      };
    };

    connectWebSocket();

    return () => {
      if (socketRef.current) {
        socketRef.current.close();
      }
    };
  }, []);

  const sendMessage = () => {
    if (socketRef.current && isConnected) {
      socketRef.current.send(input);
      console.log(
        "WebSocketサーバにメッセージを送信しました。 message -> ",
        input
      );
      setInput("");
    } else {
      console.error("WebSocketサーバとの通信を確立できません");
    }
  };

  const closeConnection = () => {
    if (socketRef.current) {
      socketRef.current.close();
      console.log("WebSocketサーバとのコネクションをクローズします");
      setIsConnected(false);
    }
  };

  return (
    <div>
      <h1 className="text-3xl font-bold p-4">WebSocket Messages</h1>
      <div className="p-4">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Enter a message"
          className="border rounded p-2 mr-2"
        />
        <button
          onClick={sendMessage}
          className="bg-blue-500 text-white p-2 rounded"
        >
          Send
        </button>
        <button
          onClick={closeConnection}
          className="bg-red-500 text-white p-2 rounded ml-2"
        >
          Close Connection
        </button>
      </div>
      <p className="ml-4">
        Status: {isConnected ? "Connected" : "Disconnected, reconnecting..."}
      </p>
      <ul className="ml-4">
        {messages.map((msg, index) => (
          <li key={index}>{msg}</li>
        ))}
      </ul>
    </div>
  );
}

WebSocketサーバの解説(Python)

さて、ここからはWebSocketサーバのコードの解説をしていこうかと思います。

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    logger.info("クライアントが接続されました")

    try:
        send_task = asyncio.create_task(send_messages_periodically(websocket))

        while True:
            data = await websocket.receive_text()
            logger.info(f"クライアントからメッセージを受け取りました: {data}")

    except WebSocketDisconnect:
        logger.warning("クライアントが切断されました")
    except Exception as e:
        logger.error(f"エラーが発生しました: {e}")
    finally:
        send_task.cancel()
        await websocket.close()
        logger.info("WebSocket接続が終了しました")


async def send_messages_periodically(websocket: WebSocket):
    for i in range(100):
        if websocket.application_state != WebSocketState.CONNECTED:
            break
        await websocket.send_text(f"test text No.{i}")
        await asyncio.sleep(0.05)

これがWebSocketサーバの全てになります。
めちゃくちゃざっくりこのコードがやっていることを説明すると、以下のとおりです。

  1. WebSocketコネクションの確立
  2. サーバ側からクライアント側に定期的にテキストを送信するタスクの実行
  3. クライアント側からテキストを受け取った時のタスクの実行
  4. WebSocketコネクションを切断する処理

です。
この4つに分けて説明していこうかと思います。

1. WebSocketコネクションの確立

まず、WebSocketコネクションの確立です。

以下の箇所でWebSocketコネクションの確立を行なっています。

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    logger.info("クライアントが接続されました")

FastAPIでは通常HTTPのエンドポイントはapp.get()app.post()のように記述すると思います。
WebSocketを用いる場合は、app.websocket()のように記述します。

websocket_endpoint()は引数にWebSocket型のwebsocketを受け取ります。
これはFastAPIが提供しているもので、websocket通信に必要なメソッドやプロパティを提供します。

参考:https://fastapi.tiangolo.com/ja/advanced/websockets/

await websocket.accept()はWebSocketコネクションを受け入れるためのものです。
このように記述することで、クライアント側からのWebSocketコネクションの要求を受け取ることができます。

クライアントからコネクションの要求を受け取り、コネクションが確立されたら、WebSocketを用いた双方向通信が可能になります。

2. サーバ側からクライアント側に定期的にテキストを送信するタスクの実行

双方向通信では、コネクションが確立されていれば送信と受信どちらのタスクもこなすことができます。
ここでは、送信に関するタスクの実装について説明します。

send_task = asyncio.create_task(send_messages_periodically(websocket))

これはasyncio.create_task()を用いて非同期にメッセージを定期的に送信するタスクを作成しています。

タスクの詳細は以下の関数になります。

async def send_messages_periodically(websocket: WebSocket):
    for i in range(100):
        if websocket.application_state != WebSocketState.CONNECTED:
            break
        await websocket.send_text(f"test text No.{i}")
        await asyncio.sleep(0.05)

これは0.05秒間隔で100回クライアントにtest text No.{i}というメッセージをクライアントに送信するというものです。ループの数だけiは増えるようになっています。

テキストの送信はawait websocket.send_text(f"test text No.{i}")で行っています。
websocket.send_text()を実行すればクライアント側にテキストを送信することができるんです。楽ですね。

ちなみに、str型のメッセージだけでなくjson形式のデータも送信することができます。

参考:https://fastapi.tiangolo.com/reference/websockets/#fastapi.WebSocket.send_bytes

この実装をすることで成果物イメージのWebSocketコネクションが確立されたら表示されるtest text No.{i}などの文字列をクライアント側に送信することができます。

2024-10-2814.37.19-ezgif.com-video-to-gif-converter (1).gif

ブラウザのコンソールのネットワークタブの中のwebsocket通信を行っている箇所を詳しく見てみると、たしかにサーバからtest text No.{i}の文字列を受け取っていることがわかります。

image.png

3. クライアント側からテキストを受け取った時のタスクの実行

続いて、双方向通信の受信に関する実装を見ていきます。

クライアントからのデータの受信は以下の箇所で定義しています。

while True:
    data = await websocket.receive_text()
    logger.info(f"クライアントからメッセージを受け取りました: {data}")

ここで重要なのはdata = await websocket.receive_text()の箇所になります。

await websocket.receive_text()とすることで、クライアントからのデータ受信を待機している状態となります。そして、クライアントからデータを受信したらdata変数に格納します。

その後、ログに受け取った内容を表示するような処理となっています。

クライアント側でテキストボックスに何かしたテキストを入力し、送信をするとサーバ側のログに確かに受け取った内容が表示されることから、確かにサーバはクライアントからデータを受信できていることがわかります。

image.png

4. WebSocketコネクションを切断する処理

最後にfinallyブロックにて非同期タスクの終了とwebsocketコネクションの切断を行います。

finally:
    send_task.cancel()
    await websocket.close()
    logger.info("WebSocket接続が終了しました")

まぁ、これは特に説明することないですね。

websocket.close()で簡単にWebSocketコネクションを切断することができるんです。便利ですね。

WebSocketクライアントの解説(Next.js)

"use client";

import { useEffect, useState, useRef } from "react";

export default function Home() {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  const [input, setInput] = useState("");
  const socketRef = useRef(null);

  useEffect(() => {
    const connectWebSocket = () => {
      socketRef.current = new WebSocket("ws://localhost:8000/ws");

      socketRef.current.onopen = () => {
        console.log("WebSocket connection established");
        setIsConnected(true);
      };

      socketRef.current.onmessage = (event) => {
        console.log("Message from server:", event.data);
        setMessages((prev) => [...prev, event.data]);
      };

      socketRef.current.onerror = (error) => {
        console.error("WebSocket error:", error);
      };

      socketRef.current.onclose = () => {
        console.log("WebSocketサーバとのコネクションをクローズしました");
        setIsConnected(false);
      };
    };

    connectWebSocket();

    return () => {
      if (socketRef.current) {
        socketRef.current.close();
      }
    };
  }, []);

  const sendMessage = () => {
    if (socketRef.current && isConnected) {
      socketRef.current.send(input);
      console.log(
        "WebSocketサーバにメッセージを送信しました。 message -> ",
        input
      );
      setInput("");
    } else {
      console.error("WebSocketサーバとの通信を確立できません");
    }
  };

  const closeConnection = () => {
    if (socketRef.current) {
      socketRef.current.close();
      console.log("WebSocketサーバとのコネクションをクローズします");
      setIsConnected(false);
    }
  };

  return (
    <div>
      <h1 className="text-3xl font-bold p-4">WebSocket Messages</h1>
      <div className="p-4">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Enter a message"
          className="border rounded p-2 mr-2"
        />
        <button
          onClick={sendMessage}
          className="bg-blue-500 text-white p-2 rounded"
        >
          Send
        </button>
        <button
          onClick={closeConnection}
          className="bg-red-500 text-white p-2 rounded ml-2"
        >
          Close Connection
        </button>
      </div>
      <p className="ml-4">
        Status: {isConnected ? "Connected" : "Disconnected, reconnecting..."}
      </p>
      <ul className="ml-4">
        {messages.map((msg, index) => (
          <li key={index}>{msg}</li>
        ))}
      </ul>
    </div>
  );
}

これがWebSocketクライアントの全てになります。
めちゃくちゃざっくりこのコードがやっていることを説明すると、以下のとおりです。

  1. WebSocketコネクションの確立
  2. サーバ側からメッセージを受け取ったらmessagesに受け取ったデータを追加していく処理
  3. クライアント側で入力したメッセージをサーバ側に送信する処理
  4. WebSocketコネクションを切断する処理

です。
この4つに分けて説明していこうかと思います。

1. WebSocketコネクションの確立

はじめに、サーバとのコネクションの確立を行う箇所の説明です。

以下の箇所でコネクションの確立を行っております。

useEffect(() => {
  const connectWebSocket = () => {
    socketRef.current = new WebSocket("ws://localhost:8000/ws");

    socketRef.current.onopen = () => {
      console.log("WebSocket connection established");
      setIsConnected(true);
    };

    // その他のコード
  };

  connectWebSocket();

  // その他のコード
}, []);

重要なのはnew WebSocket("ws://localhost:8000/ws");です。

これはWebSocketオブジェクトを生成するコードです。引数のURLに対してwebsocket通信を開始するために、インスタンスを生成しています。

WebSocketインスタンスにはWebSocketを用いた双方向通信を実現するために必要なメソッドやプロパティ、イベントリスナーがあります。

詳しくはこちらをどうぞ。

::::note info
<details><summary>WebSocketインスタンスができること</summary>
このコードは、JavaScriptでWebSocketオブジェクトを作成して、指定したURL(ws://localhost:8000/ws)に接続を試みるものです。

socketRef.current = new WebSocket("ws://localhost:8000/ws");

詳細な説明

  1. new WebSocket(url):

    • new WebSocket() は WebSocket 接続を確立するために使用するコンストラクタです。
    • 引数には接続先のURLを指定します。ws://はWebSocketプロトコルを意味し、ポート8000で待機しているサーバー(ローカルホスト)に接続します。
  2. 返り値の型:

    • new WebSocket()の返り値は、WebSocket オブジェクトです。
    • WebSocket 型で、ブラウザの Web API によって提供されるオブジェクトです。
  3. WebSocket オブジェクトでできること

    WebSocketオブジェクトには、サーバーとのリアルタイム通信を行うための様々なプロパティやメソッド、イベントリスナーがあります。以下は主なものです。

    • プロパティ
      • readyState:
        • 現在の接続状態を示すプロパティで、次の4つの数値が使われます。
          • 0: CONNECTING - 接続を試行中
          • 1: OPEN - 接続が確立され、通信可能
          • 2: CLOSING - 接続を閉じている最中
          • 3: CLOSED - 接続が閉じられた
      • url:
        • 接続先のURLが格納されています(この場合は"ws://localhost:8000/ws")。
    • メソッド
      • send(data):
        • 指定したデータ(string, Blob, ArrayBuffer など)をサーバーに送信します。
        • 例: socketRef.current.send("Hello Server");
      • close([code[, reason]]):
        • WebSocket接続を閉じます。codeは接続を閉じる理由(数値)、reasonはその理由を説明する文字列を指定します。
        • 例: socketRef.current.close();
    • イベントリスナー
      • onopen:
        • 接続が成功したときに呼び出されるイベントハンドラです。
        • 例: socketRef.current.onopen = () => { console.log("Connected"); };
      • onmessage:
        • サーバーからメッセージを受信したときに呼び出されるイベントハンドラです。イベントオブジェクトeventを引数に持ち、そのdataプロパティにメッセージが含まれます。
        • 例: socketRef.current.onmessage = (event) => { console.log(event.data); };
      • onerror:
        • エラーが発生したときに呼び出されるイベントハンドラです。
        • 例: socketRef.current.onerror = (error) => { console.error("WebSocket error:", error); };
      • onclose:
        • 接続が閉じられたときに呼び出されるイベントハンドラです。
        • 例: socketRef.current.onclose = () => { console.log("Connection closed"); };

まとめ

このコードにより生成されるWebSocketオブジェクトは、サーバーとのリアルタイムの双方向通信を可能にし、send()でメッセージの送信、onmessageでの受信、close()での接続終了などが行えます。これらの機能を組み合わせることで、WebSocketを使用したリアルタイムアプリケーションの構築が可能です。
</details>
::::

以下のコードでは、WebSocketインスタンスが格納されているsocketRef.current.onopenというイベントリスナーを用いて、WebSocketサーバとの接続ができたことを通知する処理が書かれてあります。

socketRef.current.onopen = () => {
  console.log("WebSocket connection established");
  setIsConnected(true);
};

これでWebSocket接続の確立に関する処理は完了です!

2. サーバ側からメッセージを受け取ったらmessagesに受け取ったデータを追加していく処理

これはWebSocketサーバからメッセージを受け取った時に関する処理です。

useEffect(() => {
  const connectWebSocket = () => {
    // その他のコード

    socketRef.current.onmessage = (event) => {
      console.log("Message from server:", event.data);
      setMessages((prev) => [...prev, event.data]);
    };

    socketRef.current.onerror = (error) => {
      console.error("WebSocket error:", error);
    };

    // その他のコード
  };

  // その他のコード
}, []);

onmessageはWebSocketオブジェクトにデフォルトであるイベントリスナーで、WebSocketサーバからメッセージを受け取った時に呼び出されるイベントです。

pythonのコードでawait websocket.send_text(f"test text No.{i}")というコードがあったと思うのですが、このコードが実行された時に呼び出される処理が書かれてあります。

具体的な処理内容は、WebSocketサーバから受け取ったmessageをmessagesにセットするという処理を行っています。

デモにて、test text No.{i}がぽこぽこ表示されていたのはこの処理があったためです。

2024-10-2814.37.19-ezgif.com-video-to-gif-converter (1).gif

onerrorはWebSocketサーバからエラーメッセージを受け取った時に呼び出されるイベントです。
今回はシンプルにconsole.error()を表示するだけの処理を書いてあります。

3. クライアント側で入力したメッセージをサーバ側に送信する処理

const sendMessage = () => {
if (socketRef.current && isConnected) {
  socketRef.current.send(input);
  console.log(
    "WebSocketサーバにメッセージを送信しました。 message -> ",
    input
  );
  setInput("");
} else {
  console.error("WebSocketサーバとの通信を確立できません");
}
};

これはSendボタンをクリックした時に呼び出される関数です。
処理内容はWebSocketに接続されている時、textareaに入力されている内容(input)をWebSocketサーバに送信するというものです。

send(input)でWebSocketサーバに入力内容を送信しているわけですね。実装方法楽でいいですね。

python側のコードで以下のような実装をしていました。

while True:
    data = await websocket.receive_text()
    logger.info(f"クライアントからメッセージを受け取りました: {data}")

これはクライアント側からのメッセージを待機しています。
そして、テキストを受け取ったらdata変数に格納し、ログに表示するというものでしたね。

クライアント側でsend(input)が実行されたら、Pythonコードのここが実行されるわけです。

4. WebSocketコネクションを切断する処理

const closeConnection = () => {
if (socketRef.current) {
  socketRef.current.close();
  console.log("WebSocketサーバとのコネクションをクローズします");
  setIsConnected(false);
}
};

これはマニュアル的にWebSocketコネクションを切断する処理が書かれてあります。
Close Connectionボタンをクリックしたら、この処理が走るようになっており、close()メソッドを用いてWebSocketコネクションを切断しています。

ちなみに、この関数が実行されると以下のoncloseイベントハンドラーも呼び出されます。
oncloseはWebSocketコネクションが切断された時に呼び出されるイベントハンドラーです。

socketRef.current.onclose = () => {
console.log("WebSocketサーバとのコネクションをクローズしました");
setIsConnected(false);
};

最後に

いかがでしたでしょうか。
かなり長くなってしまいましたが、私がWebSocketの実装方法や概要を理解するのに使用したコードの解説をしてみました。

WebSocketはリアルタイム性の高い処理を実装する時に使うものっていうざっくりとしたイメージしかなかったのですが、実際にコードを書いてみて具体的に実装方法やWebSocketの特性について理解をすることができました。

また、今回のコードで「もっとこうしたほうがいいよ!」「この説明間違えてね?」みたいな箇所があればバンバンコメントいただけますと幸いです。

この記事が誰かのお役に立てれば幸いです!
最後までお読みいただきありがとうございました!


あ、そういえばいろいろやらせてもろてます。
よかったら覗いてみてあげてください。

【たまーに描いた絵をアップする X ( Twitter ) 】

https://x.com/monta_no_mori

【最近始めた Instagram 】

https://www.instagram.com/monta_no_mori/

【もんたのLINEスタンプ】

https://store.line.me/stickershop/author/2887587/ja

参考になった資料たち

websocketの理解にどぞ

https://www.freshvoice.net/knowledge/word/6323/

https://qiita.com/south37/items/6f92d4268fe676347160

https://www.goo-net.com/knowledge/17024/

https://qiita.com/theFirstPenguin/items/55dd1daa9313f6b90e2f

pythonでWebSocketサーバを構築するのにどぞ

https://fastapi.tiangolo.com/ja/advanced/websockets/

https://fastapi.tiangolo.com/reference/websockets/#fastapi.WebSocket.send_bytes

ReactでWebSocketクライアントを構築するのにどぞ

https://qiita.com/_ytori/items/a92d69760e8e8a2047ac

https://apidog.com/jp/blog/react-websocket-tutorial/

Discussion