概要

PythonのWebフレームワークであるBottleで、WebSocketサーバの実装サンプルが紹介されています。

ただし、上記の内容だと何かデータをWebSocket経由で投げてきたユーザにレスポンスするのみなので、今一WebSocket感が出ていません。
そこでその内容に手を加えて、クライアントから送られてきたデータを、このWebSocketサーバ(Bottle)に接続中の別のクライアントにもブロードキャストするサンプルを書いてみました。

コード

WebSocketサーバ

コレがWebSocketサーバの全コードです。
Bottleを使えばこんなに短いコードでサーバが実装できます!
なお、事前にインストールしておかないといけないライブラリは記事の最後の方に記述します。
以下をmain.pyという名前で保存してください。

from bottle import request, Bottle, abort, static_file
from gevent.pywsgi import WSGIServer
from geventwebsocket import WebSocketError
from geventwebsocket.handler import WebSocketHandler, Client
from geventwebsocket.websocket import WebSocket
from typing import Any, Dict, List, Callable, Tuple, Type

app: Bottle = Bottle()
server: WSGIServer = WSGIServer(("0.0.0.0", 8888), app, handler_class=WebSocketHandler)

# WebSocketの処理
@app.route('/websocket')
def handle_websocket() -> Any:
    websocket: WebSocket = request.environ.get('wsgi.websocket')

    if not websocket:
        abort(400, 'Expected WebSocket request.')

    #予想だけど、WebSocketHandlerはクライアントごとに生成される?
    while True:
        try:
            handler: WebSocketHandler = websocket.handler
            message: str = websocket.receive()

            # 実際の動作に必要でないけど、以下のようなクラスのインスタンスが入ってるのが解る
            assert type(websocket.handler) == WebSocketHandler
            assert type(websocket.handler.server) == WSGIServer

            # WSGIServerには本来、clientというプロパティは無いけど、コレを継承しているWebSocketHandlerで動的に追加されてる。
            # なので、型ヒントが利用できない(?)のでignoreするように
            for client_tmp in handler.server.clients.values(): # type: ignore

                # for文では型ヒントが利用できないので、以下のように型ヒントを付けた変数に再代入している。
                client: Client = client_tmp

                if client.ws.environ:
                    print(client.ws.environ.get('HTTP_SEC_WEBSOCKET_KEY', ''))
                else:
                    print('空。ブラウザを閉じたり画面を更新したりすると、クライアントから「切断したよ」、という情報が飛んでくる。')

                client.ws.send('message: %s, (client_address :%s)' % (message, client.address))

        except WebSocketError:
            break

# WebSocketクライアントを返す
@app.route('/')
def top():
    return static_file('index.html', root='./')

server.serve_forever()

ちなみに、コードを見て頂ければ解ると思いますが、型ヒントを利用しています。
型ヒントを利用するために余計な再代入している箇所など有りますが、後からコードを読みなおしたりする際に本当に助かるので、型ヒントは今後は積極的に付けていったほうが良いなと思います。

WebSocketクライアント

こちらはVue.jsを利用してWebSocketクライアントを実装しました。CDN上の物を利用していますので、特に事前にライブラリなどをインストールする必要はありません。
このHTMLは、上記のBottleで実装したサーバのdef top()にてブラウザに返されるものです。
ということでファイル名はindex.htmlとして、上記のmain.pyと同じディレクトリに保存しておいてください。

<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template</title>
<!--CSS-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
<style>
</style>
<!-- load JavaScripts-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.15/dist/vue.js"></script>
</head>
<body>
<section class="section" id="app">
    <div class="container">
        <input type="text" class="input" v-model="text" @keyup.enter="send"/>
        <p v-for="text in messages">{{text}}</p>
    </div>
</section>

<script>
const vm = new Vue({
    el: '#app',
    data: {
        text: '',
        messages: [],
        ws: new WebSocket("ws://localhost:8888/websocket")
    },
    mounted: function () {
        // WebSocketのコネクション開いた初回の動作
        this.ws.onopen = () => {
            this.ws.send("Hello, world")
        }

        // サーバからWebSocketコネクション経由でなんか来た時の動作
        this.ws.onmessage = (evt) => {
            this.messages.push(evt.data)
        }
    },
    methods: {
        send: function() {
            this.ws.send(this.text)
            this.text = ''
        }
    }
})
</script>
</body>
</html>

実行

これで、python main.pyと実行すればサーバが起動しますので、http://localhost:8888/に複数のブラウザでアクセスして動作を確認してみてください。
テキストフィールドがあるので、そこに何かを入力してEnterすれば、他のブラウザにも同じ内容がサーバからブロードキャストされることが確認できるはずです。

必要なライブラリ

今回のコードを書く際に私がインストールしたライブラリは以下の4つです。

  • bottle
  • mypy
  • gevent
  • gevent-websocket

以下がpip freezeの結果です。上記の4つ以外の物は自動的にインストールされたものになります。

(bottle) [koji:websocket]$ pip freeze
bottle==0.12.13
gevent==1.2.2
gevent-websocket==0.10.1
greenlet==0.4.13
mypy==0.580
typed-ast==1.1.0
(bottle) [koji:websocket]$