どちらの端からも呼び出せます。
WSRPC は 1 本の WebSocket を対称にします。ブラウザがハンドラを呼び、ハンドラはブラウザを呼んで結果を待ちます。
このページ
接続
ここの各行は、サーバがこのページを呼び出して描いたものです。 このページはパッケージ同梱の JavaScript クライアントで動きます。これらのフレームは Python サーバがやり取りするものと同じです。
どちらの側からも同じ呼び出し
パッケージを入れて、ハンドラを書きます。どのタブも、上で動いているのと同じチャットを作ります。
pip install wsrpc-aiohttp
ルートクラスは 1 つの接続を担当します。ハンドラはあなたの行をページに描き、続けて台詞で答えます。
server.pyimport random
from aiohttp import web
from wsrpc_aiohttp import STATIC_DIR, Route, WebSocketAsync, decorators
QUOTES = [
("HAL 9000", "I'm sorry, Dave. I'm afraid I can't do that."),
("The Oracle", "There is no spoon."),
("T-800", "I'll be back."),
]
class Socket(WebSocketAsync):
pass
class Chat(Route[Socket]):
@decorators.proxy
async def send(self, *, text: str) -> dict:
# Draw the line in the page that called, and wait for its answer.
await self.socket.proxy.chat.append(text=text, mine=True)
sender, reply = random.choice(QUOTES)
await self.socket.proxy.chat.append(sender=sender, text=reply)
return {"delivered": True}
def create_app() -> web.Application:
Socket.add_route("chat", Chat)
app = web.Application()
app.router.add_get("/ws/", Socket)
app.router.add_static("/js/", STATIC_DIR)
return app
if __name__ == "__main__":
web.run_app(create_app(), host="127.0.0.1", port=8000)
pip install 'wsrpc-asgi[fastapi]'
同じルートクラスを、FastAPI の WebSocket エンドポイントに載せます。
app.pyimport random
from fastapi import FastAPI, WebSocket
from wsrpc_asgi import Route, WebSocketAsync, decorators
QUOTES = [
("HAL 9000", "I'm sorry, Dave. I'm afraid I can't do that."),
("The Oracle", "There is no spoon."),
("T-800", "I'll be back."),
]
class RPC(WebSocketAsync):
pass
class Chat(Route[RPC]):
@decorators.proxy
async def send(self, *, text: str) -> dict:
# Draw the line in the page that called, and wait for its answer.
await self.socket.proxy.chat.append(text=text, mine=True)
sender, reply = random.choice(QUOTES)
await self.socket.proxy.chat.append(sender=sender, text=reply)
return {"delivered": True}
RPC.add_route("chat", Chat)
app = FastAPI()
@app.websocket("/ws/")
async def rpc(websocket: WebSocket) -> None:
await RPC.serve(websocket)
go get github.com/wsrpc/wsrpc-go
ドット付きのルート名と、ハンドラからページへ戻る型付きの呼び出しです。
main.gopackage main
import (
"context"
"log"
"math/rand/v2"
"net/http"
wsrpc "github.com/wsrpc/wsrpc-go"
"github.com/wsrpc/wsrpc-go/ws"
)
type Line struct {
Sender string `json:"sender,omitempty"`
Text string `json:"text"`
Mine bool `json:"mine,omitempty"`
}
type Sent struct {
Text string `json:"text"`
}
type Delivered struct {
Delivered bool `json:"delivered"`
}
type Seen struct {
Seen bool `json:"seen"`
}
var quotes = []Line{
{Sender: "HAL 9000", Text: "I'm sorry, Dave. I'm afraid I can't do that."},
{Sender: "The Oracle", Text: "There is no spoon."},
{Sender: "T-800", Text: "I'll be back."},
}
type Chat struct{}
// Draw the line in the page that called, and wait for its answer.
func (Chat) Send(ctx context.Context, p Sent) (Delivered, error) {
peer, _ := wsrpc.FromContext(ctx)
if _, err := peer.Call[Seen](ctx, "chat.append", Line{Text: p.Text, Mine: true}); err != nil {
return Delivered{}, err
}
reply := quotes[rand.IntN(len(quotes))]
if _, err := peer.Call[Seen](ctx, "chat.append", reply); err != nil {
return Delivered{}, err
}
return Delivered{Delivered: true}, nil
}
func main() {
routes := new(wsrpc.Router)
if err := routes.Handle("chat.send", Chat{}.Send); err != nil {
log.Fatal(err)
}
rpc, err := ws.NewServer(ws.ServerOptions{Peer: wsrpc.Options{Routes: routes}})
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle("/ws/", rpc)
log.Fatal(http.ListenAndServe("127.0.0.1:8000", mux))
}
pip install wsrpc-aiohttp
クライアントは接続前にルートを登録します。そうすればサーバから呼び出せます。
client.pyimport asyncio
from wsrpc_aiohttp import Route, WSRPCClient, decorators
class Chat(Route):
@decorators.proxy
def append(self, *, text: str, sender: str = "you", mine: bool = False) -> dict:
# The server calls this method. Draw the line and confirm it.
print(f"{sender}: {text}")
return {"seen": True}
async def main() -> None:
client = WSRPCClient("http://127.0.0.1:8000/ws/")
# Register the route before the connection opens.
client.add_route("chat", Chat)
async with client:
print(await client.proxy.chat.send(text="Hello"))
await asyncio.sleep(1)
if __name__ == "__main__":
asyncio.run(main())
npm install @wsrpc/client
ページはサーバが呼ぶルートを登録し、受け取った行をすべて描きます。
chat.tsimport WSRPC from "@wsrpc/client";
interface Line {
sender?: string;
text: string;
mine?: boolean;
}
const chat = document.getElementById("chat") as HTMLElement;
const rpc = new WSRPC("/ws/");
// The server calls this route. The page draws what it gets.
rpc.addRoute("chat.append", (line: Line) => {
const row = document.createElement("p");
row.textContent = `${line.sender ?? "you"}: ${line.text}`;
chat.append(row);
return { seen: true };
});
rpc.connect();
export async function say(text: string): Promise<void> {
await rpc.call<{ delivered: boolean }>("chat.send", { text });
}
wsrpc-client.d.ts// The package ships wsrpc.d.ts. Map it onto the module name once.
declare module "@wsrpc/client" {
export default class WSRPC {
constructor(url: string, reconnectTimeout?: number);
connect(): void;
destroy(): void;
state(): "CONNECTING" | "OPEN" | "CLOSING" | "CLOSED";
call<T>(method: string, params?: object): Promise<T>;
addRoute(
route: string,
handler: (params: never) => unknown,
isAsync?: boolean,
): void;
}
}
同梱スクリプトがグローバルの WSRPC を用意します。ビルドもバンドラも不要です。
index.html<form id="say"><input name="text" autocomplete="off"></form>
<div id="chat"></div>
<script src="/js/wsrpc.min.js"></script>
<script>
const rpc = new WSRPC("/ws/");
// The server calls this route. The page draws what it gets.
rpc.addRoute("chat.append", function (line) {
const row = document.createElement("p");
row.textContent = (line.sender || "you") + ": " + line.text;
document.getElementById("chat").append(row);
return {seen: true};
});
rpc.connect();
document.getElementById("say").addEventListener("submit", function (event) {
event.preventDefault();
rpc.proxy.chat.send({text: event.target.text.value});
event.target.reset();
});
</script>
この接続で得られるもの
双方向の呼び出し
ハンドラはブラウザが登録したルートを呼び、同じソケットで結果を待ちます。
ルートクラスか素の関数
関数を公開できます。ルートクラスを公開すると、そのインスタンスは接続と同じ寿命になります。
同時に多数の呼び出し
呼び出しごとに id を持つので、結果は順序に関係なく各呼び出し元へ戻ります。
遠端のエラーをローカルで送出
相手側の失敗は型とメッセージを伴って届き、呼び出した場所で例外になります。
ライフサイクルのシグナル
接続、切断、各呼び出しを観測できます。ハンドラを包む必要はありません。
ブロッキング処理はループを止めない
非同期ハンドラはイベントループで動き、同期ハンドラはスレッドプールで動きます。
呼び出しの流れ
各フレームは 1 つの JSON オブジェクトです。呼び出しは id、メソッド、パラメータを運び、応答は同じ id で戻ります。両端が呼び出しを送れるため、ハンドラは呼び出しの処理中にブラウザへ問い合わせできます。
動作環境
ここにある各フレームワークには、実行できる例とリポジトリ内のテストがあります。
aiohttp サーバ
wsrpc-aiohttp
- aiohttp
ASGI サーバ
wsrpc-asgi
- FastAPI
- Starlette
- Django
- Channels
- Litestar
- Quart
- Sanic
- Falcon
- BlackSheep
- ASGI
Node.js サーバ
@wsrpc/server
- node:http
- Express
- Fastify
Go サーバ
github.com/wsrpc/wsrpc-go
- net/http
クライアント
@wsrpc/client
- Browsers
- Node.js
- Cloudflare Workers
どちらの Python パッケージか
実装は 2 つ、プロトコルは 1 つです。Python ピア、ブラウザピア、サーバを自由に組み合わせられます。
wsrpc-aiohttp | wsrpc-asgi | |
|---|---|---|
| サーバ | aiohttp のハンドラ | ASGI のルート、ミドルウェア、単体アプリ |
| インストール | pip install wsrpc-aiohttp | pip install 'wsrpc-asgi[fastapi,client]' |
| Python クライアント | aiohttp ベース | websockets ベース |
| JavaScript クライアント | 同梱 | 同じクライアント |
| JSON コーデック | loads と dumps を差し替え可能 | 同じ |