连接
这里的每一行,都是服务端调用本页面画出来的。 本页运行的是随包发布的 JavaScript 客户端,这些帧与 Python 服务端交换的帧完全相同。
同一个调用,任意一端
安装包,然后编写处理函数。每个标签页构建的都是上面那个聊天。
pip install wsrpc-aiohttp
一个路由类服务一条连接。处理函数先在页面画出你的那行,再用一句台词回答。
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]'
同一个路由类,挂在 FastAPI 的 WebSocket 端点上。
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
带点号的路由名,以及处理函数回调页面的类型化调用。
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
客户端在连接之前注册路由,这样服务端才能调用它。
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
页面注册服务端要调用的路由,并画出收到的每一行。
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;
}
}
随包脚本提供全局 WSRPC。无需构建步骤,也不需要打包器。
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>
这条连接提供什么
双向调用
处理函数调用浏览器注册的路由,并在同一个 socket 上等待结果。
路由类或普通函数
可以公开一个函数,也可以公开路由类,其实例与连接同寿。
并发调用
每个调用带有自己的 id,结果按任意顺序返回给各自的调用方。
远端错误在本地抛出
对端的失败带着类型和消息返回,并在你发起调用的位置抛出。
生命周期信号
观察连接、断开和每一次调用,无需包装任何处理函数。
阻塞代码不占用事件循环
异步处理函数运行在事件循环上,同步处理函数运行在线程池中。
一次调用如何传输
每个帧都是一个 JSON 对象。调用带有 id、方法名和参数,应答以相同的 id 返回。两端都可以发起调用,因此处理函数在服务一次调用时可以反问浏览器。
它能运行在哪里
这里的每个框架都有可运行的示例和仓库中的测试。
aiohttp 服务端
wsrpc-aiohttp
- aiohttp
ASGI 服务端
wsrpc-asgi
- FastAPI
- Starlette
- Django
- Channels
- Litestar
- Quart
- Sanic
- Falcon
- BlackSheep
- ASGI
Node.js 服务端
@wsrpc/server
- node:http
- Express
- Fastify
Go 服务端
github.com/wsrpc/wsrpc-go
- net/http
客户端
@wsrpc/client
- Browsers
- Node.js
- Cloudflare Workers
选择哪个 Python 包
两种实现,一个协议。Python 对端、浏览器对端和服务端可以任意组合。
wsrpc-aiohttp | wsrpc-asgi | |
|---|---|---|
| 服务端 | aiohttp 处理函数 | ASGI 路由、中间件或独立应用 |
| 安装 | pip install wsrpc-aiohttp | pip install 'wsrpc-asgi[fastapi,client]' |
| Python 客户端 | 基于 aiohttp | 基于 websockets |
| JavaScript 客户端 | 已包含 | 同一个客户端 |
| JSON 编解码 | loads 与 dumps 可替换 | 相同 |