responderのwebsocketでbroadcast

ひさびさのresponderネタ。テーマは「接続中のwebsocketクライアント全員に対する一斉送信専用メソッド」。送信専用というところがキモで「自らもクライアントとなって自らに接続する」なんてぇ無駄でカッコ悪いことはしない。これまで「作っちゃ忘れちゃ」の繰り返しで毎回思い出すのに苦労していたので、その備忘録として。

responderでwebsocketサーバを構築して「接続中のクライアントからのメッセージを全クライアントに送信する」と、それだけなら本家サンプルのままでいい。が、本件の実現のためにはまず「接続中のクライアント各々」の情報を保持する必要がある。これは接続時に得られる’sec-websocket-key’を文字通りキーとしたdictとすれば良い。

# またこれを参照することによって、クライアントから受信したメッセージを「そのクライアント以外」にだけ送信することができる

次にいよいよbroadcastだが、ここでresponderが動作しているスレッド(以下「メインスレッド」とする)からクライアントに向けてsend_????しようとしても、awaitをつけると「SyntaxError: ‘await’ outside async function」でエラー。ならばとawaitを外すと今度は「RuntimeWarning: coroutine ‘***’ was never awaited」と警告が出て送信されない。原因はといえば「responderのwebsocketはイベントループで動作しているから」。要はそれが何なのかをわかってないからこうなるのであって、結論からいうと

  1. 予めメインスレッドのイベントループを取得しておく
  2. 上記1に対しコルーチン(=async修飾子つきのdef)を登録する

とすることで解決した。以下ソース。Python3.7.9/Ubuntuで動作確認したもの。メインスレッド以外(注1)からも使いやすいようにするため、ブロードキャストしたいメッセージをキューにputする仕組みになっている。念のためdictの操作を排他的に行うようLockを使っているが、これは不要かも。

注1: メインスレッド以外のスレッドからこれをするには、上記1を使わないと「There is no current event loop in thread …」となって動かない

import responder
from starlette.websockets import WebSocket, WebSocketDisconnect
from typing import Dict
import asyncio
from threading import Thread, Lock
from queue import Queue, Empty, Full


class WebSocketServer(object):
    def __init__(self, *, address: str = '0.0.0.0', port: int = 8080, location: str = '/'):
        self.address = address
        self.port = port
        self.location = location

        self.clients: Dict[str, WebSocket] = {}
        self.locker = Lock()

        self.elo = asyncio.get_event_loop()  # Eventloop Object
        self.bcPutTimeoutSecs = 5
        self.bcGetTimeoutSecs = 5
        self.bcCounter = 0
        self.bcQueue = Queue()  # Queue for message
        self.bcThread = Thread(target=self._bcWatcher, daemon=True)
        self.bcThread.start()

        self.api = responder.API()
        self.api.add_route(route=self.location, endpoint=self.wsSession, websocket=True)

        self.api.run(address=self.address, port=self.port)

    async def wsSession(self, ws: WebSocket):
        await ws.accept()
        key = ws.headers['sec-websocket-key']
        with self.locker:
            if key not in self.clients.keys():
                self.clients[key] = ws
        while True:
            try:
                message = await ws.receive_text()
            except (WebSocketDisconnect,) as e:
                break
            else:
                for k, v in self.clients.items():
                    if k != key:
                        await v.send_text(data=message)  # no exception ???

        with self.locker:
            if key in self.clients.keys():
                del self.clients[key]
        await ws.close()

    async def _onAir(self, *, message: str) -> None:
        with self.locker:
            for k, v in self.clients.items():
                await v.send_text(data=message)

    def _bcWatcher(self):
        while True:
            try:
                message = self.bcQueue.get(timeout=self.bcGetTimeoutSecs)
            except (Empty,) as e:
                self.broadCast(message='Queue is empty!')  # for debug
            else:
                self.elo.run_until_complete(future=self._onAir(message=message))
                self.bcCounter += 1

    # ---------------------------------------------------------------------------------
    def broadCast(self, *, message: str):
        try:
            self.bcQueue.put(item=message, timeout=self.bcPutTimeoutSecs)
        except (Full,) as e:
            print(e)
            pass
    # ---------------------------------------------------------------------------------


if __name__ == '__main__':
    def main():
        S = WebSocketServer()
        pass


    main()
 

オープンドレインで負論理でプルアップで

数年前、arduinoでi2cの加速度センサーを使おうとしてどっかのサイトを参考に、INTピンとGPIOとの間にプルアップ抵抗を挟んで動かした。以来「こういう局面ではこうするもの」という理解が刷り込まれ、常にそうしてきた。

ところが今回Pi4でこれを久々にやろうとして再度調べてみたら、実はプルアップもプルダウンもピンの設定だけでできるではないか。arduinoもそうだった。「早く言ってよー!」な気分 ← 人のせいにする。

思えばあの時は表題の文脈の単語それぞれの意味に全部引っかかり、電子回路の入門書と首っ引きでヒイコラやったもんだ。

# ちなみにワタクシ文系出身で、この業界に辿り着くまで電気の知識は「乾電池と豆電球」のみでした …

でも、それが良かった。あの時「GPIOの関数でプルアップを有効にすれば○」なサンプルをそっくりパクって「動いたOK!」でスルーしていたら、恐らくそれが如何に重要なことなのか何も知らないままでいた。そういう意味ではPCとかスマホとか、或いはWebアプリとかの作成ではハードの知識が不要になってきて敷居が下がり門戸も広がったかもしれんが、ハードの開発を伴うシステム開発でそれは通用しないな、と思う。

やはり俺にはこういうのが向いてるのかな、とも。

ISDNの終焉と新たなサービス体系への移行

64Kbps×2という夢のような通信速度(いまや笑)に感動し、月末に請求金額を見て腰を抜かしたあの頃が懐かしい。そんなINSネットも来年いっぱいでサービスを終了する。そのため20年も前に作った流通業者向けのシステムを、新たなサービス体系向けに再構築しなければならない。

そこで移行に向けて提供された資料を読んでいるんだが、なんともまあこれが新しいようでいて、実に古めかしい。後方互換が重要とみたのか、ろくでもないシガラミを重く引きずっていて「どうせやるならもうちいモダンでスマートなやり方があるでしょうに」とボヤきたくなってくるが、従うしかない(溜息)。

しかしもっと気の毒なのは、ここでまた多額の設備投資とべらぼうな月額利用料金を迫られる利用業者ら。それはこの業界・業態が、令和の今に至ってもあの親方日の丸由来の独占状態から脱皮していないことの、何よりの証拠。嗚呼嘆かわしや。

カタチあるものいつかは壊れる

我が家のWinが壊れて起動しなくなった。例によって何の前触れもなく、唐突かつ最悪なシチュエイションで。しかもこれ、買ってからまだ2年にも満たない。バックアップを怠っていたので、これはもうSSDを外して必要なのだけを吸い出すしかない。このパターン、これでいったい何度目か。

今回改めて思い知らされたこと、それは「Winはいつかは壊れる」ではなく「必ず壊れる」覚悟で使えという戒め。肝に銘じておきましょう。

※ 追記 このブッコワレPC、本体をバラすためのネジ穴すらない(涙)。ハンマーで叩き割るしかないのか!

遂にこのワタクシがMacUserに

急用で帰省することに。しかしノートWinは会社に置きっぱなしで、取りに行ってる時間がない。そこで一念発起、MacBook Air(M1)を購入した。Macは数年前まで仕事(Xcode)で使っていたが、個人での購入はこれが初である。ここでなぜMacを選んだのかといえば何より「Winが嫌い」なのに加え、最新のMacBookが携帯性に優れていると聞いていたから。どうせPyCharmしか使わないんだし、ここはいっちょ最新のMacを試してみようかなと。そこから殆ど予備知識ゼロでの衝動買いである。

開封してまず驚いたのは、ACアダプタが付いてないこと。正確にいうとiPadみたいな充電器とUSBのケーブルだけで、殆どスマホ。これでMacBookのみならず、iPadもAndroidも給電+充電できるのだからありがたい。もう既にこの段階で◎である。

次に驚いたのがその薄さと軽さ。これなら今のバッグに入れて持ち歩いても苦にならない。WinではPC専用のバッグが別途必要だったのとエラい違いだ。また静かで熱くならず、バッテリーの持ちも良いので外出中の作業も楽。これまた◎である。

とまあ、ここまではベタ褒めだが少々困った事も。まず最新のOS(Big Sur)とM1とに未対応のアプリとかがまだ多くて、現時点でそれらに関する情報が錯綜していること。PyCharmは問題なく動作したが、homebrewとかでけっこう苦労した(注1)。まあこれらもいずれ解決するんだろうが、やはり開発者向けの情報の整備という点がAppleはいつも弱い。

それと、シルバーやグレイではWinみたいで嫌だったので残ったゴールドを選んだんだが、もうちいボディ色にバリエーションがあってもいいのでは? このゴールドも金というより杏色という感じで半端だし。

それでもこれは満足度、充分に高い。コイツとは長い付き合いになりそうであーる。以上!

注1: WxFormBuilderで色々試したかったのにこれが動かない(涙)

知らなかった

いわゆる不特定多数向けWebアプリから遠ざかって数年、久々にその方面に着手して驚いた。なんと最近のWebブラウザでは、テーブルの中を<THEAD>と<TBODY>で明確に区切ってやるだけでスクロールしても<THEAD>が残るではないか! ただこれだけのことにエライ苦労した、あの頃の自分に見せてこれをやりたい気分だわ。改めて最新のブラウザ事情をチェックしてみようっと♪