WSRPC

양쪽 모두 호출합니다.

WSRPC는 하나의 WebSocket을 대칭으로 만듭니다. 브라우저가 핸들러를 호출하고, 핸들러가 브라우저를 호출해 응답을 기다립니다.

브라우저 서버 연결 중

이 페이지

    연결

      여기의 모든 줄은 서버가 이 페이지를 호출해 그린 것입니다. 이 페이지는 패키지에 포함된 JavaScript 클라이언트로 동작합니다. 이 프레임들은 Python 서버가 주고받는 것과 같습니다.

      어느 쪽에서든 같은 호출

      패키지를 설치하고 핸들러를 작성하세요. 모든 탭이 위에서 동작하는 것과 같은 채팅을 만듭니다.

      pip install wsrpc-aiohttp

      라우트 클래스는 연결 하나를 담당합니다. 핸들러가 당신의 줄을 페이지에 그린 뒤 인용구로 답합니다.

      server.py
      import 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.py
      import 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.go
      package 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.py
      import 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.ts
      import 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>
      

      이 연결이 제공하는 것

      양방향 호출

      핸들러는 브라우저가 등록한 라우트를 호출하고 같은 소켓에서 결과를 기다립니다.

      라우트 클래스 또는 일반 함수

      함수를 공개하거나, 연결과 수명을 같이하는 인스턴스를 가진 라우트 클래스를 공개합니다.

      동시에 여러 호출

      호출마다 고유한 id를 가지므로 결과는 순서와 무관하게 각 호출자에게 돌아갑니다.

      원격 오류가 로컬에서 발생

      상대편의 실패는 타입과 메시지를 함께 전달하고, 호출한 위치에서 예외가 됩니다.

      수명 주기 시그널

      핸들러를 감싸지 않고도 연결, 종료, 각 호출을 관찰합니다.

      블로킹 코드가 루프를 막지 않음

      비동기 핸들러는 이벤트 루프에서, 동기 핸들러는 스레드 풀에서 실행됩니다.

      호출이 전달되는 방식

      모든 프레임은 하나의 JSON 객체입니다. 호출은 id, 메서드, 파라미터를 담고 응답은 같은 id로 돌아옵니다. 양쪽 모두 호출을 보내므로 핸들러는 호출을 처리하는 중에 브라우저에 물어볼 수 있습니다.

      브라우저 서버 {"id": 3, "method": "chat.send", "params": {"text": "hi"}} {"id": 3, "result": {"delivered": true}}
      브라우저가 서버를 호출합니다.
      브라우저 서버 핸들러 {"id": 5, "method": "chat.send", "params": {"text": "hi"}} {"id": 2, "method": "chat.append", "params": {"text": "hi"}} {"id": 2, "result": {"seen": true}} {"id": 5, "result": {"delivered": true}}
      서버가 처리 중인 호출 안에서 브라우저를 호출합니다.

      어디에서 동작하나

      여기의 각 프레임워크에는 실행 가능한 예제와 저장소의 테스트가 있습니다.

      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-aiohttpwsrpc-asgi
      서버 aiohttp 웹 핸들러 ASGI 라우트, 미들웨어 또는 단독 앱
      설치 pip install wsrpc-aiohttp pip install 'wsrpc-asgi[fastapi,client]'
      Python 클라이언트 aiohttp 기반 websockets 기반
      JavaScript 클라이언트 포함 같은 클라이언트
      JSON 코덱 loads와 dumps를 교체 가능 동일