325 lines
11 KiB
Markdown
325 lines
11 KiB
Markdown
# 任务016:TCP服务重构
|
||
|
||
## 目标
|
||
|
||
重构TCP服务,拆分为API服务和设备服务,使用Redis做服务发现和消息队列。
|
||
|
||
## 架构设计
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ API服务 (api-server) — 主服务 │
|
||
│ - HTTP API(后台管理 + H5)端口: 3000 │
|
||
│ - 前端轮询刷新(每5秒) │
|
||
│ - 业务逻辑处理(验证/存DB/告警) │
|
||
│ - 节点管理(设备服务注册/心跳) │
|
||
│ - 后台协程处理Redis队列 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↑ Redis LIST ↑ Redis LIST
|
||
│ device:report:{imei} │ device:cmd:{imei}
|
||
│ (设备上报) │ (平台指令)
|
||
│ │
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 设备服务 (device-server) — 轻量化 │
|
||
│ - TCP监听: 3002(设备长连接) │
|
||
│ - 只处理通信和指令解析 │
|
||
│ - 不存DB、不验证签名、不处理业务 │
|
||
│ - 注册到API服务(启动/心跳/注销) │
|
||
│ - Redis连接注册: device:{imei} → {node_id, ip, port} │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 核心设计原则
|
||
|
||
1. **TCP服务轻量化** — 只处理TCP通信和指令解析,不处理业务逻辑
|
||
2. **Redis做缓冲** — 服务间通信用Redis LIST,不用HTTP直接调用
|
||
3. **全异步处理** — 所有指令异步处理,msg_id匹配响应
|
||
4. **协程处理任务** — API服务用tokio::spawn处理Redis队列消息
|
||
|
||
## 重构清单
|
||
|
||
### 1. 项目结构拆分
|
||
|
||
**当前:** 单服务 `software/server/`
|
||
|
||
**重构后:**
|
||
```
|
||
software/
|
||
├── api-server/ # API服务(主服务)
|
||
│ ├── Cargo.toml
|
||
│ ├── config.toml
|
||
│ └── src/
|
||
│ ├── main.rs
|
||
│ ├── config.rs
|
||
│ ├── routes/
|
||
│ │ ├── devices.rs # 设备管理API
|
||
│ │ ├── nodes.rs # 节点管理API
|
||
│ │ └── ...
|
||
│ ├── workers/ # 后台协程
|
||
│ │ ├── report_worker.rs # 处理设备上报
|
||
│ │ └── reply_worker.rs # 处理设备响应
|
||
│ ├── middleware/
|
||
│ ── h5/
|
||
├── device-server/ # 设备服务(轻量化)
|
||
│ ├── Cargo.toml
|
||
│ ├── config.toml
|
||
│ └── src/
|
||
│ ├── main.rs
|
||
│ ├── config.rs
|
||
│ ├── tcp/
|
||
│ │ ├── server.rs # TCP监听
|
||
│ │ ├── connection.rs # 连接管理(内存HashMap)
|
||
│ │ ├── protocol.rs # 协议解析
|
||
│ │ └── handler.rs # 消息处理(解析+转发Redis)
|
||
│ └── redis.rs # Redis连接注册
|
||
```
|
||
|
||
**说明:** 暂不提取shared crate,协议定义等代码先复制到两个服务。
|
||
|
||
### 2. 配置文件(config.toml)
|
||
|
||
**api-server/config.toml:**
|
||
```toml
|
||
[server]
|
||
host = "0.0.0.0"
|
||
port = 3000
|
||
|
||
[database]
|
||
url = "mysql://root:password@10.8.0.252:3306/pms_dev"
|
||
|
||
[redis]
|
||
url = "redis://:password@10.8.0.252:6379/5"
|
||
|
||
[jwt]
|
||
secret = "pms-dev-secret-change-me-in-production"
|
||
|
||
[export]
|
||
dir = "./exports"
|
||
|
||
[node]
|
||
heartbeat_timeout = 180
|
||
```
|
||
|
||
**device-server/config.toml:**
|
||
```toml
|
||
[server]
|
||
host = "0.0.0.0"
|
||
tcp_port = 3002
|
||
node_id = "node-1"
|
||
|
||
[redis]
|
||
url = "redis://:password@10.8.0.252:6379/5"
|
||
|
||
[api_server]
|
||
url = "http://127.0.0.1:3000"
|
||
heartbeat_interval = 60
|
||
|
||
[device]
|
||
register_ttl = 180
|
||
```
|
||
|
||
### 3. 服务间通信(Redis LIST)
|
||
|
||
**设备上报(设备→API):**
|
||
```rust
|
||
// 设备服务:收到设备消息后推送到Redis
|
||
async fn forward_to_api(redis: &Redis, imei: &str, msg: &DeviceMessage) {
|
||
redis.lpush(&format!("device:report:{}", imei), serde_json::to_string(msg)).await;
|
||
}
|
||
|
||
// API服务:后台协程处理上报
|
||
async fn process_reports(redis: Redis, mysql: MySqlPool) {
|
||
loop {
|
||
let (_, msg_json) = redis.brpop(&["device:report:*"], 0).await;
|
||
let msg: DeviceMessage = serde_json::from_str(&msg_json).unwrap();
|
||
tokio::spawn(handle_report(msg, mysql.clone())); // 异步处理
|
||
}
|
||
}
|
||
```
|
||
|
||
**平台指令(API→设备):**
|
||
```rust
|
||
// API服务:下发指令推送到Redis
|
||
async fn send_command(redis: &Redis, imei: &str, cmd: DeviceCommand) {
|
||
redis.lpush(&format!("device:cmd:{}", imei), serde_json::to_string(&cmd)).await;
|
||
}
|
||
|
||
// 设备服务:阻塞弹出指令,发送到设备
|
||
async fn process_commands(redis: Redis, pool: ConnectionPool) {
|
||
loop {
|
||
let (key, cmd_json) = redis.brpop(&["device:cmd:*"], 0).await;
|
||
let imei = extract_imei(&key);
|
||
let cmd: DeviceCommand = serde_json::from_str(&cmd_json).unwrap();
|
||
|
||
// 发送到设备(异步等待响应)
|
||
if let Some(conn) = pool.get(&imei) {
|
||
conn.send(cmd).await;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**指令响应(设备→API):**
|
||
```rust
|
||
// 设备服务:收到设备响应后推送到Redis
|
||
async fn forward_reply(redis: &Redis, imei: &str, reply: DeviceResponse) {
|
||
redis.lpush(&format!("device:reply:{}", imei), serde_json::to_string(&reply)).await;
|
||
}
|
||
|
||
// API服务:后台协程匹配msg_id
|
||
async fn process_replies(redis: Redis, pending: PendingCommands) {
|
||
loop {
|
||
let (_, reply_json) = redis.brpop(&["device:reply:*"], 0).await;
|
||
let reply: DeviceResponse = serde_json::from_str(&reply_json).unwrap();
|
||
if let Some(tx) = pending.remove(&reply.msg_id) {
|
||
tx.send(reply).ok();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. 节点管理(API服务)
|
||
|
||
**节点注册:**
|
||
```rust
|
||
// POST /api/nodes/register
|
||
async fn register_node(Json(req): Json<NodeRegisterRequest>) -> Result<Json<Value>> {
|
||
// 记录节点信息到内存/DB
|
||
nodes.insert(req.node_id.clone(), NodeInfo {
|
||
id: req.node_id,
|
||
ip: req.ip,
|
||
tcp_port: req.tcp_port,
|
||
status: "online",
|
||
last_heartbeat: Instant::now(),
|
||
});
|
||
Ok(Json(json!({"suc": 1})))
|
||
}
|
||
```
|
||
|
||
**节点心跳:**
|
||
```rust
|
||
// POST /api/nodes/:node_id/heartbeat
|
||
async fn node_heartbeat(Path(node_id): Path<String>) -> Result<Json<Value>> {
|
||
if let Some(node) = nodes.get_mut(&node_id) {
|
||
node.last_heartbeat = Instant::now();
|
||
}
|
||
Ok(Json(json!({"suc": 1})))
|
||
}
|
||
```
|
||
|
||
**节点注销:**
|
||
```rust
|
||
// POST /api/nodes/:node_id/deregister
|
||
async fn deregister_node(Path(node_id): Path<String>) -> Result<Json<Value>> {
|
||
nodes.remove(&node_id);
|
||
Ok(Json(json!({"suc": 1})))
|
||
}
|
||
```
|
||
|
||
**节点列表:**
|
||
```rust
|
||
// GET /api/nodes
|
||
async fn list_nodes() -> Result<Json<Vec<NodeInfo>>> {
|
||
Ok(Json(json!(nodes.values().collect::<Vec<_>>())))
|
||
}
|
||
```
|
||
|
||
### 5. 设备服务注册流程
|
||
|
||
```rust
|
||
// 设备服务启动时
|
||
async fn startup(api_url: &str, node_id: &str, ip: &str, tcp_port: u16) {
|
||
// 1. 注册到API服务
|
||
let client = reqwest::Client::new();
|
||
client.post(&format!("{}/api/nodes/register", api_url))
|
||
.json(&json!({ "node_id": node_id, "ip": ip, "tcp_port": tcp_port }))
|
||
.send().await;
|
||
|
||
// 2. 启动心跳协程(每60秒)
|
||
let api_url = api_url.to_string();
|
||
let node_id = node_id.to_string();
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
||
loop {
|
||
interval.tick().await;
|
||
client.post(&format!("{}/api/nodes/{}/heartbeat", api_url, node_id))
|
||
.send().await;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 设备服务关闭时
|
||
async fn shutdown(api_url: &str, node_id: &str) {
|
||
let client = reqwest::Client::new();
|
||
client.post(&format!("{}/api/nodes/{}/deregister", api_url, node_id))
|
||
.send().await;
|
||
}
|
||
```
|
||
|
||
### 6. 异步指令处理(msg_id匹配)
|
||
|
||
```rust
|
||
// API服务下发指令
|
||
async fn send_command_to_device(imei: &str, cmd: DeviceCommand) -> Result<DeviceResponse> {
|
||
let (tx, rx) = oneshot::channel();
|
||
pending_commands.insert(cmd.msg_id, tx);
|
||
|
||
// 推送到Redis队列
|
||
redis.lpush(&format!("device:cmd:{}", imei), serde_json::to_string(&cmd)).await;
|
||
|
||
// 等待响应(超时60秒)
|
||
match tokio::time::timeout(Duration::from_secs(60), rx).await {
|
||
Ok(Ok(response)) => Ok(response),
|
||
Ok(Err(_)) => Err(AppError::Internal("通道关闭".into())),
|
||
Err(_) => {
|
||
pending_commands.remove(&cmd.msg_id);
|
||
Err(AppError::Timeout("设备响应超时".into()))
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7. 自动充电流程
|
||
|
||
```rust
|
||
// API服务收到 bat_in 上报
|
||
async fn handle_bat_in(imei: &str, channel: &str, bms: &BmsData) {
|
||
// 1. 检查通道是否已有电池
|
||
if let Some(old_battery) = battery_map.get(channel) {
|
||
// 旧电池强制结束
|
||
record_charge_complete(channel, old_battery, ChargeEndReason::Forced);
|
||
}
|
||
|
||
// 2. 记录新电池
|
||
battery_map.insert(channel.to_string(), battery.clone());
|
||
|
||
// 3. 验证电池合规性
|
||
match validate_battery(bms) {
|
||
Ok(()) => {
|
||
// 4. 自动下发充电指令
|
||
send_command_to_device(imei, DeviceCommand {
|
||
act: "on".to_string(),
|
||
id: channel.to_string(),
|
||
msg_id: generate_msg_id(),
|
||
}).await;
|
||
}
|
||
Err(e) => {
|
||
// 验证失败,记录日志
|
||
tracing::warn!("电池验证失败: {}", e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 质量约束
|
||
|
||
1. 完成代码后执行 `cargo check` + `cargo clippy`,零报错零警告
|
||
2. 分层拆分,单函数≤80行,命名语义化,完整注释
|
||
3. 所有外部IO/网络请求异常捕获,禁止裸panic
|
||
4. 分支逻辑全覆盖,不遗漏兜底分支
|
||
5. 常量抽离,不使用废弃API
|
||
6. Rust内存安全,合理管理所有权,禁用unsafe无合理理由
|
||
7. 异步代码无阻塞操作
|
||
8. 指令处理必须异步,msg_id匹配响应
|
||
9. TCP服务不处理业务逻辑,只负责通信和解析
|