続・Python で WebSocket：Python のクライアントとサーバーでのブロードキャスト

以下の記事で簡単なお試しを少しやっていた、「Python を使った WebSocket によるリアルタイム通信」の話の続きです。

●Python でシンプルなオウム返しをする WebSocketサーバーを作ってみる（＋ Node.js のクライアントからメッセージを送ってみる） - Qiita
　https://qiita.com/youtoy/items/f74e4ae26a1b67c310c0

今回は、以下の内容を追加でやってみました。

- WebSocket を使った通信のクライアント側を Python で実装（※ 前回は Node.js で実装したクライアントを利用）
   - クライアント側のメッセージ送信処理を、プログラム実行時の単発の処理だけではなく、一定の時間間隔で定期実行する処理にしてみる
- サーバー側でメッセージを受け取った時に、複数のクライアントが接続している場合には、送信元にオウム返しするだけでなく他の接続元クライアントを含めてのブロードキャストを行う

## 簡単な通信を試す
まずは、サーバーもクライアントも Python を使った形で、簡単な WebSocket による通信を試します。

### クライアント側
冒頭の記事で参照していた公式のドキュメントを見て、クライアント側を実装します。

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

```python
import asyncio
from websockets.asyncio.client import connect

async def hello():
    async with connect("ws://localhost:8765") as websocket:
        await websocket.send("Hello world!")
        message = await websocket.recv()
        print(message)


if __name__ == "__main__":
    asyncio.run(hello())
```

### サーバー側の準備と仮想環境を用いた動作確認
サーバーは、ひとまずは前回作ったものを利用します。
そして、前回作った仮想環境も流用し、そこで WebSocket を使った通信のサーバー・クライアントの両方の処理を試します。

作成済みの仮想環境を、以下で利用します（※ Mac用の処理です）。

```zsh
source myenv/bin/activate
```

そしてサーバーを動作させた後に、上記のクライアント側の処理を実行します。

その結果、サーバー・クライアント側の両方でメッセージのやりとりが成功しているのが確認できました。

## 連続した送信をするクライアントを作って試す
次に、クライアント側の送信処理を単発のものではなく、一定の時間間隔でメッセージを送る処理にしてみます。

送る内容は、送信処理ごとにカウントアップしていく数字とし、送信処理を 1秒間隔ごとに行うようにしました。

その際、実装内容の参考情報として以下の公式ドキュメントを用いました。

●Quick start - websockets 14.2 documentation
　https://websockets.readthedocs.io/en/stable/howto/quickstart.html

具体的な実装内容は、以下の通りです。

```python
import asyncio
from websockets.asyncio.client import connect

async def send_numbers(websocket):
    counter = 1
    while True:
        await websocket.send(str(counter))
        print(f"Sent: {counter}")
        counter += 1
        await asyncio.sleep(1)

async def receive_messages(websocket):
    while True:
        message = await websocket.recv()
        print(f"Received: {message}")

async def run_client():
    async with connect("ws://localhost:8765") as websocket:
        send_task = asyncio.create_task(send_numbers(websocket))
        receive_task = asyncio.create_task(receive_messages(websocket))
        await asyncio.gather(send_task, receive_task)

if __name__ == "__main__":
    asyncio.run(run_client())
```

上記でも用いた `asyncio.create_task()` や `asyncio.gather()` に関しては、以下にも事例が書かれているものです。

●Using asyncio - websockets 14.2 documentation
　https://websockets.readthedocs.io/en/stable/faq/asyncio.html

上記のクライアントとサーバーとを動作させてみた時の様子は以下の通りで、想定通りの動作となりました（左上のほうがサーバー、右側にあるほうがクライアントです）。

<iframe width="560" height="315" src="https://www.youtube.com/embed/qW4-leCBl4g?si=ySsdflGkpyN41G9z" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## ブロードキャストを扱う

最後に、複数のクライアントが接続されている場合に、いずれかのクライアントからサーバーへのメッセージ送信が行われたら、サーバーから全クライアントへのメッセージ送信をする形にしたものを試してみます。

### サーバー側の実装

前回作ったサーバーの処理に手を加えます。

公式ドキュメントでやり方を確認しつつ、以下のようにしてみました。

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

connected_clients = set()

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

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

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

クライアント側を複数にするため、先ほどの用いたクライアントと別に、もう 1つクライアントを用意します。

もう 1つ用意したクライアントは、基本的な処理はほぼ同じで、少しだけ違いを作りました。具体的には、単純に `await asyncio.sleep(1)` を `await asyncio.sleep(2.5)` に変えただけです。

それを動作させた時の様子が以下です。

<iframe width="560" height="315" src="https://www.youtube.com/embed/YAJeOscnZQ0?si=hbygxdKa-ewev4jX" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

画面の上にあるのがサーバーで、画面の左下と右下にあるのがクライアントです。これも、想定通りの動作をしていることが確認できました。