From 46499bedc7578112d9d741d65f57a1f62323a90d Mon Sep 17 00:00:00 2001 From: abearxiong Date: Sun, 1 Mar 2026 01:10:16 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E9=A1=B9=E7=89=88=E6=9C=AC=E4=BB=A5=E4=BF=9D=E6=8C=81=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +- pnpm-lock.yaml | 56 ++--- system_design/ws-server.md | 416 ------------------------------------- 3 files changed, 32 insertions(+), 448 deletions(-) diff --git a/package.json b/package.json index 9a9bf1d..a687c12 100644 --- a/package.json +++ b/package.json @@ -27,17 +27,17 @@ "@kevisual/dts": "^0.0.4", "@kevisual/js-filter": "^0.0.5", "@kevisual/local-proxy": "^0.0.8", - "@kevisual/query": "^0.0.49", + "@kevisual/query": "^0.0.52", "@kevisual/use-config": "^1.0.30", - "@opencode-ai/plugin": "^1.2.10", + "@opencode-ai/plugin": "^1.2.15", "@types/bun": "^1.3.9", - "@types/node": "^25.3.0", + "@types/node": "^25.3.2", "@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.12.2", + "hono": "^4.12.3", "nanoid": "^5.1.6", "path-to-regexp": "^8.3.0", "send": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9716ac8..222d5e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,20 +28,20 @@ importers: specifier: ^0.0.8 version: 0.0.8 '@kevisual/query': - specifier: ^0.0.49 - version: 0.0.49 + specifier: ^0.0.52 + version: 0.0.52 '@kevisual/use-config': specifier: ^1.0.30 version: 1.0.30(dotenv@17.2.3) '@opencode-ai/plugin': - specifier: ^1.2.10 - version: 1.2.10 + specifier: ^1.2.15 + version: 1.2.15 '@types/bun': specifier: ^1.3.9 version: 1.3.9 '@types/node': - specifier: ^25.3.0 - version: 25.3.0 + specifier: ^25.3.2 + version: 25.3.2 '@types/send': specifier: ^1.2.1 version: 1.2.1 @@ -58,8 +58,8 @@ importers: specifier: ^3.3.3 version: 3.3.3 hono: - specifier: ^4.12.0 - version: 4.12.0 + specifier: ^4.12.3 + version: 4.12.3 nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -142,8 +142,8 @@ packages: '@kevisual/local-proxy@0.0.8': resolution: {integrity: sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q==} - '@kevisual/query@0.0.49': - resolution: {integrity: sha512-GrWW+QlBO5lkiqvb7PjOstNtpTQVSR74EHHWjm7YoL9UdT1wuPQXGUApZHmMBSh3NIWCf0AL2G1hPWZMC7YeOQ==} + '@kevisual/query@0.0.52': + resolution: {integrity: sha512-m1UbyDTIxtfAQXM+EqhXA4ytE2V8rV8mXTZVBwzfW9O6+gtvAcRY7K1YYxfewTSXLVh9nwvfHe0KQ8MDL5ukyw==} '@kevisual/use-config@1.0.30': resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==} @@ -166,11 +166,11 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@opencode-ai/plugin@1.2.10': - resolution: {integrity: sha512-Z1BMqNHnD8AGAEb+kUz0b2SOuiODwdQLdCA4aVGTXqkGzhiD44OVxr85MeoJ5AMTnnea9SnJ3jp9GAQ5riXA5g==} + '@opencode-ai/plugin@1.2.15': + resolution: {integrity: sha512-mh9S05W+CZZmo6q3uIEBubS66QVgiev7fRafX7vemrCfz+3pEIkSwipLjU/sxIewC9yLiDWLqS73DH/iEQzVDw==} - '@opencode-ai/sdk@1.2.10': - resolution: {integrity: sha512-SyXcVqry2hitPVvQtvXOhqsWyFhSycG/+LTLYXrcq8AFmd9FR7dyBSDB3f5Ol6IPkYOegk8P2Eg2kKPNSNiKGw==} + '@opencode-ai/sdk@1.2.15': + resolution: {integrity: sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ==} '@rollup/plugin-commonjs@29.0.0': resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==} @@ -371,8 +371,8 @@ packages: '@types/node@25.2.3': resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} - '@types/node@25.3.0': - resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + '@types/node@25.3.2': + resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -503,8 +503,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hono@4.12.0: - resolution: {integrity: sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==} + hono@4.12.3: + resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==} engines: {node: '>=16.9.0'} http-errors@2.0.1: @@ -744,7 +744,7 @@ snapshots: '@kevisual/local-proxy@0.0.8': {} - '@kevisual/query@0.0.49': {} + '@kevisual/query@0.0.52': {} '@kevisual/use-config@1.0.30(dotenv@17.2.3)': dependencies: @@ -765,12 +765,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@opencode-ai/plugin@1.2.10': + '@opencode-ai/plugin@1.2.15': dependencies: - '@opencode-ai/sdk': 1.2.10 + '@opencode-ai/sdk': 1.2.15 zod: 4.1.8 - '@opencode-ai/sdk@1.2.10': {} + '@opencode-ai/sdk@1.2.15': {} '@rollup/plugin-commonjs@29.0.0(rollup@4.57.1)': dependencies: @@ -904,7 +904,7 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/node@25.3.0': + '@types/node@25.3.2': dependencies: undici-types: 7.18.2 @@ -912,15 +912,15 @@ snapshots: '@types/send@1.2.1': dependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.2 '@types/ws@8.18.1': dependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.2 '@types/xml2js@0.4.14': dependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.2 acorn-walk@8.3.4: dependencies: @@ -936,7 +936,7 @@ snapshots: bun-types@1.3.9: dependencies: - '@types/node': 25.3.0 + '@types/node': 25.3.2 commondir@1.0.1: {} @@ -1005,7 +1005,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.12.0: {} + hono@4.12.3: {} http-errors@2.0.1: dependencies: diff --git a/system_design/ws-server.md b/system_design/ws-server.md index 6b631f0..e846202 100644 --- a/system_design/ws-server.md +++ b/system_design/ws-server.md @@ -3,419 +3,3 @@ ## 概述 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, - pub path: Option, - pub key: Option, - pub payload: Option, - pub data: Option, - pub code: Option, - pub message: Option, -} - -// Listener 配置 -pub struct Listener { - pub path: String, - pub io: bool, - pub func: Option Result<(), Box> + Send>>, - pub json: bool, -} - -// WsServer -pub struct WsServer { - pub router: QueryRouter, - pub path: String, - pub listeners: Vec, -} - -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, -) -> 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"} -```