chore: 更新依赖项版本以保持兼容性
This commit is contained in:
@@ -27,17 +27,17 @@
|
|||||||
"@kevisual/dts": "^0.0.4",
|
"@kevisual/dts": "^0.0.4",
|
||||||
"@kevisual/js-filter": "^0.0.5",
|
"@kevisual/js-filter": "^0.0.5",
|
||||||
"@kevisual/local-proxy": "^0.0.8",
|
"@kevisual/local-proxy": "^0.0.8",
|
||||||
"@kevisual/query": "^0.0.49",
|
"@kevisual/query": "^0.0.52",
|
||||||
"@kevisual/use-config": "^1.0.30",
|
"@kevisual/use-config": "^1.0.30",
|
||||||
"@opencode-ai/plugin": "^1.2.10",
|
"@opencode-ai/plugin": "^1.2.15",
|
||||||
"@types/bun": "^1.3.9",
|
"@types/bun": "^1.3.9",
|
||||||
"@types/node": "^25.3.0",
|
"@types/node": "^25.3.2",
|
||||||
"@types/send": "^1.2.1",
|
"@types/send": "^1.2.1",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"eventemitter3": "^5.0.4",
|
"eventemitter3": "^5.0.4",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"hono": "^4.12.2",
|
"hono": "^4.12.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"path-to-regexp": "^8.3.0",
|
"path-to-regexp": "^8.3.0",
|
||||||
"send": "^1.2.1",
|
"send": "^1.2.1",
|
||||||
|
|||||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -28,20 +28,20 @@ importers:
|
|||||||
specifier: ^0.0.8
|
specifier: ^0.0.8
|
||||||
version: 0.0.8
|
version: 0.0.8
|
||||||
'@kevisual/query':
|
'@kevisual/query':
|
||||||
specifier: ^0.0.49
|
specifier: ^0.0.52
|
||||||
version: 0.0.49
|
version: 0.0.52
|
||||||
'@kevisual/use-config':
|
'@kevisual/use-config':
|
||||||
specifier: ^1.0.30
|
specifier: ^1.0.30
|
||||||
version: 1.0.30(dotenv@17.2.3)
|
version: 1.0.30(dotenv@17.2.3)
|
||||||
'@opencode-ai/plugin':
|
'@opencode-ai/plugin':
|
||||||
specifier: ^1.2.10
|
specifier: ^1.2.15
|
||||||
version: 1.2.10
|
version: 1.2.15
|
||||||
'@types/bun':
|
'@types/bun':
|
||||||
specifier: ^1.3.9
|
specifier: ^1.3.9
|
||||||
version: 1.3.9
|
version: 1.3.9
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.3.0
|
specifier: ^25.3.2
|
||||||
version: 25.3.0
|
version: 25.3.2
|
||||||
'@types/send':
|
'@types/send':
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
@@ -58,8 +58,8 @@ importers:
|
|||||||
specifier: ^3.3.3
|
specifier: ^3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
hono:
|
hono:
|
||||||
specifier: ^4.12.0
|
specifier: ^4.12.3
|
||||||
version: 4.12.0
|
version: 4.12.3
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.1.6
|
specifier: ^5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
@@ -142,8 +142,8 @@ packages:
|
|||||||
'@kevisual/local-proxy@0.0.8':
|
'@kevisual/local-proxy@0.0.8':
|
||||||
resolution: {integrity: sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q==}
|
resolution: {integrity: sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q==}
|
||||||
|
|
||||||
'@kevisual/query@0.0.49':
|
'@kevisual/query@0.0.52':
|
||||||
resolution: {integrity: sha512-GrWW+QlBO5lkiqvb7PjOstNtpTQVSR74EHHWjm7YoL9UdT1wuPQXGUApZHmMBSh3NIWCf0AL2G1hPWZMC7YeOQ==}
|
resolution: {integrity: sha512-m1UbyDTIxtfAQXM+EqhXA4ytE2V8rV8mXTZVBwzfW9O6+gtvAcRY7K1YYxfewTSXLVh9nwvfHe0KQ8MDL5ukyw==}
|
||||||
|
|
||||||
'@kevisual/use-config@1.0.30':
|
'@kevisual/use-config@1.0.30':
|
||||||
resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==}
|
resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==}
|
||||||
@@ -166,11 +166,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
'@opencode-ai/plugin@1.2.10':
|
'@opencode-ai/plugin@1.2.15':
|
||||||
resolution: {integrity: sha512-Z1BMqNHnD8AGAEb+kUz0b2SOuiODwdQLdCA4aVGTXqkGzhiD44OVxr85MeoJ5AMTnnea9SnJ3jp9GAQ5riXA5g==}
|
resolution: {integrity: sha512-mh9S05W+CZZmo6q3uIEBubS66QVgiev7fRafX7vemrCfz+3pEIkSwipLjU/sxIewC9yLiDWLqS73DH/iEQzVDw==}
|
||||||
|
|
||||||
'@opencode-ai/sdk@1.2.10':
|
'@opencode-ai/sdk@1.2.15':
|
||||||
resolution: {integrity: sha512-SyXcVqry2hitPVvQtvXOhqsWyFhSycG/+LTLYXrcq8AFmd9FR7dyBSDB3f5Ol6IPkYOegk8P2Eg2kKPNSNiKGw==}
|
resolution: {integrity: sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ==}
|
||||||
|
|
||||||
'@rollup/plugin-commonjs@29.0.0':
|
'@rollup/plugin-commonjs@29.0.0':
|
||||||
resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==}
|
resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==}
|
||||||
@@ -371,8 +371,8 @@ packages:
|
|||||||
'@types/node@25.2.3':
|
'@types/node@25.2.3':
|
||||||
resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
|
resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==}
|
||||||
|
|
||||||
'@types/node@25.3.0':
|
'@types/node@25.3.2':
|
||||||
resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==}
|
resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==}
|
||||||
|
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
@@ -503,8 +503,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
hono@4.12.0:
|
hono@4.12.3:
|
||||||
resolution: {integrity: sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==}
|
resolution: {integrity: sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==}
|
||||||
engines: {node: '>=16.9.0'}
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
@@ -744,7 +744,7 @@ snapshots:
|
|||||||
|
|
||||||
'@kevisual/local-proxy@0.0.8': {}
|
'@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)':
|
'@kevisual/use-config@1.0.30(dotenv@17.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -765,12 +765,12 @@ snapshots:
|
|||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.20.1
|
fastq: 1.20.1
|
||||||
|
|
||||||
'@opencode-ai/plugin@1.2.10':
|
'@opencode-ai/plugin@1.2.15':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opencode-ai/sdk': 1.2.10
|
'@opencode-ai/sdk': 1.2.15
|
||||||
zod: 4.1.8
|
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)':
|
'@rollup/plugin-commonjs@29.0.0(rollup@4.57.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -904,7 +904,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@types/node@25.3.0':
|
'@types/node@25.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.18.2
|
undici-types: 7.18.2
|
||||||
|
|
||||||
@@ -912,15 +912,15 @@ snapshots:
|
|||||||
|
|
||||||
'@types/send@1.2.1':
|
'@types/send@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.3.0
|
'@types/node': 25.3.2
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.3.0
|
'@types/node': 25.3.2
|
||||||
|
|
||||||
'@types/xml2js@0.4.14':
|
'@types/xml2js@0.4.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.3.0
|
'@types/node': 25.3.2
|
||||||
|
|
||||||
acorn-walk@8.3.4:
|
acorn-walk@8.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -936,7 +936,7 @@ snapshots:
|
|||||||
|
|
||||||
bun-types@1.3.9:
|
bun-types@1.3.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.3.0
|
'@types/node': 25.3.2
|
||||||
|
|
||||||
commondir@1.0.1: {}
|
commondir@1.0.1: {}
|
||||||
|
|
||||||
@@ -1005,7 +1005,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
hono@4.12.0: {}
|
hono@4.12.3: {}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -3,419 +3,3 @@
|
|||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
WebSocket 服务器支持实时双向通信,可与 HTTP 服务器共享同一端口。所有 WebSocket 连接统一入口为 `/api/router`,通过 `type` 参数区分业务类型。
|
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