Python でシンプルなオウム返しをする WebSocketサーバーを作ってみる（＋ Node.js のクライアントからメッセージを送ってみる）

## はじめに
ローカルLLM を扱うプログラムを外部のアプリ・プログラムと連携させたいと考えていて、いったんそれに活用できそうなリアルタイム通信を試してみる話です。

それで、WebSocket を扱ってみようと思います。

### JavaScript でリアルタイム通信
リアルタイム通信自体は、過去に JavaScript（Node.js、ブラウザで動作させるものの両方）や、IoT に使えるデバイスで扱っていて、WebSocket や MQTT を使っていました。

#### 過去に書いている記事
Qiita にもたくさん記事を書いていて、キーワード ＋ ユーザー名での検索をかけると、それらが出てきます。

●「websocket user:youtoy」の検索結果 - Qiita
　https://qiita.com/search?q=websocket%20user%3Ayoutoy&sort=created

●「mqtt user:youtoy」の検索結果 - Qiita
　https://qiita.com/search?sort=created&q=mqtt+user%3Ayoutoy

WebSocket を扱う場合、WebSocketサーバーはよく [ws](https://www.npmjs.com/package/ws) を使っていたりします。

## Python でシンプルな WebSocketサーバー
今回、Python でシンプルな WebSocketサーバーを作ってみます。

### ベースを ChatGPT で作る
ベースのところは、まずは ChatGPT（モデルは o3-mini-high）で生成してみます。

そうすると、以下のような内容を提示されました。

```python
import asyncio
import websockets

async def echo(websocket, path):
    print("クライアントが接続しました")
    try:
        async for message in websocket:
            print(f"受信: {message}")
            response = f"Echo: {message}"
            await websocket.send(response)
            print(f"送信: {response}")
    except websockets.exceptions.ConnectionClosed:
        print("クライアントとの接続が閉じられました")

async def main():
    # localhost の 8765 番ポートでサーバーを起動
    async with websockets.serve(echo, "localhost", 8765):
        print("WebSocketサーバーが起動しました")
        await asyncio.Future()  # 永久に待機

if __name__ == '__main__':
    asyncio.run(main())
```

以下の 2つを使った実装になるようです。

●websockets · PyPI
　https://pypi.org/project/websockets/

●asyncio · PyPI
　https://pypi.org/project/asyncio/

### ドキュメント等を確認して書きかえる

とりあえず、上で書いたページの中の [websockets](https://pypi.org/project/websockets/) の方を見てみます。

![2025-02-09_01-48-35.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/e6d38497-f790-62f0-7e5f-b7bdcb8715f6.jpeg)

あわせてドキュメントも参照してみます。

●websockets 14.2 documentation
　https://websockets.readthedocs.io/en/stable/

![2025-02-09_02-06-46.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/505c8624-26d6-02fc-7285-10513795a2d9.jpeg)

#### 書きかえ後の内容
それらを見つつ、先ほどのプログラムを以下のようにしてみました。

```python
import asyncio
from websockets.asyncio.server import serve

async def echo(websocket):
    print("クライアントが接続しました")
    try:
        async for message in websocket:
            print(f"受信: {message}")
            response = f"Echo: {message}"
            await websocket.send(response)
            print(f"送信: {response}")
    except Exception as e:
        print("エラー:", e)
    finally:
        print("クライアントとの接続が終了しました")

async def main():
    async with serve(echo, "localhost", 8765) as server:
        print("WebSocketサーバーが起動しました")
        await server.serve_forever()


if __name__ == '__main__':
    asyncio.run(main())
```

最初に掲載していたものと `websockets.asyncio.server` や `server.serve_forever()` というあたりは変えていますが、その部分は公式のドキュメントの以下を参照して書きかえたものです。

●Server (new asyncio) - websockets 14.2 documentation
　https://websockets.readthedocs.io/en/stable/reference/asyncio/server.html


### 仮想環境で動かす
上記を仮想環境で動かすことにします。

まずは以下で仮想環境の作成などを行います（※ 以下の 2行目は Mac用の処理です）。

```zsh
python -m venv myenv
source myenv/bin/activate
```

次に仮想環境内で websockets をインストールします。

```zsh
pip install websockets
```

そしてこの環境で、上記の Python のプログラムを実行してみます。

#### クライアントから接続
さらに、クライアントから接続してみます。

Node.js からの接続を試します。
以前、以下の記事で書いたように Node.js v22 以降では、デフォルトで WebSocket のクライアントの処理が扱えます。

●Node.js と ws でシンプルな WebSocketサーバー ＋ Node.js v22 でフラグなしで使える WebSocket - Qiita
　https://qiita.com/youtoy/items/9e1eeae728dc98f15679

上記の記事で書いた、以下のプログラムを使うことにします。

```javascript
const socket = new WebSocket("ws://localhost:8765");

socket.addEventListener("open", (event) => {
  socket.send("Hello Server!");
});

socket.addEventListener("message", (event) => {
  console.log("Message from server ", event.data);
});
```

上記を実行した後の、サーバー側の出力を確認してみます。

![2025-02-09_02-15-14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/e13ab0c1-89b8-5364-8428-20647f434884.jpeg)

上記のとおり、クライアントからのメッセージを受信し、そしてオウム返しをしています。

その後のクライアント側の出力も確認してみます。

![2025-02-09_02-17-43.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9a351f8c-738a-e1ca-d720-344df6cd4053.jpeg)

サーバーからのオウム返しされたメッセージを受信できているのが分かります。

また、メッセージの内容を少しだけ変えた 2つ目のクライアントを用意し、それを実行してみます。
その結果、以下のように 2つめのクライアントからのメッセージの受信と、それに対するオウム返しが成功しているのが分かります。

![2025-02-09_02-16-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/18c7c73c-dece-bbe7-0ded-fe541344f22e.png)

ひとまず簡単なものですが、Python の WebSocketサーバーと、Node.js の WebSocketクライアントとの間での通信ができたことが確認できました。