Les deux extrémités appellent.
WSRPC rend un seul WebSocket symétrique. Le navigateur appelle vos gestionnaires, et un gestionnaire appelle le navigateur puis attend la réponse.
Cette page
La connexion
Chaque ligne ici a été écrite par un appel du serveur dans cette page. Cette page utilise le client JavaScript livré avec le paquet, et ces trames sont celles qu'échange un serveur Python.
Le même appel depuis chaque côté
Installez le paquet, puis écrivez le gestionnaire. Chaque onglet construit le même chat que celui ci-dessus.
pip install wsrpc-aiohttp
Une classe de route sert une connexion. Le gestionnaire écrit votre ligne dans la page, puis répond par une citation.
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 même classe de route, montée sur un point 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 nom de route pointé, et un appel typé du gestionnaire vers la page.
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
Le client enregistre sa route avant de se connecter, pour que le serveur puisse l'appeler.
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 page enregistre la route que le serveur appelle, et écrit chaque ligne reçue.
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;
}
}
Le script fourni ajoute un global WSRPC. Sans étape de build 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>
Ce que la connexion apporte
Des appels dans les deux sens
Un gestionnaire appelle une route enregistrée par le navigateur et attend son résultat sur le même socket.
Routes ou fonctions simples
Exposez une fonction, ou une classe de route dont l'instance vit aussi longtemps que la connexion.
Plusieurs appels à la fois
Chaque appel porte son propre id, donc les résultats reviennent à leur appelant, dans n'importe quel ordre.
Les erreurs distantes remontent en local
Un échec de l'autre côté arrive avec son type et son message, et remonte là où vous avez appelé.
Des signaux de cycle de vie
Observez la connexion, la déconnexion et chaque appel sans envelopper un seul gestionnaire.
Le code bloquant ne tient pas la boucle
Les gestionnaires asynchrones tournent sur la boucle d'événements. Les synchrones, dans un pool de threads.
Le trajet d'un appel
Chaque trame est un objet JSON. L'appel porte un id, une méthode et des paramètres ; la réponse revient avec le même id. Les deux pairs envoient des appels, donc un gestionnaire peut interroger le navigateur pendant qu'il en traite un.
Où cela tourne
Chaque framework ici a un exemple exécutable et un test dans son dépôt.
Serveur aiohttp
wsrpc-aiohttp
- aiohttp
Serveurs ASGI
wsrpc-asgi
- FastAPI
- Starlette
- Django
- Channels
- Litestar
- Quart
- Sanic
- Falcon
- BlackSheep
- ASGI
Serveurs Node.js
@wsrpc/server
- node:http
- Express
- Fastify
Serveurs Go
github.com/wsrpc/wsrpc-go
- net/http
Clients
@wsrpc/client
- Browsers
- Node.js
- Cloudflare Workers
Quel paquet Python ?
Deux implémentations, un protocole. Un pair Python, un pair navigateur et un serveur se combinent librement.
wsrpc-aiohttp | wsrpc-asgi | |
|---|---|---|
| Serveur | gestionnaire web aiohttp | route ASGI, middleware ou application autonome |
| Installation | pip install wsrpc-aiohttp | pip install 'wsrpc-asgi[fastapi,client]' |
| Client Python | basé sur aiohttp | basé sur websockets |
| Client JavaScript | inclus | le même client |
| Codec JSON | loads et dumps remplaçables | identique |