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.
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.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]'
Dieselbe Routenklasse, an einem WebSocket-Endpunkt von 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
Ein Routenname mit Punkt, und ein typisierter Aufruf des Handlers zurück in die Seite.
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
Der Client registriert seine Route vor dem Verbinden, damit der Server sie aufrufen kann.
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
Die Seite registriert die Route, die der Server aufruft, und schreibt jede erhaltene Zeile.
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;
}
}
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.
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-aiohttp | wsrpc-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 |