WSRPC

Beide Seiten rufen auf.

WSRPC macht einen einzigen WebSocket symmetrisch. Der Browser ruft deine Handler auf, und ein Handler ruft den Browser auf und wartet auf die Antwort.

Browser Server verbinde

Diese Seite

    Die Verbindung

      Jede Zeile hier hat ein Aufruf des Servers in diese Seite geschrieben. Diese Seite nutzt den JavaScript-Client aus dem Paket, und diese Frames sind dieselben, die ein Python-Server austauscht.

      Derselbe Aufruf von jeder Seite

      Installiere das Paket, dann schreibe den Handler. Jeder Tab baut denselben Chat, der oben läuft.

      pip install wsrpc-aiohttp

      Eine Routenklasse bedient eine Verbindung. Der Handler schreibt deine Zeile in die Seite und antwortet dann mit einem Zitat.

      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]'

      Dieselbe Routenklasse, an einem WebSocket-Endpunkt von 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

      Ein Routenname mit Punkt, und ein typisierter Aufruf des Handlers zurück in die Seite.

      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

      Der Client registriert seine Route vor dem Verbinden, damit der Server sie aufrufen kann.

      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

      Die Seite registriert die Route, die der Server aufruft, und schreibt jede erhaltene Zeile.

      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;
        }
      }
      

      Das mitgelieferte Skript setzt ein globales WSRPC. Ohne Build-Schritt und ohne Bundler.

      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>
      

      Was die Verbindung bietet

      Aufrufe in beide Richtungen

      Ein Handler ruft eine Route auf, die der Browser registriert hat, und wartet im selben Socket auf das Ergebnis.

      Routen oder einfache Funktionen

      Veröffentliche eine Funktion oder eine Routenklasse, deren Instanz so lange lebt wie die Verbindung.

      Viele Aufrufe gleichzeitig

      Jeder Aufruf trägt seine eigene id, deshalb kommen Ergebnisse in beliebiger Reihenfolge beim Aufrufer an.

      Entfernte Fehler werfen lokal

      Ein Fehler der anderen Seite kommt mit Typ und Meldung an und wird dort geworfen, wo du aufgerufen hast.

      Signale für den Lebenszyklus

      Beobachte Verbindung, Trennung und jeden Aufruf, ohne einen Handler zu umhüllen.

      Blockierender Code hält die Loop nicht auf

      Asynchrone Handler laufen auf der Event-Loop. Synchrone Handler laufen in einem Thread-Pool.

      Der Weg eines Aufrufs

      Jeder Frame ist ein JSON-Objekt. Der Aufruf trägt id, Methode und Parameter; die Antwort kommt mit derselben id zurück. Beide Seiten senden Aufrufe, deshalb kann ein Handler den Browser fragen, während er einen Aufruf bearbeitet.

      Browser Server {"id": 3, "method": "chat.send", "params": {"text": "hi"}} {"id": 3, "result": {"delivered": true}}
      Der Browser ruft den Server auf.
      Browser Server Handler {"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}}
      Der Server ruft den Browser innerhalb des Aufrufs auf, den er bearbeitet.

      Wo es läuft

      Jedes Framework hier hat ein lauffähiges Beispiel und einen Test in seinem Repository.

      aiohttp-Server

      wsrpc-aiohttp

      • aiohttp

      ASGI-Server

      wsrpc-asgi

      • FastAPI
      • Starlette
      • Django
      • Channels
      • Litestar
      • Quart
      • Sanic
      • Falcon
      • BlackSheep
      • ASGI

      Node.js-Server

      @wsrpc/server

      • node:http
      • Express
      • Fastify

      Go-Server

      github.com/wsrpc/wsrpc-go

      • net/http

      Clients

      @wsrpc/client

      • Browsers
      • Node.js
      • Cloudflare Workers

      Welches Python-Paket?

      Zwei Implementierungen, ein Protokoll. Ein Python-Peer, ein Browser-Peer und ein Server lassen sich frei kombinieren.

      wsrpc-aiohttpwsrpc-asgi
      Server aiohttp-Web-Handler ASGI-Route, Middleware oder eigenständige Anwendung
      Installation pip install wsrpc-aiohttp pip install 'wsrpc-asgi[fastapi,client]'
      Python-Client auf aiohttp auf websockets
      JavaScript-Client enthalten derselbe Client
      JSON-Codec loads und dumps austauschbar ebenso