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

## これはなに

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

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

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

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

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

::::note warn
<details><summary>ChatGPTのストリーミングについて [更新日:2024/10/30]</summary>

余談ですが、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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/396752c1-51b4-d713-54fe-b5a617ebb6ac.png)
</details>
::::

質問を投げると、生成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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/050d44ad-913a-b1e5-cb96-c6163b2c0ec0.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/1468fc99-798a-d96c-344e-ccdab3507ab0.png)

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


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

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


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

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

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

:::note info
<details><summary>WebSocketのユースケース</summary>
WebSocketを使うことでできることを効果的に示すため、リアルタイム性や双方向通信が求められるユースケースを例に挙げると理解が深まります。以下の具体例は、その特徴が活かされる代表的なユースケースです：

1. **チャットアプリケーション**  
   WebSocketの双方向通信を活用して、ユーザー同士のメッセージが即時に反映されるチャットアプリを構築できます。例えば、SlackやLINEのように、相手がメッセージを送信した瞬間にリアルタイムで表示されるため、スムーズなコミュニケーションが可能です。

2. **ライブ通知やプッシュ通知**  
   新しいイベントや通知が発生した際に、即座にユーザーに通知を届けるアプリケーションに利用されます。例えば、SNSでのコメントやフォロワーの更新通知、eコマースサイトの「在庫切れ」通知など、変化がすぐにユーザーに通知される場面で役立ちます。

3. **オンラインゲームのリアルタイム通信**  
   オンラインマルチプレイヤーゲームなど、素早いデータのやり取りが重要なゲームでもWebSocketが利用されます。プレイヤーの動作やステータスが即時に他のプレイヤーに共有されるため、スムーズなプレイ体験が実現できます。

4. **株価や暗号通貨のリアルタイム更新**  
   株価や暗号通貨の取引データがリアルタイムで変動する場面でもWebSocketが役立ちます。WebSocketを使えば、価格の変動が画面上に即座に反映されるため、投資家が最新の情報を元に取引を行いやすくなります。

5. **IoTデバイスのモニタリング**  
   センサーなどのIoTデバイスが生成するデータをリアルタイムでモニタリングしたり、制御したりする場面でもWebSocketは効果的です。例えば、スマートホームアプリで、温度や湿度、セキュリティ状況を即時に反映させ、必要に応じてユーザーが遠隔操作する際に活用されます。

6. **コラボレーションツールのリアルタイム編集**  
   FigmaやGoogle Docsのように、複数のユーザーが同じドキュメントやデザインを同時に編集できるアプリケーションにもWebSocketが使われます。WebSocketによって他のユーザーの編集内容が瞬時に反映され、共同編集がスムーズに行えます。

これらの例を使うことで、WebSocketがもたらす「リアルタイム通信」や「双方向性」の利点が理解しやすくなります。また、それぞれのユースケースで「なぜHTTPではなくWebSocketが必要か」を説明すると、より説得力が増すでしょう。
</details>
:::

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




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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/fffafb2e-638f-4865-474f-17760524437c.png)


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

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


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

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/b343009b-f826-23a1-ea40-0c4e3f0bdc5f.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

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


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/dc14e4aa-dad7-3638-e3f4-20fb2fb010e5.png)

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


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


## ハンズオン

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

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

- **成果物イメージ**

![2024-10-2814.37.19-ezgif.com-video-to-gif-converter (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/89b8a37e-e43e-1b3b-ef6a-b6cc00f09995.gif)

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/69f6163e-7c05-0e2f-f913-388410950ee9.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. インフラ周り
インフラ周りのコードです。
これぺたぺたして環境構築してもらえればと思います。

```yaml
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**
```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**
```Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY . /app

RUN npm install

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


#### 2. python-server周り

- **main.py**
```python
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**
```python
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**
```javascript
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**
```javascript
"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サーバのコードの解説をしていこうかと思います。

```python
@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コネクションの確立を行なっています。
```python
@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. サーバ側からクライアント側に定期的にテキストを送信するタスクの実行

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

```python
send_task = asyncio.create_task(send_messages_periodically(websocket))
```

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

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

```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/89b8a37e-e43e-1b3b-ef6a-b6cc00f09995.gif)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/6feb1dce-a513-8df8-b287-b95746924378.png)



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

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

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

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

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

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/69f6163e-7c05-0e2f-f913-388410950ee9.png)


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

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

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

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

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



## WebSocketクライアントの解説（Next.js）

```javascript
"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コネクションの確立

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

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

```javascript
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）に接続を試みるものです。

```jsx
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サーバとの接続ができたことを通知する処理が書かれてあります。

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

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


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

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

```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2038763/89b8a37e-e43e-1b3b-ef6a-b6cc00f09995.gif)

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


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


```javascript
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側のコードで以下のような実装をしていました。

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

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

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


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

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

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

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

```javascript
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/