Los dos extremos llaman.
WSRPC vuelve simétrico un solo WebSocket. El navegador llama a tus manejadores, y un manejador llama al navegador y espera la respuesta.
Esta página
La conexión
Cada línea de aquí la dibujó una llamada del servidor a esta página. Esta página usa el cliente JavaScript que viene con el paquete, y estas tramas son las que intercambia un servidor de Python.
La misma llamada desde cualquier extremo
Instala el paquete y escribe el manejador. Cada pestaña construye el mismo chat que funciona arriba.
pip install wsrpc-aiohttp
Una clase de ruta atiende una conexión. El manejador dibuja tu línea en la página y luego responde con una cita.
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]'
La misma clase de ruta, montada en un endpoint WebSocket de 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
Un nombre de ruta con punto, y una llamada tipada del manejador de vuelta a la página.
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
El cliente registra su ruta antes de conectarse, para que el servidor pueda llamarla.
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
La página registra la ruta que llama el servidor y dibuja cada línea que recibe.
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;
}
}
El script incluido añade un global WSRPC. Sin paso de compilación ni 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>
Qué ofrece la conexión
Llamadas en ambos sentidos
Un manejador llama a una ruta que registró el navegador y espera su resultado en el mismo socket.
Rutas o funciones simples
Expón una función, o una clase de ruta cuya instancia vive tanto como la conexión que atiende.
Muchas llamadas a la vez
Cada llamada lleva su propio id, así los resultados vuelven a quien llamó, en cualquier orden.
Los errores remotos se lanzan en local
Un fallo del otro extremo llega con su tipo y su mensaje, y se lanza donde hiciste la llamada.
Señales del ciclo de vida
Observa la conexión, la desconexión y cada llamada sin envolver ningún manejador.
El código bloqueante no ocupa el bucle
Los manejadores asíncronos corren en el bucle de eventos. Los síncronos, en un pool de hilos.
Cómo viaja una llamada
Cada trama es un objeto JSON. La llamada lleva un id, un método y parámetros; la respuesta vuelve con el mismo id. Ambos extremos envían llamadas, así que un manejador puede preguntar al navegador mientras atiende una.
Dónde funciona
Cada framework aquí tiene un ejemplo ejecutable y una prueba en su repositorio.
Servidor aiohttp
wsrpc-aiohttp
- aiohttp
Servidores ASGI
wsrpc-asgi
- FastAPI
- Starlette
- Django
- Channels
- Litestar
- Quart
- Sanic
- Falcon
- BlackSheep
- ASGI
Servidores Node.js
@wsrpc/server
- node:http
- Express
- Fastify
Servidores Go
github.com/wsrpc/wsrpc-go
- net/http
Clientes
@wsrpc/client
- Browsers
- Node.js
- Cloudflare Workers
¿Qué paquete de Python?
Dos implementaciones, un protocolo. Un par en Python, un par en el navegador y un servidor se combinan libremente.
wsrpc-aiohttp | wsrpc-asgi | |
|---|---|---|
| Servidor | manejador web de aiohttp | ruta ASGI, middleware o aplicación independiente |
| Instalación | pip install wsrpc-aiohttp | pip install 'wsrpc-asgi[fastapi,client]' |
| Cliente Python | sobre aiohttp | sobre websockets |
| Cliente JavaScript | incluido | el mismo cliente |
| Códec JSON | loads y dumps se sustituyen | igual |