Add design documents for HTTP server, router, and WebSocket server
- Created `https-server.md` detailing the HTTP server design, including request normalization, routing, and built-in routes. - Added `router.md` outlining the router system design, core components, and execution flow. - Introduced `ws-server.md` for the WebSocket server design, covering connection handling, message protocols, and custom listener registration.
This commit is contained in:
34
bun.lock
34
bun.lock
@@ -9,21 +9,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/code-builder": "^0.0.6",
|
||||
"@kevisual/context": "^0.0.6",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/dts": "^0.0.4",
|
||||
"@kevisual/js-filter": "^0.0.5",
|
||||
"@kevisual/local-proxy": "^0.0.8",
|
||||
"@kevisual/query": "^0.0.47",
|
||||
"@kevisual/query": "^0.0.49",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@opencode-ai/plugin": "^1.2.6",
|
||||
"@opencode-ai/plugin": "^1.2.10",
|
||||
"@types/bun": "^1.3.9",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"fast-glob": "^3.3.3",
|
||||
"hono": "^4.11.9",
|
||||
"hono": "^4.12.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"path-to-regexp": "^8.3.0",
|
||||
"send": "^1.2.1",
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
"@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="],
|
||||
|
||||
"@kevisual/context": ["@kevisual/context@0.0.6", "", {}, "sha512-w7HBOuO3JH37n6xT6W3FD7ykqHTwtyxOQzTzfEcKDCbsvGB1wVreSxFm2bvoFnnFLuxT/5QMpKlnPrwvmcTGnw=="],
|
||||
"@kevisual/context": ["@kevisual/context@0.0.8", "", {}, "sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA=="],
|
||||
|
||||
"@kevisual/dts": ["@kevisual/dts@0.0.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.57.1", "rollup-plugin-dts": "^6.3.0", "tslib": "^2.8.1" }, "bin": { "dts": "bin/dts.mjs" } }, "sha512-FVUaH/0nyhbHWpEVjFTGP54PLMm4Hf06aqWLdHOYHNPIgr1aK1C26kOH7iumklGFGk9w93IGxj8Zxe5fap5N2A=="],
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
"@kevisual/local-proxy": ["@kevisual/local-proxy@0.0.8", "", {}, "sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q=="],
|
||||
|
||||
"@kevisual/query": ["@kevisual/query@0.0.47", "", {}, "sha512-ZR7WXeDDGUSzBtcGVU3J173sA0hCqrGTw5ybGbdNGlM0VyJV/XQIovCcSoZh1YpnciLRRqJvzXUgTnCkam+M3g=="],
|
||||
"@kevisual/query": ["@kevisual/query@0.0.49", "", {}, "sha512-GrWW+QlBO5lkiqvb7PjOstNtpTQVSR74EHHWjm7YoL9UdT1wuPQXGUApZHmMBSh3NIWCf0AL2G1hPWZMC7YeOQ=="],
|
||||
|
||||
"@kevisual/use-config": ["@kevisual/use-config@1.0.30", "", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="],
|
||||
|
||||
@@ -63,9 +63,9 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.6", "", { "dependencies": { "@opencode-ai/sdk": "1.2.6", "zod": "4.1.8" } }, "sha512-CJEp3k17yWsjyfivm3zQof8L42pdze3a7iTqMOyesHgJplSuLiBYAMndbBYMDuJkyAh0dHYjw8v10vVw7Kfl4Q=="],
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.15", "", { "dependencies": { "@opencode-ai/sdk": "1.2.15", "zod": "4.1.8" } }, "sha512-mh9S05W+CZZmo6q3uIEBubS66QVgiev7fRafX7vemrCfz+3pEIkSwipLjU/sxIewC9yLiDWLqS73DH/iEQzVDw=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.6", "", {}, "sha512-dWMF8Aku4h7fh8sw5tQ2FtbqRLbIFT8FcsukpxTird49ax7oUXP+gzqxM/VdxHjfksQvzLBjLZyMdDStc5g7xA=="],
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.15", "", {}, "sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ=="],
|
||||
|
||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" } }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="],
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, ""],
|
||||
|
||||
"@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
||||
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, ""],
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
|
||||
|
||||
"hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
|
||||
"hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, ""],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"ws": ["@kevisual/ws@8.0.0", "", {}, "sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg=="],
|
||||
|
||||
@@ -279,6 +279,16 @@
|
||||
|
||||
"@types/xml2js/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, ""],
|
||||
|
||||
"@types/send/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
|
||||
|
||||
"@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
|
||||
|
||||
"@types/xml2js/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, ""],
|
||||
}
|
||||
}
|
||||
|
||||
452
system_design/https-server.md
Normal file
452
system_design/https-server.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# HTTP Server 设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
HTTP 服务器层负责接收外部 HTTP 请求,将其归一化后传递给 Router 层处理。所有请求统一入口为 `/api/router`。
|
||||
|
||||
## 请求入口
|
||||
|
||||
```
|
||||
POST /api/router?path=demo&key=01
|
||||
GET /api/router?path=demo&key=01
|
||||
```
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| path | /api/router | 请求入口路径 |
|
||||
|
||||
## 请求参数归一化
|
||||
|
||||
HTTP 请求的参数来源于两个部分,最终合并为一个 Message 对象:
|
||||
|
||||
### 1. URL Query (searchParams)
|
||||
|
||||
```typescript
|
||||
// GET /api/router?path=demo&key=01&token=xxx
|
||||
{
|
||||
path: "demo",
|
||||
key: "01",
|
||||
token: "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. POST Body
|
||||
|
||||
```typescript
|
||||
// POST /api/router
|
||||
// Body: { "name": "test", "value": 123 }
|
||||
{
|
||||
name: "test",
|
||||
value: 123
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 合并规则
|
||||
|
||||
最终的 Message = Query + Body(后者覆盖前者)
|
||||
|
||||
```typescript
|
||||
// GET /api/router?path=demo&key=01
|
||||
// POST Body: { "key": "02", "extra": "data" }
|
||||
{
|
||||
path: "demo",
|
||||
key: "02", // body 覆盖 query
|
||||
extra: "data"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. payload 参数
|
||||
|
||||
如果 query 或 body 中有 `payload` 字段且为 JSON 字符串,会自动解析为对象:
|
||||
|
||||
```typescript
|
||||
// GET /api/router?path=demo&key=01&payload={"id":123}
|
||||
{
|
||||
path: "demo",
|
||||
key: "01",
|
||||
payload: { id: 123 } // 自动解析
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 认证信息
|
||||
|
||||
| 来源 | 字段名 | 说明 |
|
||||
|------|--------|------|
|
||||
| Authorization header | token | Bearer token |
|
||||
| Cookie | token | cookie 中的 token |
|
||||
| Cookie | cookies | 完整 cookie 对象 |
|
||||
|
||||
## 路由匹配
|
||||
|
||||
### 方式一:path + key
|
||||
|
||||
```bash
|
||||
# 访问 path=demo, key=01 的路由
|
||||
POST /api/router?path=demo&key=01
|
||||
```
|
||||
|
||||
### 方式二:id
|
||||
|
||||
```bash
|
||||
# 直接通过路由 ID 访问
|
||||
POST /api/router?id=abc123
|
||||
```
|
||||
|
||||
## 内置路由
|
||||
|
||||
所有内置路由使用统一的访问方式:
|
||||
|
||||
| 路由 | 访问方式 | 说明 |
|
||||
|------|----------|------|
|
||||
| router.list | POST /api/router?path=router&key=list | 获取路由列表 |
|
||||
|
||||
### router.list
|
||||
|
||||
获取当前应用所有路由列表。
|
||||
|
||||
**请求:**
|
||||
|
||||
```bash
|
||||
POST /api/router?path=router&key=list
|
||||
```
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": "router$#$list",
|
||||
"path": "router",
|
||||
"key": "list",
|
||||
"description": "列出当前应用下的所有的路由信息",
|
||||
"middleware": [],
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"isUser": false
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
## Go 设计
|
||||
|
||||
```go
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Message HTTP 请求归一化后的消息
|
||||
type Message struct {
|
||||
Path string // 路由 path
|
||||
Key string // 路由 key
|
||||
ID string // 路由 ID (优先于 path+key)
|
||||
Token string // 认证 token
|
||||
Payload map[string]interface{} // payload 参数
|
||||
Query url.Values // 原始 query
|
||||
Cookies map[string]string // cookie
|
||||
// 其他参数
|
||||
Extra map[string]interface{}
|
||||
}
|
||||
|
||||
// HandleServer 解析 HTTP 请求
|
||||
func HandleServer(req *http.Request) (*Message, error) {
|
||||
method := req.Method
|
||||
if method != "GET" && method != "POST" {
|
||||
return nil, fmt.Errorf("method not allowed")
|
||||
}
|
||||
|
||||
// 解析 query
|
||||
query := req.URL.Query()
|
||||
|
||||
// 获取 token
|
||||
token := req.Header.Get("Authorization")
|
||||
if token != "" {
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
if token == "" {
|
||||
if cookie, err := req.Cookie("token"); err == nil {
|
||||
token = cookie.Value
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 body (POST)
|
||||
var body map[string]interface{}
|
||||
if method == "POST" {
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
body = make(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
// 合并 query 和 body
|
||||
msg := &Message{
|
||||
Token: token,
|
||||
Query: query,
|
||||
Cookies: parseCookies(req.Cookies()),
|
||||
}
|
||||
|
||||
// query 参数
|
||||
if v := query.Get("path"); v != "" {
|
||||
msg.Path = v
|
||||
}
|
||||
if v := query.Get("key"); v != "" {
|
||||
msg.Key = v
|
||||
}
|
||||
if v := query.Get("id"); v != "" {
|
||||
msg.ID = v
|
||||
}
|
||||
if v := query.Get("payload"); v != "" {
|
||||
if err := json.Unmarshal([]byte(v), &msg.Payload); err == nil {
|
||||
// payload 解析成功
|
||||
}
|
||||
}
|
||||
|
||||
// body 参数覆盖 query
|
||||
for k, v := range body {
|
||||
msg.Extra[k] = v
|
||||
switch k {
|
||||
case "path":
|
||||
msg.Path = v.(string)
|
||||
case "key":
|
||||
msg.Key = v.(string)
|
||||
case "id":
|
||||
msg.ID = v.(string)
|
||||
case "payload":
|
||||
msg.Payload = v.(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// RouterHandler 创建 HTTP 处理函数
|
||||
func RouterHandler(router *QueryRouter) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
// CORS
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := HandleServer(req)
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(Result{Code: 400, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用 router.run
|
||||
result, err := router.Run(*msg, nil)
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(Result{Code: 500, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Server HTTP 服务器
|
||||
type Server struct {
|
||||
Router *QueryRouter
|
||||
Path string
|
||||
Handlers []http.HandlerFunc
|
||||
}
|
||||
|
||||
func (s *Server) Listen(addr string) error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// 自定义处理器
|
||||
for _, h := range s.Handlers {
|
||||
mux.HandleFunc(s.Path, h)
|
||||
}
|
||||
|
||||
// 路由处理器
|
||||
mux.HandleFunc(s.Path, RouterHandler(s.Router))
|
||||
|
||||
return http.ListenAndServe(addr, mux)
|
||||
}
|
||||
```
|
||||
|
||||
## Rust 设计
|
||||
|
||||
```rust
|
||||
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// Message HTTP 请求归一化后的消息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
pub path: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub id: Option<String>,
|
||||
pub token: Option<String>,
|
||||
pub payload: Option<HashMap<String, Value>>,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
// Result 调用结果
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Result {
|
||||
pub code: i32,
|
||||
pub data: Option<Value>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
// 解析 HTTP 请求
|
||||
pub async fn handle_server(req: HttpRequest, body: web::Bytes) -> Result<Message, String> {
|
||||
let method = req.method().as_str();
|
||||
if method != "GET" && method != "POST" {
|
||||
return Err("method not allowed".to_string());
|
||||
}
|
||||
|
||||
// 获取 query 参数
|
||||
let query: web::Query<HashMap<String, String>> = web::Query::clone(&req);
|
||||
|
||||
// 获取 token
|
||||
let mut token = None;
|
||||
if let Some(auth) = req.headers().get("authorization") {
|
||||
if let Ok(s) = auth.to_str() {
|
||||
token = Some(s.trim_start_matches("Bearer ").to_string());
|
||||
}
|
||||
}
|
||||
if token.is_none() {
|
||||
if let Some(cookie) = req.cookie("token") {
|
||||
token = Some(cookie.value().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 body (POST)
|
||||
let mut body_map = HashMap::new();
|
||||
if method == "POST" {
|
||||
if let Ok(v) = serde_json::from_slice::<Value>(&body) {
|
||||
if let Some(obj) = v.as_object() {
|
||||
for (k, val) in obj {
|
||||
body_map.insert(k.clone(), val.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建 Message
|
||||
let mut msg = Message {
|
||||
path: query.get("path").cloned(),
|
||||
key: query.get("key").cloned(),
|
||||
id: query.get("id").cloned(),
|
||||
token,
|
||||
payload: None,
|
||||
extra: HashMap::new(),
|
||||
};
|
||||
|
||||
// 处理 payload
|
||||
if let Some(p) = query.get("payload") {
|
||||
if let Ok(v) = serde_json::from_str::<Value>(p) {
|
||||
msg.payload = v.as_object().map(|m| m.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// body 覆盖 query
|
||||
for (k, v) in body_map {
|
||||
match k.as_str() {
|
||||
"path" => msg.path = v.as_str().map(|s| s.to_string()),
|
||||
"key" => msg.key = v.as_str().map(|s| s.to_string()),
|
||||
"id" => msg.id = v.as_str().map(|s| s.to_string()),
|
||||
"payload" => msg.payload = v.as_object().map(|m| m.clone()),
|
||||
_ => msg.extra.insert(k, v),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
// HTTP 处理函数
|
||||
async fn router_handler(
|
||||
req: HttpRequest,
|
||||
body: web::Bytes,
|
||||
router: web::Data<QueryRouter>,
|
||||
) -> impl Responder {
|
||||
let msg = match handle_server(req, body).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return HttpResponse::BadRequest().json(Result {
|
||||
code: 400,
|
||||
data: None,
|
||||
message: Some(e),
|
||||
}),
|
||||
};
|
||||
|
||||
// 调用 router.run
|
||||
match router.run(msg, None).await {
|
||||
Ok(result) => HttpResponse::Ok().json(result),
|
||||
Err(e) => HttpResponse::InternalServerError().json(Result {
|
||||
code: 500,
|
||||
data: None,
|
||||
message: Some(e.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// Server HTTP 服务器
|
||||
pub struct Server {
|
||||
pub router: QueryRouter,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn listen(self, addr: &str) -> std::io::Result<()> {
|
||||
let router = web::Data::new(self.router);
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(router.clone())
|
||||
.route(&self.path, web::post().to(router_handler))
|
||||
.route(&self.path, web::get().to(router_handler))
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 请求示例
|
||||
|
||||
### 基础请求
|
||||
|
||||
```bash
|
||||
# 访问 demo/01 路由
|
||||
curl -X POST "http://localhost:4002/api/router?path=demo&key=01"
|
||||
|
||||
# 带 body
|
||||
curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"test","value":123}'
|
||||
```
|
||||
|
||||
### 获取路由列表
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:4002/api/router?path=router&key=list"
|
||||
```
|
||||
|
||||
### 带认证
|
||||
|
||||
```bash
|
||||
# 通过 Header
|
||||
curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \
|
||||
-H "Authorization: Bearer your-token"
|
||||
|
||||
# 通过 Cookie
|
||||
curl -X POST "http://localhost:4002/api/router?path=demo&key=01" \
|
||||
-b "token=your-token"
|
||||
```
|
||||
378
system_design/router.md
Normal file
378
system_design/router.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# Router 系统设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
轻量级路由框架,支持链式路由、中间件模式、统一上下文。适用于构建 API 服务,支持跨语言实现(Go、Rust 等)。
|
||||
|
||||
## 核心组件
|
||||
|
||||
### Route
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| path | string | 一级路径 |
|
||||
| key | string | 二级路径 |
|
||||
| id | string | 唯一标识 |
|
||||
| run | Handler | 业务处理函数 |
|
||||
| nextRoute | NextRoute? | 下一个路由 |
|
||||
| middleware | string[] | 中间件 ID 列表 |
|
||||
| metadata | T | 元数据/参数 schema |
|
||||
| type | string | 类型:route / middleware |
|
||||
| isDebug | bool | 是否开启调试 |
|
||||
|
||||
### NextRoute
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string? | 路由 ID |
|
||||
| path | string? | 一级路径 |
|
||||
| key | string? | 二级路径 |
|
||||
|
||||
### RouteContext
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| appId | string? | 应用 ID |
|
||||
| query | object | URL 参数和 payload 合并结果 |
|
||||
| args | object | 同 query |
|
||||
| body | any | 响应 body |
|
||||
| code | number | 响应状态码 |
|
||||
| message | string | 响应消息 |
|
||||
| state | object | 状态传递 |
|
||||
| currentId | string? | 当前路由 ID |
|
||||
| currentPath | string? | 当前路径 |
|
||||
| currentKey | string? | 当前 key |
|
||||
| currentRoute | Route? | 当前路由对象 |
|
||||
| progress | [string, string][] | 路由执行路径 |
|
||||
| nextQuery | object | 传递给下一个路由的参数 |
|
||||
| end | boolean | 是否提前结束 |
|
||||
| app | QueryRouter? | 路由实例引用 |
|
||||
| error | any | 错误信息 |
|
||||
| call | function | 调用其他路由(返回完整上下文) |
|
||||
| run | function | 调用其他路由(返回简化结果) |
|
||||
| throw | function | 抛出错误 |
|
||||
| needSerialize | boolean | 是否需要序列化 |
|
||||
|
||||
### QueryRouter
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| add(route, opts?) | 添加路由 |
|
||||
| remove(route) | 按 path/key 移除路由 |
|
||||
| removeById(id) | 按 ID 移除路由 |
|
||||
| runRoute(path, key, ctx) | 执行单个路由 |
|
||||
| parse(message, ctx) | 入口解析,返回完整上下文 |
|
||||
| call(message, ctx) | 调用路由,返回完整上下文 |
|
||||
| run(message, ctx) | 调用路由,返回简化结果 {code, data, message} |
|
||||
| getHandle() | 获取 HTTP 处理函数 |
|
||||
| setContext(ctx) | 设置默认上下文 |
|
||||
| getList(filter?) | 获取路由列表 |
|
||||
| hasRoute(path, key) | 检查路由是否存在 |
|
||||
| findRoute(opts) | 查找路由 |
|
||||
| exportRoutes() | 导出所有路由 |
|
||||
| importRoutes(routes) | 批量导入路由 |
|
||||
| createRouteList(opts) | 创建内置的路由列表功能 |
|
||||
|
||||
### QueryRouterServer
|
||||
|
||||
继承 QueryRouter,新增:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| appId | string | 应用 ID |
|
||||
| handle | function | HTTP 处理函数 |
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| setHandle(wrapperFn, ctx) | 设置处理函数 |
|
||||
| route(path, key?, opts?) | 工厂方法创建路由 |
|
||||
|
||||
## Go 设计
|
||||
|
||||
```go
|
||||
package router
|
||||
|
||||
// Route 路由单元
|
||||
type Route struct {
|
||||
Path string
|
||||
Key string
|
||||
ID string
|
||||
Run func(ctx *RouteContext) (*RouteContext, error)
|
||||
NextRoute *NextRoute
|
||||
Middleware []string
|
||||
Metadata map[string]interface{}
|
||||
Type string
|
||||
IsDebug bool
|
||||
}
|
||||
|
||||
// NextRoute 下一个路由
|
||||
type NextRoute struct {
|
||||
ID string
|
||||
Path string
|
||||
Key string
|
||||
}
|
||||
|
||||
// RouteContext 请求上下文
|
||||
type RouteContext struct {
|
||||
AppID string
|
||||
Query map[string]interface{}
|
||||
Args map[string]interface{}
|
||||
Body interface{}
|
||||
Code int
|
||||
Message string
|
||||
State map[string]interface{}
|
||||
CurrentID string
|
||||
CurrentPath string
|
||||
CurrentKey string
|
||||
CurrentRoute *Route
|
||||
Progress [][2]string
|
||||
NextQuery map[string]interface{}
|
||||
End bool
|
||||
App *QueryRouter
|
||||
Error error
|
||||
NeedSerialize bool
|
||||
// Methods
|
||||
Call func(msg interface{}, ctx *RouteContext) (*RouteContext, error)
|
||||
Run func(msg interface{}, ctx *RouteContext) (interface{}, error)
|
||||
Throw func(err interface{})
|
||||
}
|
||||
|
||||
// Message 调用消息
|
||||
type Message struct {
|
||||
ID string
|
||||
Path string
|
||||
Key string
|
||||
Payload map[string]interface{}
|
||||
}
|
||||
|
||||
// Result 调用结果
|
||||
type Result struct {
|
||||
Code int
|
||||
Data interface{}
|
||||
Message string
|
||||
}
|
||||
|
||||
// AddOpts 添加选项
|
||||
type AddOpts struct {
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
// QueryRouter 路由管理器
|
||||
type QueryRouter struct {
|
||||
Routes []*Route
|
||||
MaxNextRoute int
|
||||
Context *RouteContext
|
||||
}
|
||||
|
||||
func NewQueryRouter() *QueryRouter
|
||||
|
||||
func (r *QueryRouter) Add(route *Route, opts *AddOpts)
|
||||
func (r *QueryRouter) Remove(path, key string)
|
||||
func (r *QueryRouter) RemoveByID(id string)
|
||||
func (r *QueryRouter) RunRoute(path, key string, ctx *RouteContext) (*RouteContext, error)
|
||||
func (r *QueryRouter) Parse(msg Message, ctx *RouteContext) (*RouteContext, error)
|
||||
func (r *QueryRouter) Call(msg Message, ctx *RouteContext) (*RouteContext, error)
|
||||
func (r *QueryRouter) Run(msg Message, ctx *RouteContext) (Result, error)
|
||||
func (r *QueryRouter) GetHandle() func(msg interface{}) Result
|
||||
func (r *QueryRouter) SetContext(ctx *RouteContext)
|
||||
func (r *QueryRouter) GetList() []Route
|
||||
func (r *QueryRouter) HasRoute(path, key string) bool
|
||||
func (r *QueryRouter) FindRoute(opts FindOpts) *Route
|
||||
|
||||
// QueryRouterServer 服务端
|
||||
type QueryRouterServer struct {
|
||||
QueryRouter
|
||||
AppID string
|
||||
Handle func(msg interface{}) Result
|
||||
}
|
||||
|
||||
type ServerOpts struct {
|
||||
HandleFn func(msg interface{}, ctx interface{}) Result
|
||||
Context *RouteContext
|
||||
AppID string
|
||||
}
|
||||
|
||||
func NewQueryRouterServer(opts *ServerOpts) *QueryRouterServer
|
||||
|
||||
func (s *QueryRouterServer) SetHandle(wrapperFn func(msg interface{}, ctx interface{}) Result, ctx *RouteContext)
|
||||
func (s *QueryRouterServer) Route(path string, key ...string) *Route
|
||||
```
|
||||
|
||||
## Rust 设计
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
// Route 路由单元
|
||||
pub struct Route<M = Value> {
|
||||
pub path: String,
|
||||
pub key: String,
|
||||
pub id: String,
|
||||
pub run: Option<Box<dyn Fn(RouteContext) -> Pin<Box<dyn Future<Output = Result<RouteContext>>>> + Send>>,
|
||||
pub next_route: Option<NextRoute>,
|
||||
pub middleware: Vec<String>,
|
||||
pub metadata: M,
|
||||
pub route_type: String,
|
||||
pub is_debug: bool,
|
||||
}
|
||||
|
||||
// NextRoute 下一个路由
|
||||
#[derive(Clone)]
|
||||
pub struct NextRoute {
|
||||
pub id: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
// RouteContext 请求上下文
|
||||
#[derive(Clone)]
|
||||
pub struct RouteContext {
|
||||
pub app_id: Option<String>,
|
||||
pub query: HashMap<String, Value>,
|
||||
pub args: HashMap<String, Value>,
|
||||
pub body: Option<Value>,
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
pub state: HashMap<String, Value>,
|
||||
pub current_id: Option<String>,
|
||||
pub current_path: Option<String>,
|
||||
pub current_key: Option<String>,
|
||||
pub current_route: Option<Box<Route>>,
|
||||
pub progress: Vec<(String, String)>,
|
||||
pub next_query: HashMap<String, Value>,
|
||||
pub end: bool,
|
||||
pub app: Option<Box<QueryRouter>>,
|
||||
pub error: Option<Value>,
|
||||
pub need_serialize: bool,
|
||||
}
|
||||
|
||||
// Message 调用消息
|
||||
#[derive(Clone)]
|
||||
pub struct Message {
|
||||
pub id: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub payload: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
// Result 调用结果
|
||||
pub struct Result {
|
||||
pub code: i32,
|
||||
pub data: Option<Value>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
// AddOpts 添加选项
|
||||
pub struct AddOpts {
|
||||
pub overwrite: bool,
|
||||
}
|
||||
|
||||
// FindOpts 查找选项
|
||||
pub struct FindOpts {
|
||||
pub path: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
// QueryRouter 路由管理器
|
||||
pub struct QueryRouter {
|
||||
pub routes: Vec<Route>,
|
||||
pub max_next_route: usize,
|
||||
pub context: RouteContext,
|
||||
}
|
||||
|
||||
impl QueryRouter {
|
||||
pub fn new() -> Self
|
||||
pub fn add(&mut self, route: Route, opts: Option<AddOpts>)
|
||||
pub fn remove(&mut self, path: &str, key: &str)
|
||||
pub fn remove_by_id(&mut self, id: &str)
|
||||
pub async fn run_route(&self, path: &str, key: &str, ctx: RouteContext) -> Result<RouteContext>
|
||||
pub async fn parse(&self, msg: Message, ctx: Option<RouteContext>) -> Result<RouteContext>
|
||||
pub async fn call(&self, msg: Message, ctx: Option<RouteContext>) -> Result<RouteContext>
|
||||
pub async fn run(&self, msg: Message, ctx: Option<RouteContext>) -> Result<Result>
|
||||
pub fn get_handle(&self) -> impl Fn(Message) -> Result + '_
|
||||
pub fn set_context(&mut self, ctx: RouteContext)
|
||||
pub fn get_list(&self) -> Vec<Route>
|
||||
pub fn has_route(&self, path: &str, key: &str) -> bool
|
||||
pub fn find_route(&self, opts: FindOpts) -> Option<&Route>
|
||||
}
|
||||
|
||||
// ServerOpts 服务端选项
|
||||
pub struct ServerOpts {
|
||||
pub handle_fn: Option<Box<dyn Fn(Message, Option<RouteContext>) -> Result + Send>>,
|
||||
pub context: Option<RouteContext>,
|
||||
pub app_id: Option<String>,
|
||||
}
|
||||
|
||||
// QueryRouterServer 服务端
|
||||
pub struct QueryRouterServer {
|
||||
pub router: QueryRouter,
|
||||
pub app_id: String,
|
||||
pub handle: Option<Box<dyn Fn(Message) -> Result + Send>>,
|
||||
}
|
||||
|
||||
impl QueryRouterServer {
|
||||
pub fn new(opts: Option<ServerOpts>) -> Self
|
||||
pub fn set_handle(&mut self, wrapperFn: Box<dyn Fn(Message) -> Result + Send>)
|
||||
pub fn route(&self, path: &str, key: Option<&str>) -> Route
|
||||
}
|
||||
```
|
||||
|
||||
## 执行流程
|
||||
|
||||
```
|
||||
Message → parse() → runRoute() → [middleware] → run() → [nextRoute] → ...
|
||||
↓
|
||||
RouteContext (层层传递)
|
||||
```
|
||||
|
||||
1. `parse()` 接收消息,初始化上下文(query、args、state)
|
||||
2. `runRoute()` 查找路由,先执行 middleware,再执行 run
|
||||
3. middleware 执行出错立即返回错误
|
||||
4. 如有 nextRoute,递归执行下一个路由(最多 40 层)
|
||||
5. 返回最终 RouteContext
|
||||
|
||||
## 特性说明
|
||||
|
||||
- **双层路径**: path + key 构成唯一路由
|
||||
- **链式路由**: nextRoute 支持路由链式执行
|
||||
- **中间件**: 每个 Route 可挂载多个 middleware
|
||||
- **统一上下文**: RouteContext 贯穿整个请求生命周期
|
||||
|
||||
## 内置路由
|
||||
|
||||
框架内置以下路由,通过 HTTP 访问时使用 `path` 和 `key` 参数:
|
||||
|
||||
| 路由 path | 路由 key | 说明 |
|
||||
|-----------|----------|------|
|
||||
| router | list | 获取当前应用所有路由列表 |
|
||||
|
||||
### router/list
|
||||
|
||||
获取当前应用所有路由列表。
|
||||
|
||||
**访问方式:** `POST /api/router?path=router&key=list`
|
||||
|
||||
**响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": "router$#$list",
|
||||
"path": "router",
|
||||
"key": "list",
|
||||
"description": "列出当前应用下的所有的路由信息",
|
||||
"middleware": [],
|
||||
"metadata": {}
|
||||
}
|
||||
],
|
||||
"isUser": false
|
||||
},
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
421
system_design/ws-server.md
Normal file
421
system_design/ws-server.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# WebSocket Server 设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
WebSocket 服务器支持实时双向通信,可与 HTTP 服务器共享同一端口。所有 WebSocket 连接统一入口为 `/api/router`,通过 `type` 参数区分业务类型。
|
||||
|
||||
## 连接入口
|
||||
|
||||
```
|
||||
ws://host:port/api/router?token=xxx&id=client-id
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| token | 认证 token |
|
||||
| id | 客户端标识 |
|
||||
|
||||
## 消息协议
|
||||
|
||||
### 请求消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "router",
|
||||
"path": "demo",
|
||||
"key": "01",
|
||||
"payload": { "name": "test" }
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| type | string | 消息类型:router |
|
||||
| path | string | 路由 path |
|
||||
| key | string | 路由 key |
|
||||
| payload | object | 请求参数 |
|
||||
|
||||
### 响应消息格式
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "router",
|
||||
"code": 200,
|
||||
"data": { "result": "success" },
|
||||
"message": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 内置消息类型
|
||||
|
||||
| 消息 | 说明 |
|
||||
|------|------|
|
||||
| ping | 服务端返回 pong |
|
||||
| close | 关闭连接 |
|
||||
| connected | 连接成功通知 |
|
||||
|
||||
## 自定义 WebSocket 监听器
|
||||
|
||||
可以注册自定义的 WebSocket 监听器来处理特定路径的 WebSocket 连接。
|
||||
|
||||
### 注册监听器
|
||||
|
||||
```typescript
|
||||
app.on([
|
||||
{
|
||||
path: '/ws/chat',
|
||||
io: true,
|
||||
func: async (req, res) => {
|
||||
const { data, token, pathname, id, ws, emitter } = req;
|
||||
// 处理聊天消息
|
||||
res.end({ message: 'received' });
|
||||
}
|
||||
}
|
||||
]);
|
||||
```
|
||||
|
||||
### Listener 配置
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| path | string | WebSocket 路径 |
|
||||
| io | boolean | 是否为 WebSocket 监听器 |
|
||||
| func | function | 处理函数 |
|
||||
| json | boolean | 是否自动解析 JSON |
|
||||
|
||||
## 执行流程
|
||||
|
||||
```
|
||||
WebSocket 连接 → 查找 listener → 自定义处理 / 默认 router 处理 → 响应
|
||||
```
|
||||
|
||||
1. WebSocket 连接建立时,解析 URL 获取 pathname、token、id
|
||||
2. 查找是否有匹配的自定义 listener
|
||||
3. 如有自定义 listener,调用对应处理函数
|
||||
4. 如无自定义 listener,使用默认处理:
|
||||
- type = 'router' 时,调用 handle 处理路由
|
||||
- 支持 ping/pong/close 内置命令
|
||||
5. 响应消息通过 ws.send 返回
|
||||
|
||||
## Go 设计
|
||||
|
||||
```go
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketMessage WebSocket 消息
|
||||
type WebSocketMessage struct {
|
||||
Type string `json:"type"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Payload map[string]interface{} `json:"payload,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// WebSocketUpgrader WebSocket 升级配置
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// Listener WebSocket 监听器
|
||||
type Listener struct {
|
||||
Path string
|
||||
IO bool
|
||||
Func func(msg WebSocketMessage, ws *websocket.Conn) error
|
||||
JSON bool
|
||||
}
|
||||
|
||||
// WsServer WebSocket 服务器
|
||||
type WsServer struct {
|
||||
Router *QueryRouter
|
||||
Path string
|
||||
Listeners []Listener
|
||||
Listen func() error
|
||||
}
|
||||
|
||||
// HandleWebSocket 处理 WebSocket 连接
|
||||
func (s *WsServer) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// 解析 token 和 id
|
||||
token := r.URL.Query().Get("token")
|
||||
id := r.URL.Query().Get("id")
|
||||
|
||||
for {
|
||||
// 读取消息
|
||||
_, msgBytes, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var msg WebSocketMessage
|
||||
if err := json.Unmarshal(msgBytes, &msg); err != nil {
|
||||
ws.WriteJSON(WebSocketMessage{
|
||||
Code: 400,
|
||||
Message: "invalid message",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找自定义 listener
|
||||
pathname := r.URL.Path
|
||||
var handled bool
|
||||
for _, listener := range s.Listeners {
|
||||
if listener.IO && listener.Path == pathname {
|
||||
if err := listener.Func(msg, ws); err != nil {
|
||||
ws.WriteJSON(WebSocketMessage{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
handled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if handled {
|
||||
continue
|
||||
}
|
||||
|
||||
// 默认处理:router 类型
|
||||
if msg.Type == "router" {
|
||||
result, err := s.Router.Run(Message{
|
||||
Path: msg.Path,
|
||||
Key: msg.Key,
|
||||
Payload: msg.Payload,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
ws.WriteJSON(WebSocketMessage{
|
||||
Code: 500,
|
||||
Message: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
ws.WriteJSON(WebSocketMessage{
|
||||
Type: "router",
|
||||
Code: result.Code,
|
||||
Data: result.Data,
|
||||
Message: result.Message,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// 内置命令
|
||||
if string(msgBytes) == "ping" {
|
||||
ws.WriteMessage(websocket.TextMessage, []byte("pong"))
|
||||
continue
|
||||
}
|
||||
|
||||
if string(msgBytes) == "close" {
|
||||
break
|
||||
}
|
||||
|
||||
ws.WriteJSON(WebSocketMessage{
|
||||
Code: 400,
|
||||
Message: "unknown type",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务器
|
||||
func (s *WsServer) Start(addr string) error {
|
||||
http.HandleFunc(s.Path, s.HandleWebSocket)
|
||||
return http.ListenAndServe(addr, nil)
|
||||
}
|
||||
```
|
||||
|
||||
## Rust 设计
|
||||
|
||||
```rust
|
||||
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse, Responder};
|
||||
use actix_ws::{Message, Session, Aggregate};
|
||||
use std::sync::Arc;
|
||||
|
||||
// WebSocket 消息
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct WsMessage {
|
||||
pub r#type: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub key: Option<String>,
|
||||
pub payload: Option<serde_json::Value>,
|
||||
pub data: Option<serde_json::Value>,
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
// Listener 配置
|
||||
pub struct Listener {
|
||||
pub path: String,
|
||||
pub io: bool,
|
||||
pub func: Option<Box<dyn Fn(WsMessage, &mut Session) -> Result<(), Box<dyn std::error::Error>> + Send>>,
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
// WsServer
|
||||
pub struct WsServer {
|
||||
pub router: QueryRouter,
|
||||
pub path: String,
|
||||
pub listeners: Vec<Listener>,
|
||||
}
|
||||
|
||||
impl WsServer {
|
||||
pub async fn start(&self, addr: &str) -> std::io::Result<()> {
|
||||
let router = web::Data::new(self.router.clone());
|
||||
let path = self.path.clone();
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(router.clone())
|
||||
.service(
|
||||
web::resource(&path)
|
||||
.route(web::get().to(ws_handler))
|
||||
)
|
||||
})
|
||||
.bind(addr)?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn ws_handler(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
router: web::Data<QueryRouter>,
|
||||
) -> impl Responder {
|
||||
let (resp, session, msg) = actix_ws::handle(&req, stream)?;
|
||||
|
||||
let mut session = session;
|
||||
|
||||
// 处理消息
|
||||
while let Some(result) = msg.next().await {
|
||||
match result {
|
||||
Ok(Message::Text(text)) => {
|
||||
let ws_msg: WsMessage = match serde_json::from_str(&text) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
let _ = session.text(r#"{"code":400,"message":"invalid message"}"#).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// 调用 router
|
||||
if ws_msg.r#type.as_deref() == Some("router") {
|
||||
let msg = Message {
|
||||
path: ws_msg.path.clone(),
|
||||
key: ws_msg.key.clone(),
|
||||
payload: ws_msg.payload.clone(),
|
||||
};
|
||||
|
||||
match router.run(msg, None).await {
|
||||
Ok(result) => {
|
||||
let resp = WsMessage {
|
||||
r Some("router".to_string()),
|
||||
code: Some(result.code),
|
||||
data: result.data,
|
||||
message: result.message,
|
||||
};
|
||||
let _ = session.text(serde_json::to_string(&resp).unwrap()).await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = session.text(format!(r#"{{"code":500,"message":"{}"}}"#, e)).await;
|
||||
}
|
||||
}
|
||||
} else if text == "ping" {
|
||||
let _ = session.text("pong").await;
|
||||
} else if text == "close" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => break,
|
||||
Err(e) => {
|
||||
let _ = session.text(format!(r#"{{"code":500,"message":"{}"}}"#, e)).await;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
```
|
||||
|
||||
## 客户端示例
|
||||
|
||||
### TypeScript 客户端
|
||||
|
||||
```typescript
|
||||
import { ReconnectingWebSocket } from '@kevisual/router';
|
||||
|
||||
const ws = new ReconnectingWebSocket('ws://localhost:4002/api/router', {
|
||||
onOpen: () => {
|
||||
console.log('connected');
|
||||
},
|
||||
onMessage: (data) => {
|
||||
console.log('received:', data);
|
||||
}
|
||||
});
|
||||
|
||||
// 调用路由
|
||||
ws.send({
|
||||
type: 'router',
|
||||
path: 'demo',
|
||||
key: '01',
|
||||
payload: { name: 'test' }
|
||||
});
|
||||
```
|
||||
|
||||
### 原生 WebSocket
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket('ws://localhost:4002/api/router?token=xxx&id=client1');
|
||||
|
||||
ws.onopen = () => {
|
||||
// 调用路由
|
||||
ws.send(JSON.stringify({
|
||||
type: 'router',
|
||||
path: 'demo',
|
||||
key: '01',
|
||||
payload: { name: 'test' }
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('received:', data);
|
||||
};
|
||||
|
||||
// 发送 ping
|
||||
ws.send('ping');
|
||||
|
||||
// 关闭连接
|
||||
ws.send('close');
|
||||
```
|
||||
|
||||
## 连接示例
|
||||
|
||||
```bash
|
||||
# 基础连接
|
||||
wscat -c ws://localhost:4002/api/router
|
||||
|
||||
# 带 token 和 id
|
||||
wscat -c "ws://localhost:4002/api/router?token=xxx&id=client1"
|
||||
|
||||
# 发送路由请求
|
||||
{"type":"router","path":"demo","key":"01"}
|
||||
```
|
||||
Reference in New Issue
Block a user