# 任务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) -> Result> { // 记录节点信息到内存/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) -> Result> { 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) -> Result> { nodes.remove(&node_id); Ok(Json(json!({"suc": 1}))) } ``` **节点列表:** ```rust // GET /api/nodes async fn list_nodes() -> Result>> { Ok(Json(json!(nodes.values().collect::>()))) } ``` ### 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 { 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服务不处理业务逻辑,只负责通信和解析