WSRPC

Вызывают обе стороны.

WSRPC делает один WebSocket симметричным. Браузер вызывает ваши обработчики, а обработчик вызывает браузер и ждёт ответа.

браузер сервер подключение

Эта страница

    Соединение

      Каждую строку здесь нарисовал вызов сервера в эту страницу. Страница работает на том JavaScript-клиенте, который входит в пакет, а кадры — те же, которыми обменивается сервер на Python.

      Один и тот же вызов с любой стороны

      Поставьте пакет, потом напишите обработчик. В каждой вкладке — тот же чат, что работает выше.

      pip install wsrpc-aiohttp

      Класс-маршрут живёт столько же, сколько соединение. Обработчик рисует вашу строку в странице и отвечает цитатой.

      server.py
      import 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.py
      import 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.go
      package 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.py
      import 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.ts
      import 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. Вызовы отправляют обе стороны, поэтому обработчик может спросить браузер, пока обслуживает вызов.

      браузер сервер {"id": 3, "method": "chat.send", "params": {"text": "hi"}} {"id": 3, "result": {"delivered": true}}
      Браузер вызывает сервер.
      браузер сервер обработчик {"id": 5, "method": "chat.send", "params": {"text": "hi"}} {"id": 2, "method": "chat.append", "params": {"text": "hi"}} {"id": 2, "result": {"seen": true}} {"id": 5, "result": {"delivered": true}}
      Сервер вызывает браузер внутри обслуживаемого вызова.

      Где это работает

      Для каждого фреймворка здесь есть рабочий пример и тест в его репозитории.

      Сервер на 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-aiohttpwsrpc-asgi
      Сервер обработчик aiohttp маршрут ASGI, middleware или отдельное приложение
      Установка pip install wsrpc-aiohttp pip install 'wsrpc-asgi[fastapi,client]'
      Клиент на Python на aiohttp на websockets
      JavaScript-клиент в комплекте тот же клиент
      Кодек JSON loads и dumps заменяемы так же