不能直接用tcp连接池模拟总线,因为tcp是全双工非广播协议,慢客户端会阻塞send()拖垮全局循环,且无法可靠感知连接状态;而udp组播(224.0.0.1~239.255.255.255)支持多进程同时接收,内核自动复制分发,更符合总线语义。

为什么不能直接用 TCP 连接池模拟总线
很多人一上来就想着用 std::vector<:shared_ptr>></:shared_ptr> 存所有客户端,然后每次发消息遍历广播——这看似像总线,但实际会卡死:TCP 是全双工但非广播协议,一个慢客户端阻塞 send() 会拖垮整个循环;更糟的是,没人断开连接时你根本不知道该不该清理句柄。真正的局域网总线得靠 UDP 组播或本地 socket 广播,而不是“假装多路复用 TCP”。
用 UDP 组播实现最简可靠总线(Linux/macOS)
局域网内最轻量、最接近硬件总线语义的方式是 UDP 组播:224.0.0.1 到 239.255.255.255 范围内的地址可被多个进程同时 bind() 和 recvfrom(),发一次,所有订阅者自动收到,内核完成复制。关键点不是“怎么发”,而是“怎么让客户端安全加入/退出”:
-
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop))必须设为 0,否则自己也会收到自己发的包 - 订阅时要调用
ip_mreq+setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, ...),退订对应IP_DROP_MEMBERSHIP - 发送端不需要
connect(),但必须用sendto()指定组播地址+端口;接收端bind()到INADDR_ANY即可 - 别用
localhost或127.0.0.1测试——组播默认不走回环,要用真实网卡 IP 或确保net.inet.ip.mcast.loop=1(macOS)或net.ipv4.ip_forward=1(Linux)已启用
Windows 上组播 socket 的特殊坑
Windows 对组播支持弱于 Unix,常见错误是 WSAENETDOWN 或收不到包,根源常是网卡绑定顺序和接口索引没显式指定:
- 调用
setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (char*)&iface_addr, sizeof(iface_addr))显式指定出口网卡,iface_addr必须是本机某个网卡的真实 IPv4 地址(不能是INADDR_ANY) - 注册组播组前,先用
getaddrinfo()查本机所有 IPv4 地址,挑一个活跃的、非 127.0.0.1 的填进ip_mreq.imr_interface.s_addr -
SO_REUSEADDR在 Windows 上对组播 socket 是必需的,否则第二个进程bind()直接失败
如何避免“订阅者收不到第一条消息”
这是最容易被忽略的时序问题:客户端刚加入组播组,还没来得及 recvfrom(),服务端就发了一条广播——这条消息就丢了。解决方案不是重传,而是让总线带状态同步机制:
- 新客户端连接后,先发一条
GET_STATE请求到专用控制 UDP 端口(比如总线端口+1),服务端响应当前最新消息 ID 和最近 3 条 payload - 或者更简单:服务端启动后,每 5 秒固定发一条空心跳包(含单调递增序列号),所有客户端启动时先等一个心跳再开始处理业务消息
- 千万别依赖“发完再 bind”或“sleep(100ms)”——网络栈调度不可控,只有显式握手或周期心跳能覆盖所有竞态
组播本身不保证送达,但局域网丢包率通常低于 0.1%,真正要防的是初始化阶段的状态错位,而不是丢包重传。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











