11 KiB
11 KiB
任务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} │
└─────────────────────────────────────────────────────────────┘
核心设计原则
- TCP服务轻量化 — 只处理TCP通信和指令解析,不处理业务逻辑
- Redis做缓冲 — 服务间通信用Redis LIST,不用HTTP直接调用
- 全异步处理 — 所有指令异步处理,msg_id匹配响应
- 协程处理任务 — 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:
[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:
[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):
// 设备服务:收到设备消息后推送到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→设备):
// 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):
// 设备服务:收到设备响应后推送到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服务)
节点注册:
// 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})))
}
节点心跳:
// 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})))
}
节点注销:
// 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})))
}
节点列表:
// GET /api/nodes
async fn list_nodes() -> Result<Json<Vec<NodeInfo>>> {
Ok(Json(json!(nodes.values().collect::<Vec<_>>())))
}
5. 设备服务注册流程
// 设备服务启动时
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匹配)
// 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. 自动充电流程
// 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);
}
}
}
质量约束
- 完成代码后执行
cargo check+cargo clippy,零报错零警告 - 分层拆分,单函数≤80行,命名语义化,完整注释
- 所有外部IO/网络请求异常捕获,禁止裸panic
- 分支逻辑全覆盖,不遗漏兜底分支
- 常量抽离,不使用废弃API
- Rust内存安全,合理管理所有权,禁用unsafe无合理理由
- 异步代码无阻塞操作
- 指令处理必须异步,msg_id匹配响应
- TCP服务不处理业务逻辑,只负责通信和解析