别再用轮询假装实时了:设备端长连接到底该怎么做
本文最后更新于30 天前,其中的信息可能已经过时,如有错误请发送邮件到2177367423@qq.com

博客很久没更新了,这篇算是临时插一脚。

起因很简单:最近看了不少设备管理类程序,很多作者做了很久,设备端收命令还是靠 HTTP 或 HTTPS 轮询。几秒请求一次服务器,没命令也请求,有命令还要等下一轮。设备少的时候还能凑合,设备多了以后,延迟、流量、服务器压力都会变得很难看。

说难听点,早期凑合用可以理解,但一直这么写,就有点说不过去了。

这篇不是为了展示我自己的业务系统,也不会贴真实接口和完整代码。这里只聊一个通用问题:设备端如果想从“高频刷新”升级到“真正实时”,长连接到底该怎么设计。

这篇也算是改版的,原先近5000字文章,包括做了一些示例代码,结构做法,踩过的坑,解决办法,直接发布还是太良心了,弯路全让我走完了,为了让你们也多走走弯路,于是就有这一版本

轮询的问题

传统轮询大概是这样:

设备定时请求服务器
服务器返回待执行命令
设备执行命令
设备再上报结果

这个方案简单,但问题也明显:

命令延迟取决于轮询间隔
没有命令也会产生大量无效请求
设备越多,服务器压力越大
网络差时容易堆积请求
看起来在线,实际实时性很差

把轮询间隔从 5 秒改成 1 秒,只是用更多资源换更低延迟,不是从根上解决问题。这不叫实时通信,只是高频刷新。

WebSocket 的定位

WebSocket 适合解决“服务器主动通知设备”的问题。连接建立后,设备不需要一直问服务器有没有命令,服务器有任务时可以主动推送。

但这里有个误区:不是用了 WebSocket,就自动变高级了。

真正重要的是:

设备身份是否可信
消息是否只发给目标设备
命令是否会重复执行
结果是否可靠保存
断线后状态能否恢复
服务端是否能识别假在线

这些没做好,只是把轮询的问题换成了长连接的问题。

C 端不要手写协议

如果设备端是 C 语言,确实可以从 socket 开始手写 WebSocket 握手、mask、分片、ping/pong。但实际项目里没必要这么干。

更合理的是使用成熟 WebSocket 库,让库处理协议层细节,自己把精力放在业务层:

连接管理
认证流程
消息分发
命令队列
执行结果回传
断线重连
本地状态恢复

很多人第一版能连上,但跑一段时间就各种假死,原因通常不是 WebSocket 协议本身,而是业务层状态没设计好。

连接成功不代表可信

设备连上 WebSocket,只能说明 TCP 和握手成功,不代表这台设备就是合法设备。

如果连接成功后就马上信任设备,风险很大。任何人只要知道连接地址,就可能伪造成设备接入。更严重的是,如果所有设备共用公共通道,命令隔离就会失效。

可能出现的问题包括:

非法设备伪装上线
设备收到不属于自己的命令
攻击者监听公共通道消息
设备 A 的命令被设备 B 执行
结果回传到错误记录
在线状态被伪造
恶意连接拖垮实时服务

有些人会在消息里带设备 ID,然后让客户端自己判断“这条命令是不是我的”。这不是安全边界,只是靠客户端自觉。客户端代码一改,这层判断就没了。

正确思路是:认证和隔离必须放在服务端做。

设备连接后,服务端至少要确认:

设备是否存在
设备凭证是否正确
当前连接是否允许代表这台设备
这台设备是否只能接收自己的消息
旧连接是否仍然有效

只有服务端确认通过,设备才应该进入自己的隔离通道。不要让所有设备挤在一个公共通道里靠自觉过滤。

回调里不要执行耗时任务

设备端收到命令后,不建议直接在 WebSocket 回调里执行。

更稳的结构是:

连接线程只负责收发消息
收到命令后放入本地队列
执行线程从队列取任务
执行完成后回传状态

这样可以避免一个耗时命令卡住长连接。否则命令执行慢一点,心跳、后续消息、断线检测都会受影响。

很多所谓“长连接不稳定”,其实不是连接不稳定,而是开发者把业务执行、消息处理、心跳维护全塞在一起,最后互相阻塞。

结果怎么回传

不用 HTTP 轮询收命令,不代表不能用 HTTP 上报结果。

结果回传常见有三种思路:

WebSocket 收命令,HTTP 上报结果
WebSocket 收命令,WebSocket 回传结果
小结果走 WebSocket,大文件走上传通道

第一种最稳,也最容易落库。它不是落后,落后的是用 HTTP 高频轮询收命令。结果上报是事件驱动,不是无脑刷新。

第二种看起来更统一,但要求更高。服务端必须能接收设备消息、保存结果、返回确认,并处理重复上报。否则设备以为发了,服务端未必真的保存了。

第三种更适合实际项目。小段状态、退出码、简短文本可以走长连接,大日志、截图、文件、压缩包这类内容不要硬塞 WebSocket。长连接是实时通道,不是大文件传输通道。

ACK 不是可有可无

如果结果走 WebSocket,必须考虑确认机制。

设备发了结果,不等于服务端保存成功。服务端保存成功后,应该有确认。设备没收到确认前,不能轻易丢掉本地结果。

否则断线时会出现这些问题:

服务端以为命令发了,设备其实没收到
设备以为结果发了,服务端其实没保存
连接断过一次,命令状态全乱了
重复上报导致结果被覆盖
旧状态覆盖新状态

这里面每一个坑,都会在设备量上来以后放大。

状态不要只有成功和失败

命令执行状态不要只设计成“成功”和“失败”。

更合理的是至少区分:

等待下发
设备已收到
执行中
执行成功
执行失败
执行超时
已取消

这样前端展示命令历史时才清楚。否则你只看到“已下发”,但不知道设备到底有没有收到,也不知道是在执行中还是已经卡死。

状态越粗糙,排查问题越痛苦。

心跳不是摆设

WebSocket 不处理好心跳,很容易出现“看起来在线,其实已经死了”的情况。

心跳至少要分两层:

协议层 ping/pong
业务层 heartbeat

协议层用于判断连接是否还活着,业务层用于判断设备状态是否正常。业务心跳可以带一些轻量信息,比如版本、队列长度、运行时间、当前连接标识等。

服务端判断在线,不应该只看 socket 有没有断,而应该看最后一次有效业务心跳。

重连要克制

设备端一定会断线。移动网络、代理、DNS、TLS、服务端重启,都可能导致连接中断。

重连策略不要写成死循环狂连。比较合理的是:

首次失败短暂等待
连续失败逐渐增加间隔
达到上限后低频重连
连接成功后重置失败次数
认证失败不要高频重试
被服务端拒绝时按服务端策略等待

还要处理连接假死:

长时间没有收到消息
长时间没有收到 pong
订阅后迟迟没有确认
连续写入失败

这些情况都应该主动断开旧连接,再重新建立。

重复连接必须处理

设备端经常会出现旧进程没死,新进程又起来的情况。于是同一台设备可能同时存在两个连接。

服务端如果两个都信,就会出现:

命令重复下发
结果重复回传
在线状态反复跳变
旧连接覆盖新连接
新版本被旧进程抢占

服务端必须能判断同一设备的活跃连接关系。旧连接超时可以接管,旧连接仍活跃时要拒绝或踢掉。这里处理不好,后面排查问题会非常痛苦。

HTTP 轮询还要不要

我的观点是:不要把 HTTP 轮询当实时主通道,但可以保留低频补偿。

主通道应该是:

服务端有命令 -> 主动推送给设备

补偿通道可以是:

低频检查是否存在漏掉的任务

这不是为了实时,而是为了可靠。网络世界没有百分百可靠的推送。除非你把 ACK、重连恢复、本地暂存、服务端重发都做完整,否则保留低频补偿并不丢人。

真正丢人的是几秒一次硬刷,然后对外说自己是实时。

如果完全不用轮询

如果完全不用轮询,那就要补上可靠性设计。

至少要考虑:

命令唯一标识
设备收到确认
执行中状态
最终结果确认
服务端保存确认
本地未确认结果暂存
断线后补发
重复上报幂等
服务端未确认命令重发

也就是说,你省掉了轮询,但不能省掉状态恢复。否则只是把简单问题换成复杂问题。

安全边界不能省

设备端通信必须有安全边界。

至少要考虑:

设备认证
连接鉴权
消息隔离
命令白名单
参数校验
结果大小限制
频率限制
TLS
幂等处理
日志脱敏

尤其是命令执行,不要设计成服务端传什么字符串,设备端就无脑执行什么字符串。真实项目里,命令应该尽量设计成明确动作,而不是随意远程执行。

动作越明确,风险越可控。

总结

设备端实时通信不是玄学,WebSocket 也不是什么新东西。

真正难的不是“连上”,而是连上以后:

怎么认证
怎么隔离
怎么收命令
怎么防重复
怎么回传结果
怎么确认保存
怎么断线恢复
怎么避免假在线

HTTP 轮询不是完全不能用。HTTP 可以用来认证、上传、结果上报、低频补偿。但如果你还在用高频 HTTP 轮询当主通道收命令,那就真的该升级了。

设备少的时候,很多问题都能被掩盖。设备一多,延迟、压力、假在线、重复执行、状态错乱都会暴露出来。

这篇不贴具体实现,也不贴业务接口。只是想给还停留在高频轮询阶段的同行提个醒:该升级就升级,别让设备一直傻等下一轮请求。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇