As duas pontas chamam.
O WSRPC torna um único WebSocket simétrico. O navegador chama seus handlers, e um handler chama o navegador e espera a resposta.
Esta página
A conexão
Cada linha aqui foi desenhada por uma chamada do servidor nesta página. Esta página usa o cliente JavaScript que acompanha o pacote, e estes quadros são os mesmos que um servidor em Python troca.
A mesma chamada de qualquer ponta
Instale o pacote e escreva o handler. Cada aba constrói o mesmo chat que funciona acima.
pip install wsrpc-aiohttp
Uma classe de rota atende uma conexão. O handler desenha sua linha na página e depois responde com uma citação.
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]'
A mesma classe de rota, montada em um endpoint WebSocket do 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
Um nome de rota com ponto, e uma chamada tipada do handler de volta para a 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
O cliente registra sua rota antes de conectar, para que o servidor possa chamá-la.
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
A página registra a rota que o servidor chama e desenha cada linha que recebe.
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;
}
}
O script incluído adiciona um global WSRPC. Sem etapa de build e sem 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>
O que a conexão oferece
Chamadas nos dois sentidos
Um handler chama uma rota que o navegador registrou e espera o resultado no mesmo socket.
Rotas ou funções simples
Exponha uma função, ou uma classe de rota cuja instância vive o tempo da conexão que atende.
Muitas chamadas ao mesmo tempo
Cada chamada carrega o seu id, então os resultados voltam a quem chamou, em qualquer ordem.
Erros remotos sobem localmente
Uma falha do outro lado chega com tipo e mensagem, e sobe onde você fez a chamada.
Sinais do ciclo de vida
Acompanhe conexão, desconexão e cada chamada sem envolver nenhum handler.
Código bloqueante não prende o loop
Handlers assíncronos rodam no loop de eventos. Handlers síncronos rodam em um pool de threads.
Como uma chamada trafega
Cada quadro é um objeto JSON. A chamada carrega id, método e parâmetros; a resposta volta com o mesmo id. As duas pontas enviam chamadas, então um handler pode perguntar ao navegador enquanto atende uma.
Onde funciona
Cada framework aqui tem um exemplo executável e um teste no seu repositório.
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
Qual pacote de Python?
Duas implementações, um protocolo. Um par em Python, um par no navegador e um servidor combinam livremente.
wsrpc-aiohttp | wsrpc-asgi | |
|---|---|---|
| Servidor | handler web do aiohttp | rota ASGI, middleware ou aplicação independente |
| Instalação | pip install wsrpc-aiohttp | pip install 'wsrpc-asgi[fastapi,client]' |
| Cliente Python | sobre aiohttp | sobre websockets |
| Cliente JavaScript | incluído | o mesmo cliente |
| Codec JSON | loads e dumps são substituíveis | igual |