Вызывают обе стороны.
WSRPC делает один WebSocket симметричным. Браузер вызывает ваши обработчики, а обработчик вызывает браузер и ждёт ответа.
Эта страница
Соединение
Каждую строку здесь нарисовал вызов сервера в эту страницу. Страница работает на том JavaScript-клиенте, который входит в пакет, а кадры — те же, которыми обменивается сервер на Python.
Один и тот же вызов с любой стороны
Поставьте пакет, потом напишите обработчик. В каждой вкладке — тот же чат, что работает выше.
pip install wsrpc-aiohttp
Класс-маршрут живёт столько же, сколько соединение. Обработчик рисует вашу строку в странице и отвечает цитатой.
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]'
Тот же класс-маршрут, подключённый к WebSocket-эндпоинту FastAPI.
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, поэтому результаты приходят своим вызывающим в любом порядке.
Удалённые ошибки поднимаются локально
Сбой на другой стороне приходит с типом и сообщением и поднимается там, где вы сделали вызов.
Сигналы жизненного цикла
Следите за подключением, отключением и каждым вызовом, не оборачивая обработчики.
Блокирующий код не держит цикл
Асинхронные обработчики идут в цикле событий. Синхронные выполняются в пуле потоков.
Как проходит вызов
Каждый кадр — один объект 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 выбрать
Две реализации, один протокол. Клиент на Python, браузер и сервер соединяются в любом сочетании.
wsrpc-aiohttp | wsrpc-asgi | |
|---|---|---|
| Сервер | обработчик aiohttp | маршрут ASGI, middleware или отдельное приложение |
| Установка | pip install wsrpc-aiohttp | pip install 'wsrpc-asgi[fastapi,client]' |
| Клиент на Python | на aiohttp | на websockets |
| JavaScript-клиент | в комплекте | тот же клиент |
| Кодек JSON | loads и dumps заменяемы | так же |