initial commit
This commit is contained in:
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# .env.example
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=your_password_here
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
repomix-output.xml
|
||||||
|
.env
|
||||||
174
README.md
Normal file
174
README.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
好的 👍 我来帮你写一份专业的 **README.md**,既能让开发者快速上手,又能作为项目的正式说明文档。这里我会结合你的代码仓库结构和实现特点来写。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# README.md
|
||||||
|
|
||||||
|
# 🚀 WebSocket + Redis Pub/Sub Service
|
||||||
|
|
||||||
|
本项目是一个基于 **WebSocket** 与 **Redis** 的实时消息推送服务,支持多客户端订阅、消息分发与发布。
|
||||||
|
通过 `docker-compose` 可一键启动服务,方便部署与扩展。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── client/
|
||||||
|
│ └── test-client.js # WebSocket 客户端示例(测试用)
|
||||||
|
├── server/
|
||||||
|
│ ├── Dockerfile # 服务端 Docker 构建配置
|
||||||
|
│ ├── index.js # WebSocket + Redis 服务端逻辑
|
||||||
|
│ └── package.json # Node.js 依赖配置
|
||||||
|
└── docker-compose.yml # 服务编排配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 功能特性
|
||||||
|
|
||||||
|
* 🔑 **认证机制**:客户端需通过 `auth` 消息声明 `app` 名称。
|
||||||
|
* 📡 **订阅/取消订阅**:客户端可动态订阅频道,实时接收消息。
|
||||||
|
* 📤 **消息发布**:通过 Redis Pub/Sub 将消息广播至所有订阅者。
|
||||||
|
* 🛠 **易于扩展**:支持分布式部署,多个 WebSocket 节点可共享 Redis。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1️⃣ 启动服务
|
||||||
|
|
||||||
|
确保已安装 **Docker** 与 **Docker Compose**。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
服务启动后默认监听:
|
||||||
|
|
||||||
|
```
|
||||||
|
ws://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ 运行测试客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node client/test-client.js
|
||||||
|
```
|
||||||
|
|
||||||
|
预期输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 已连接 WebSocket
|
||||||
|
📩 收到消息: {"type":"auth_ok","app":"demo"}
|
||||||
|
📩 收到消息: {"type":"subscribed","channel":"chat"}
|
||||||
|
📩 收到消息: {"app":"demo","channel":"chat","event":"message","data":{"text":"Hello Redis WS 🚀"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 协议说明
|
||||||
|
|
||||||
|
所有消息均为 **JSON 格式**,字段如下:
|
||||||
|
|
||||||
|
### 🔑 认证
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "auth", "app": "demo" }
|
||||||
|
```
|
||||||
|
|
||||||
|
服务端响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "auth_ok", "app": "demo" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📌 订阅频道
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "subscribe", "channel": "chat" }
|
||||||
|
```
|
||||||
|
|
||||||
|
服务端响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "type": "subscribed", "channel": "chat" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📤 发布消息
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "publish",
|
||||||
|
"app": "demo",
|
||||||
|
"channel": "chat",
|
||||||
|
"event": "message",
|
||||||
|
"data": { "text": "Hello Redis WS 🚀" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
被订阅该频道的客户端将收到:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app": "demo",
|
||||||
|
"channel": "chat",
|
||||||
|
"event": "message",
|
||||||
|
"data": { "text": "Hello Redis WS 🚀" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐳 环境变量
|
||||||
|
|
||||||
|
通过 `docker-compose.yml` 设置:
|
||||||
|
|
||||||
|
| 变量名 | 默认值 | 说明 |
|
||||||
|
| ---------------- | ------------ | -------------- |
|
||||||
|
| `REDIS_HOST` | `main_redis` | Redis 主机地址 |
|
||||||
|
| `REDIS_PORT` | `6379` | Redis 端口 |
|
||||||
|
| `REDIS_PASSWORD` | 空字符串 | Redis 密码 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 构建与部署
|
||||||
|
|
||||||
|
### 本地构建镜像
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t websocket-server ./server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 运行容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8080:8080 --name ws-server websocket-server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 技术栈
|
||||||
|
|
||||||
|
* **WebSocket** (`ws`): 提供实时通信能力
|
||||||
|
* **Redis** (`ioredis`): 实现分布式消息队列与广播
|
||||||
|
* **Docker** + **Docker Compose**: 快速部署与服务编排
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 注意事项
|
||||||
|
|
||||||
|
* 本项目的 `Repomix` 合并文件仅供分析和审查,请勿直接修改。
|
||||||
|
* 如果需要调整功能,请修改原始仓库中的对应文件。
|
||||||
|
* 确保 Redis 服务正常运行,否则消息发布/订阅功能不可用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
|
||||||
|
[ISC](./server/package.json)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
要不要我帮你在 README 里再加一张 **架构图(WebSocket + Redis Pub/Sub 流程)**?这样别人一眼就能看懂整个系统的消息流向 🔄。
|
||||||
27
client/test-client.js
Normal file
27
client/test-client.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
const ws = new WebSocket("ws://localhost:8080");
|
||||||
|
|
||||||
|
ws.on("open", () => {
|
||||||
|
console.log("✅ 已连接 WebSocket");
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({ type: "auth", app: "demo" }));
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({ type: "subscribe", channel: "chat" }));
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "publish",
|
||||||
|
app: "demo",
|
||||||
|
channel: "chat",
|
||||||
|
event: "message",
|
||||||
|
data: { text: "Hello Redis WS 🚀" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (msg) => {
|
||||||
|
console.log("📩 收到消息:", msg.toString());
|
||||||
|
});
|
||||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
websocket:
|
||||||
|
build: ./server
|
||||||
|
container_name: my_websocket
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- REDIS_HOST=${REDIS_HOST}
|
||||||
|
- REDIS_PORT=${REDIS_PORT}
|
||||||
|
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- ./server:/app
|
||||||
|
- /app/node_modules
|
||||||
|
command: node index.js
|
||||||
|
networks:
|
||||||
|
- 1panel-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
1panel-network:
|
||||||
|
external: true
|
||||||
123
docs/update-suggestion.md
Normal file
123
docs/update-suggestion.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 🔧 项目升级与完善规划
|
||||||
|
|
||||||
|
本项目目前已实现 **WebSocket + Redis Pub/Sub** 的基础功能:
|
||||||
|
|
||||||
|
* 客户端认证
|
||||||
|
* 频道订阅与消息分发
|
||||||
|
* Redis 支撑的多实例共享消息通道
|
||||||
|
|
||||||
|
虽然已可用于基本的实时通信场景,但仍有较大的优化与升级空间。以下为可行的改进方向:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1️⃣ 架构层面优化
|
||||||
|
|
||||||
|
* **水平扩展**
|
||||||
|
|
||||||
|
* 通过 **Kubernetes / Docker Swarm** 部署多个 WebSocket 节点,借助 Redis 保持消息一致性。
|
||||||
|
* 支持 **负载均衡**(Nginx / HAProxy / Traefik),实现高并发环境下的稳定服务。
|
||||||
|
|
||||||
|
* **可插拔协议支持**
|
||||||
|
|
||||||
|
* 除 WebSocket 外,增加 **HTTP SSE**(Server-Sent Events)作为备用方案,兼容不支持 WebSocket 的环境。
|
||||||
|
* 未来可扩展为 **gRPC streaming** 以支持更强大的微服务交互。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2️⃣ 功能升级
|
||||||
|
|
||||||
|
* **频道管理**
|
||||||
|
|
||||||
|
* 支持 **取消订阅 (unsubscribe)** 功能。
|
||||||
|
* 增加频道权限控制(如仅允许部分用户订阅)。
|
||||||
|
* 频道分组/命名空间机制,避免全局频道名冲突。
|
||||||
|
|
||||||
|
* **消息增强**
|
||||||
|
|
||||||
|
* 增加 **消息持久化**(可选存储到 MongoDB/PostgreSQL),实现断线重连后消息回溯。
|
||||||
|
* 支持 **延时消息 / 定时任务**(借助 Redis 的 TTL/Stream 功能)。
|
||||||
|
* 引入 **QoS(消息投递保证)**:保证至少一次或仅一次投递。
|
||||||
|
|
||||||
|
* **客户端管理**
|
||||||
|
|
||||||
|
* 增加 **心跳机制** 检测连接状态,避免僵尸连接占用资源。
|
||||||
|
* 提供 **在线用户列表 API**,供业务层调用。
|
||||||
|
* 支持 **多租户模式**(不同 `app` 间消息隔离)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3️⃣ 安全性增强
|
||||||
|
|
||||||
|
* **认证与鉴权**
|
||||||
|
|
||||||
|
* 使用 **JWT / OAuth2** 代替简单的 `auth` 消息,保证身份可信。
|
||||||
|
* 支持 **API Key / Token** 校验,防止非法客户端连接。
|
||||||
|
|
||||||
|
* **传输安全**
|
||||||
|
|
||||||
|
* 支持 **wss\:// (TLS)**,避免明文传输敏感数据。
|
||||||
|
* 考虑接入 **Cloudflare Tunnel / Nginx TLS 反代**,增强边界安全。
|
||||||
|
|
||||||
|
* **防护机制**
|
||||||
|
|
||||||
|
* 增加 **速率限制 (Rate Limit)**,防止恶意刷消息。
|
||||||
|
* 监控异常行为,如频繁订阅/发布,触发封禁或警报。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4️⃣ 运维与监控
|
||||||
|
|
||||||
|
* **日志与监控**
|
||||||
|
|
||||||
|
* 集成 **Prometheus + Grafana**,监控连接数、消息吞吐、Redis 状态等。
|
||||||
|
* 提供 **健康检查 API**,供 Kubernetes 自动探测容器状态。
|
||||||
|
|
||||||
|
* **可观测性**
|
||||||
|
|
||||||
|
* 增加 **请求追踪 ID**,方便日志关联与问题定位。
|
||||||
|
* 统计消息流量、订阅情况,生成实时仪表盘。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5️⃣ 开发体验提升
|
||||||
|
|
||||||
|
* **SDK 封装**
|
||||||
|
|
||||||
|
* 提供 JavaScript/TypeScript 客户端 SDK,封装常见操作(认证、订阅、发布、心跳)。
|
||||||
|
* 后续可扩展到 **Python/Go/Java SDK**,方便跨语言接入。
|
||||||
|
|
||||||
|
* **测试与 CI/CD**
|
||||||
|
|
||||||
|
* 增加单元测试与集成测试,确保消息收发正确性。
|
||||||
|
* 通过 **GitHub Actions / GitLab CI** 自动构建与发布 Docker 镜像。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6️⃣ 未来扩展方向
|
||||||
|
|
||||||
|
* **消息队列升级**
|
||||||
|
|
||||||
|
* 采用 **Redis Streams** 或 **Kafka**,支持高吞吐与持久化订阅。
|
||||||
|
|
||||||
|
* **服务对接**
|
||||||
|
|
||||||
|
* 与 **业务 API / 数据库** 集成,实现业务事件实时推送(如订单状态更新、聊天消息分发)。
|
||||||
|
|
||||||
|
* **跨平台应用**
|
||||||
|
|
||||||
|
* 提供 **Web / Mobile / Desktop** 多端接入方案。
|
||||||
|
* 支持 **Unity/Unreal SDK**,扩展到游戏开发场景。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 总结
|
||||||
|
|
||||||
|
本项目的核心定位是一个 **轻量级的实时消息推送服务**。
|
||||||
|
后续升级的重点在于:
|
||||||
|
|
||||||
|
1. **稳定性**(扩展架构 & 健壮性)
|
||||||
|
2. **安全性**(认证、加密、防护)
|
||||||
|
3. **可观测性**(监控 & 日志)
|
||||||
|
4. **易用性**(SDK & 管理 API)
|
||||||
|
|
||||||
|
通过以上优化,可以逐步从一个 **Demo 项目** 演进为 **可商用的分布式实时通信平台**。
|
||||||
20
server/Dockerfile
Normal file
20
server/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 使用 Node.js 官方镜像
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 拷贝依赖清单
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安装依赖(只安装生产依赖即可)
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# 拷贝源码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 暴露 WebSocket 端口
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["node", "index.js"]
|
||||||
80
server/index.js
Normal file
80
server/index.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
import Redis from "ioredis";
|
||||||
|
|
||||||
|
const redisSub = new Redis({
|
||||||
|
host: process.env.REDIS_HOST || "main_redis",
|
||||||
|
port: process.env.REDIS_PORT || 6379,
|
||||||
|
password: process.env.REDIS_PASSWORD || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const redisPub = new Redis({
|
||||||
|
host: process.env.REDIS_HOST || "main_redis",
|
||||||
|
port: process.env.REDIS_PORT || 6379,
|
||||||
|
password: process.env.REDIS_PASSWORD || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = 8080;
|
||||||
|
const wss = new WebSocketServer({ port: PORT });
|
||||||
|
|
||||||
|
const clients = new Map(); // ws => { app, channels: Set }
|
||||||
|
|
||||||
|
function parseMessage(msg) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(msg);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wss.on("connection", (ws) => {
|
||||||
|
console.log("👤 新客户端连接");
|
||||||
|
clients.set(ws, { app: null, channels: new Set() });
|
||||||
|
|
||||||
|
ws.on("message", async (raw) => {
|
||||||
|
const msg = parseMessage(raw);
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
const { type, app, channel, event, data, token } = msg;
|
||||||
|
|
||||||
|
if (type === "auth") {
|
||||||
|
clients.get(ws).app = app;
|
||||||
|
ws.send(JSON.stringify({ type: "auth_ok", app }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "subscribe") {
|
||||||
|
clients.get(ws).channels.add(channel);
|
||||||
|
ws.send(JSON.stringify({ type: "subscribed", channel }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "publish") {
|
||||||
|
const payload = { app, channel, event, data };
|
||||||
|
await redisPub.publish(channel, JSON.stringify(payload));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
clients.delete(ws);
|
||||||
|
console.log("❌ 客户端断开");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redis 订阅所有频道
|
||||||
|
redisSub.psubscribe("*", (err) => {
|
||||||
|
if (err) console.error("Redis 订阅失败", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分发消息
|
||||||
|
redisSub.on("pmessage", (pattern, channel, message) => {
|
||||||
|
const payload = JSON.parse(message);
|
||||||
|
|
||||||
|
for (const [ws, info] of clients) {
|
||||||
|
if (info.channels.has(channel)) {
|
||||||
|
ws.send(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ WebSocket 服务运行在 ws://0.0.0.0:${PORT}`);
|
||||||
18
server/package.json
Normal file
18
server/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "ws.tootaio.com",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node index.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.17.0",
|
||||||
|
"dependencies": {
|
||||||
|
"ioredis": "^5.7.0",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user