一、整体架构图
bash
设备/客户端
|
| TCP JSON\n
v
ChatServer :8888 <-- 网关/生产者,也负责广播
|
| TCP JSON\n
v
Broker :7777 <-- 消息代理/转发器
|
| 轮询 TCP JSON\n
v
WriteDB-1 / WriteDB-2 / WriteDB-3 <-- 消费者
|
| SQL INSERT
v
MySQL IoT_monitor.IoTtable
同时:
bash
ChatServer 收到客户端消息后,还会广播给所有在线客户端。
角色注册流程:
bash
WriteDB -> Broker: {"role":"consumer"}\n
ChatServer -> Broker: {"role":"producer"}\n
Broker根据角色,把连接分别放进_producers 和 _consumers
二、三个服务的职责
1、ChatServer:接入网关 + 生产者
它做两件事:
1、作为TCP Server,监听8888,接收设备/客户端发来的JSON
2、作为TCP Client,连接Broker127.0.0.1:7777,把数据转发给Broker
它相当于:
- 对设备来说:它是服务器
- 对Broker来说:它是生产者producer
关键逻辑:
bash
_server.setThreadNum(4);
ChatServer 使用 4 个 IO 线程,所以 _connections 这个客户端集合需要加锁:
cpp
std::lock_guard<std::mutex> lock(_conn_mutex);
连接 Broker 后,发送角色声明:
cpp
conn->send("{\"role\":\"producer\"}\n");
收到客户端消息后:
cpp
Json::Value root;
if (!unserialize(msg, root)) { ... }
if (!root.isMember("device_id") || ... ) { ... }
std::string device_id = root["device_id"].asString();
float temperature = root["temperature"].asFloat();
float humidity = root["humidity"].asFloat();
然后重新构造一个只包含必要字段的 JSON:
cpp
Json::Value out;
out["device_id"] = device_id;
out["temperature"] = temperature;
out["humidity"] = humidity;
Json::StreamWriterBuilder builder;
builder["indentation"] = "";
std::string out_msg = Json::writeString(builder, out);
再转发给 Broker:
cpp
_db_client->connection()->send(out_msg + "\n");
同时广播给所有在线客户端:
cpp
for (auto it = _connections.begin(); it != _connections.end(); ++it)
{
(*it)->send(msg + "\n");
}
为什么这样写?
-
接入层只负责收数据、校验、转发、广播。
-
不直接写 MySQL,避免网关和数据库强耦合。
-
通过 Broker 解耦,后面可以挂多个写库服务。
-
广播让所有在线客户端能看到最新传感器数据。
2、Broker:消息代理 + 轮询负载均衡
Broker是系统的中心,它监听7777,维护两个列表:
cpp
std::vector<TcpConnectionPtr> _producers; // ChatServer
std::vector<TcpConnectionPtr> _consumers; // WriteDB
std::mutex _mtx;
size_t _next_index = 0;
它做的事情:
1、接收ChatServer和WriteDB的连接
2、根据角色消息,把连接等级为producer或consumer
3、收到业务消息,轮询转发给某个consumer
4、连接断开时,从对应列表移除
角色注册逻辑:
cpp
if (msg.find("\"role\"") != std::string::npos)
{
if (msg.find("producer") != std::string::npos)
{
_producers.push_back(conn);
}
else if (msg.find("consumer") != std::string::npos)
{
_consumers.push_back(conn);
}
return;
}
业务转发逻辑:
cpp
if (_consumers.empty())
{
LOG_WARN << "【Broker】没有消费者,消息丢弃";
return;
}
_next_index = _next_index % _consumers.size();
_consumers[_next_index]->send(msg + "\n");
_next_index++;
为什么这样写?
-
Broker 是生产者-消费者模式里的中间人。
-
ChatServer 不需要知道 WriteDB 的 IP 和数量。
-
WriteDB 不需要知道 ChatServer 是谁。
-
新增 WriteDB 实例时,只要连上 Broker 并声明
consumer,就能自动加入负载均衡。 -
轮询算法简单、均匀,适合写库这种无状态消费者。
为什么加锁?
因为:
cpp
_server.setThreadNum(2);
Broker 有多个 IO 线程,onConnection 和 onMessage 可能在不同线程并发执行,_producers、_consumers、_next_index 都是共享状态,必须加锁。
3、writeDB:消费者 + 写入MySQL
它作为 TCP Client 连接 Broker:
cpp
_client(loop, InetAddress("127.0.0.1", 7777), "WriteDBCli")
连接成功后声明自己是消费者:
cpp
conn->send("{\"role\":\"consumer\"}\n");
收到 Broker 转发的消息后:
cpp
Json::Value root;
if (!unserialize(msg, root)) { ... }
if (!root.isMember("device_id") || ... ) { ... }
std::string device_id = root["device_id"].asString();
float temperature = root["temperature"].asFloat();
float humidity = root["humidity"].asFloat();
拼 SQL:
cpp
std::string sql =
"INSERT INTO IoTtable(device_id, temperature, humidity) VALUES ('"
+ _mysql->escape(device_id) + "', "
+ std::to_string(temperature) + ", "
+ std::to_string(humidity) + ")";
加锁执行:
cpp
std::lock_guard<std::mutex> lock(_db_mutex);
if (!_mysql->execute(sql)) { ... }
为什么这样写?
-
消费者只关心"收到 JSON,写进 MySQL"。
-
多个 WriteDB 实例可以同时消费,提高写库吞吐。
-
用
escape防止device_id里的引号破坏 SQL,降低注入风险。 -
加锁是因为 MySQL 连接对象通常不是线程安全的。虽然当前 WriteDB 是单 EventLoop,但加锁是为将来扩展或线程安全兜底。
三、通信协议:TCP + JSON + \n
三个程序都用了同一套拆包逻辑:
cpp
while (true)
{
const char* data = buf->peek();
size_t len = buf->readableBytes();
if (len == 0) break;
const char* newline = static_cast<const char*>(memchr(data, '\n', len));
if (newline == nullptr) break;
std::string msg(data, newline - data);
buf->retrieveUntil(newline + 1);
if (msg.empty()) continue;
handleOneMessage(msg);
}
这是处理 TCP 粘包/半包的标准写法:
-
TCP 是字节流,没有消息边界。
-
用
\n作为一条消息的结束符。 -
如果没找到
\n,说明消息不完整,等下一次数据。 -
如果一次读到多条消息,循环拆开。
-
retrieveUntil消费掉已经处理的数据。
为什么用 JSON?
-
可读性好,方便调试。
-
字段扩展方便。
-
跨语言。
为什么用 \n 分帧?
-
简单。
-
文本协议直观。
-
但 JSON 内部不能有换行,所以 ChatServer 序列化时设置了:
cpp
builder["indentation"] = "";
否则 JSON 美化输出会带换行,破坏协议。
四、数据流完整时序
假设有一个设备发数据:
cpp
{"device_id":"sensor01","temperature":25.6,"humidity":60.2}
流程:
-
设备连接 ChatServer
8888。 -
设备发送 JSON +
\n。 -
ChatServer 收到,拆包,解析 JSON。
-
ChatServer 校验
device_id、temperature、humidity。 -
ChatServer 重新构造精简 JSON,发给 Broker
7777。 -
ChatServer 同时把原始消息广播给所有在线客户端。
-
Broker 收到业务消息,检查消费者列表。
-
Broker 用轮询选一个 WriteDB:
cpp
_next_index = _next_index % _consumers.size();
_consumers[_next_index]->send(msg + "\n");
_next_index++;
-
WriteDB 收到消息,拆包,解析 JSON。
-
WriteDB 拼 SQL,写入 MySQL:
cpp
INSERT INTO IoTtable(device_id, temperature, humidity)
VALUES ('sensor01', 25.600000, 60.200000);
- 如果某个 WriteDB 挂掉,TCP 断开,Broker 在
onConnection中把它从_consumers移除,其他 WriteDB 继续工作。
五、为什么这样设计?
1、解耦
如果 ChatServer 直接写 MySQL:
-
网关要处理数据库连接。
-
写库失败会影响网关。
-
无法方便地增加多个写库实例。
-
数据库压力直接打到网关。
引入 Broker 后:
-
ChatServer 只管接入和转发。
-
WriteDB 只管消费和写库。
-
Broker 只管路由和负载均衡。
-
每个角色职责单一。
2、生产者 - 消费者
-
ChatServer 是生产者。
-
WriteDB 是消费者。
-
Broker 是中间队列/代理。
这是消息队列的基本思想,类似简化版 Kafka、RabbitMQ、Redis Pub/Sub
3、轮询负载均衡
cpp
_next_index = _next_index % _consumers.size();
_consumers[_next_index]->send(msg + "\n");
_next_index++;
好处:
-
简单。
-
不需要记录每个消费者的负载。
-
多个 WriteDB 可以均摊写库压力
4、故障隔离
WriteDB 挂掉:
-
TCP 断开。
-
Broker 的
onConnection检测到。 -
removeFromList(_consumers, conn)移除。 -
其他 WriteDB 继续消费。
这体现了一定故障隔离能力
5、多线程与锁
-
ChatServer
setThreadNum(4),多 IO 线程,所以_connections加锁。 -
Broker
setThreadNum(2),多 IO 线程,所以_producers/_consumers/_next_index加锁。 -
WriteDB 单 EventLoop,但 MySQL 连接非线程安全,所以
_db_mutex加锁。
这说明作者意识到 muduo 多线程回调会并发访问共享数据
六、为什么这是小型分布式?
严格说,如果都跑在 127.0.0.1,它是单机多进程。但从架构风格看,它已经是小型分布式系统的雏形。
它具备分布式系统的这些特征:
-
多节点/多进程
-
ChatServer、Broker、WriteDB 是独立进程。
-
如果把
127.0.0.1改成不同机器 IP,就可以多机部署。
-
-
网络通信
-
不是函数调用,而是 TCP 长连接。
-
有明确的网络协议:JSON +
\n。
-
-
角色分离
-
接入、转发、存储分离。
-
类似微服务。
-
-
服务注册
-
WriteDB 连上 Broker 后发
{"role":"consumer"}。 -
ChatServer 连上 Broker 后发
{"role":"producer"}。 -
Broker 动态维护生产者/消费者列表。
-
-
负载均衡
- Broker 轮询转发给多个 WriteDB。
-
故障隔离
-
单个 WriteDB 挂掉,其他 WriteDB 继续工作。
-
Broker 会从消费者列表移除断开的连接。
-
-
水平扩展
-
多启动几个 WriteDB,都连 Broker,就能增加写库能力。
-
不需要改 ChatServer。
-
但它还不是生产级分布式,因为缺少:
-
Broker 集群和副本,Broker 是单点。
-
消息持久化,Broker 内存转发,崩溃丢消息。
-
ACK 确认、重试、幂等。
-
消费者健康检查、心跳。
-
服务发现,IP/端口硬编码。
-
分布式协调,如 etcd/ZooKeeper。
-
数据库主从、分库分表。
-
监控、限流、熔断、链路追踪。
-
安全认证、TLS、鉴权。
-
消息顺序保证。
-
背压机制
七、总结
用 ChatServer 做接入网关,用 Broker 做简化消息队列和轮询负载均衡,用多个 WriteDB 做消费者写 MySQL。通过 TCP + JSON + 换行分帧,把接入、转发、存储解耦,形成一个可扩展、可故障隔离的小型分布式数据管道