
做直播后端这几年我经常被问到一个问题直播间右上角那个不断跳动的人气数字到底是哪个接口返回的实现逻辑是什么尤其是当很多人想做直播数据监控、直播间热度分析的时候第一反应都是去抓抖音的接口。我先把结论放在前面人气数字不等于真实在线人数接口也远没有你想的那么简单而市面上流传的“协议”基本都游走在违规边缘。这篇文章我会从公开可观察的前端协议、通用实时通信技术、业务算法设计三个角度完整拆解一个直播间人气系统是怎么运转的也会讲讲哪些事情真的不能碰。1. 先搞清楚“人气”到底是什么1.1 一个数字背后的产品口径你看到的“XXX人在观看”或者右上角的在线人数产品上叫“直播间热度”或“实时观众数”。很多人误以为它就是当前有多少个WebSocket连接挂在这个直播间实际上这个数字几乎是“算”出来的而不是“数”出来的。一个直播间的观众可以分成几类正在看的人、刚进来的路人、刷到推荐页停留了3秒的人、点了关注但没进直播间的人。产品经理要的“人气”通常是一个能够体现直播间活跃度和商业价值的综合指标。我做过电商直播相关的项目我们当时的口径是最近5分钟内有互动行为或持续观看超过一定时长的独立用户数再叠加一定的热度权重。这个口径和“当前连接数”差别很大因为用户退到后台、切到别的App、锁屏之后客户端连接还在但用户已经不看了。还有一个容易被忽略的点平台展示给人看的数字普遍有“缩放”和“延迟”。抖音这类超大型直播平台不会把真实在线人数原样展示因为真实数值波动太剧烈会给观众造成“人数怎么突然掉了1万”的负面感受。所以你会看到很多直播间的人数固定在某个区间或者变化非常平滑这背后是有意做的平滑策略。1.2 为什么人气不直接等于在线人数如果你自己做过带聊天室功能的应用第一反应肯定是用一个Redis计数器有人进房间就incr有人离开就decr然后前端定时拉一下。这套方案在几百人、几千人的场景下完全够用但放到百万级同时在线的直播间会有三个致命问题。第一连接数不等于真人。直播间有大量的协议连接是客户端主动建立的但用户可能只是把App挂在后台听声音或者手机锁屏了。这个状态下如果计入在线人数数字会严重虚高。第二短时间进出非常频繁。一场直播的流量高峰里每秒可能有上千人进出如果每次进出都更新一次精确计数存储层和推送层都会被打爆。第三产品需要的是“可解释的稳定指标”不是一个物理计数值。平台要拿这个数字投放广告、和主播结算、做流量调控它必须是可干预、可校准的。这时候你会明白真正的人气算法通常分成两层底层是实时采集所有用户行为事件例如进房、退房、评论、点赞、送礼、观看时长上层是消费这些事件流做滑动窗口计算、去重、加权最终产出一个供展示用的热度值。1.3 从后端视角定义数据链路我习惯把整条链路拆成这样几段客户端行为采集进房、退房、心跳、评论、点赞、送礼、分享全部以消息事件的形式上报。接入层WebSocket接入负责维持长连接、鉴权、心跳管理。消息管道事件进入Kafka或者类似的分布式消息队列按直播间ID做分区保证同一个直播间的消息是有序的。实时计算层消费消息流做5分钟或1分钟的滑动窗口计算产出当前人气值。下发通道把人气值、榜单、评论等数据推回客户端通常还是走WebSocket。客户端渲染拿到增量数据后做动画插值让数字平滑滚动。很多研究“抖音协议”的人只看到最后一层也就是客户端接收到的那几条消息于是以为拿到消息格式就等于拿到了协议。实际上真正复杂的是中间那一整条流水线。就算你拿到了消息字段没有签名、鉴权和风控策略照样连不上服务器。2. 客户端是怎么拿到人气数据的协议侧拆解2.1 最常见的三种实时链路做实时数据下发业界通用的方案就三种HTTP短轮询、WebSocket长连接、SSE单向推送。直播间这类高实时、双向交互的场景主流方案是WebSocket但并不是所有数据都走同一条通道。我观察过不少直播类App的实际表现包括我自己参与过的项目通常会有两条通道并存。一条是HTTP短连接接口负责基础数据的拉取例如进入直播间时先请求一次当前房间的基础信息包括主播信息、房间状态、初始人气值、推荐位列表。另一条是WebSocket长连接负责后续所有增量数据推送比如有人进入的提示条、人气数字变更、评论消息、礼物特效事件。为什么要分两条通道因为HTTP请求天然适合“一次性拉取全量状态”的场景而WebSocket适合“持续接收增量事件”。你要是把所有数据都塞进长连接客户端冷启动时会有一大段空白期体验很差。所以常规做法是先用HTTP把快照拿到再用长连接做增量同步。2.2 轮询和长连接的选择逻辑很多人好奇为什么不用更简单的定时轮询。早期直播平台确实用过大概2秒到5秒拉一次在线人数。这个方案的缺点是人数少的时候浪费请求人数多的时候数据还是不够实时同时服务器压力巨大。后来演进成WebSocket原因不只是实时性还有一个经济学考量HTTP轮询每个请求都要走完整的TCP握手、TLS握手、HTTP头解析即使开启keep-alive也要处理大量闲置连接。而WebSocket只需要一次握手后续都通过同一个TCP连接传输帧数据非常适合直播这类高频率消息场景。推送通道还有一个设计细节服务端不会把每一次人气变化都单独推一条消息那样消息量太大了。一般会做批量聚合服务端每1到2秒算一次最新值然后通过一条消息发出去消息里可能包含人气值、在线人数、互动次数等多个字段。这就是为什么你在Wireshark里看直播App的流量WebSocket帧每隔一两秒才出现一条而不是每毫秒都在跳。2.3 消息格式与增量更新直播间消息格式各个平台大同小异基本逃不出几个套路一个命令字cmd或type一个数据体data一个序列号seq有时还带一个时间戳。人气更新这类消息的data体里通常有当前人气值、在线人数、涨粉数、点赞数等。增量更新是这个链路里比较讲究的部分。一个直播间的人气数字如果每次都推送全量值客户端倒是简单但服务端要承担很大的序列化开销。更重要的是如果用户断线重连推送的是全量值还是增量值决定了对账逻辑的复杂度。我建议的做法是服务端推送增量事件同时保留全量快照接口。客户端正常在线时只处理增量断线重连后先拉一次快照再恢复增量推送。这样做的好处是重连期间错过的消息不会导致客户端状态永久错位。增量事件通常携带一个单调递增的seq客户端可以拿它做乱序判断和去重。你如果自己设计这套协议seq一定不要用时间戳因为同一毫秒内可能有多条消息时间戳无法保证顺序。2.4 前端如何让右上角数字滚动协议层拿到人气值后前端还有一个让人忽略的工作数字不能一下子跳变。你看足球比赛的观赛人数往往是平滑地往上滚很少突然掉一半因为有动画插值。前端编程里拿到新值不直接setState而是把新旧值之间的差值分摊到几百毫秒内用requestAnimationFrame逐帧更新。这样用户看到的是一段自然滚动而不是生硬的数字跳变。中间还涉及防抖。如果服务端一秒内推来3个人气变更消息前端可能只取最后一条做动画中间的过渡过程靠自己补间。这个策略反过来也会影响你对服务端协议的判断——你抓包看到手机上人气数字在滚动不代表服务端每秒推了那么多次很可能是前端自己在补动画。3. 人气数字是怎么被算出来的算法与风控3.1 核心指标实时热度、并发UV、平均停留说完协议侧再看服务端的算法。我参与过直播业务后端的项目可以说人气值算法没有一个统一标准但核心指标基本围绕三个维度实时热度、并发UV、平均停留时长。实时热度是一个综合分不仅看当前在线人数还要看单位时间内的互动量。评论区刷得飞快的直播间热度一定比人数相同但没人说话的直播间高。并发UV指的是单位时间内的独立用户数这个指标能反映直播间拉新能力。平均停留时长则是反映内容质量的关键停留时间越长说明内容越吸引人。这三个指标会按不同的权重合成一个分数再经过归一化处理映射到展示给人看的数字区间。权重并不是固定的平台会按直播类目、时间段、主播等级动态调整。比如游戏直播更看重弹幕互动颜值直播更看重在线时长带货直播更看重点击购物车的转化行为。3.2 加权与校准如果只是把在线人数直接展示绝大多数直播间的数字会显得冷冰冰的产品团队通常会给数字加一个“温度系数”。举个实际例子某直播间真实并发UV是1万但平台展示出来是1.3万多出来的部分就是通过互动行为加权的热度增量。加权方案里有一个常用模型基础权重加行为加成。每个进入直播间的用户计1个基础人气产生评论加若干人气点赞、送礼、分享分别再加不同权重。这个模型的好处是能引导主播去引导互动坏处是容易被刷量所以必须有风控。校准这个动作也很关键。由于展示值经过缩放和加权它和后台真实数据不是完全对应的。业务方如果要拿真实数据做结算、投放、推荐不能直接读展示值必须读内部原始指标。我在项目里就遇到过运营同学拿着前端展示的数字来核对日报结果对不上折腾了半天才发现两边口径不同。3.3 降噪去重、异常流量、账号质量人气算法的另一半是降噪。直播间的流量里存在大量异常数据典型的包括同一设备短时间反复进出产生大量进房事件。机器人账号或批量注册账号挂机维持虚假在线时长。通过改机工具模拟大量设备在线。黑灰产团伙用脚本批量点赞、评论。实时计算层要做的是在窗口内对这些行为做识别和剔除。常见的做法是维护一个设备指纹和账号维度的小表记录行为频次。比如一个账号一分钟内进房超过20次或者一个设备指纹关联的账号数超过阈值就会被标记为低质流量不计入人气。去重不是只做一次而是多层。接入层做基础频率限制消息管道做设备维度的滑窗去重实时计算层再做账号质量过滤。每一层都有延迟所以最终产出的人气值往往不是实时的而是延迟几十秒甚至几分钟的“准实时值”。这也是为什么你看到的在线人数总感觉有点滞后。3.4 分片与最终一致性超大直播间的消息量会达到每秒数万条甚至更高。这时候单机计算必然扛不住需要做分片。分片键一般是直播间ID同一个直播间的所有消息进入同一个计算任务。这个任务内部管理一个滑动窗口窗口大小通常是5分钟步长1秒。分片带来一个新的问题同一个直播间的不同事件可能落到不同的消息分区如果分区策略不当顺序就乱了。通常的做法是消息队列按直播间ID做key保证同一个直播间的事件进入同一个分区这样消费的时候不会乱序。但乱序依然可能发生在客户端网络层所以协议里seq还是必须有。计算完成之后人气值会写入Redis或者内存态存储再通过推送服务推给客户端。多个计算节点同时产出数据时可能会短暂出现不同客户端看到的人气值不一样的情况。这就是最终一致性在实时系统里的体现几分钟内会收敛但做不到强一致。如果谁能做到全球所有用户看到的在线人数完全一致那他一定是牺牲了实时性。4. 想自己动手验证合规实验指南4.1 三条合规路线我知道很多人看到这个话题第一反应是想自己抓包、逆向、写脚本。我必须把话说清楚直接分析抖音的私有协议、逆向客户端签名、绕过风控属于违反平台规则甚至法律的行为我不提供这方面的帮助也不建议任何人去做。如果你确实对直播协议和算法感兴趣有合规的路线可以走阅读平台开放的API文档。抖音开放平台、微信的公众平台、电商直播的开放接口都有合法的数据接入方式能拿到部分公开数据和授权数据。自己做一套直播间服务端。用Netty或Go写一个支持WebSocket的直播间后端自己定义协议自己写人气算法这能让你把整条链路跑通。研究通用协议标准。WebSocket是RFC 6455标准的公开协议你可以抓自己服务器的包研究消息帧格式、心跳机制、关闭帧这些知识完全可以迁移到任何直播系统上。这三条路线都不碰红线又能真正学到东西。我认识的很多优秀后端工程师都是从自己写一个聊天室、一个直播间开始理解这套体系的。4.2 一个可复现的WebSocket订阅示例如果你想亲手体验“长连接推送”的感觉不需要逆向任何大厂App用公开的WebSocket服务就能试。很多免费公共服务支持你直接连接然后订阅一个频道比如一些公开的加密行情频道、公共消息广播频道。我提供一个最小示例用Python实现一个WebSocket客户端先连接服务器然后订阅一个主题并持续接收消息。这段代码用的是标准websockets库只做通用协议演示不涉及任何私有协议。import asyncio import json import websockets async def subscribe(): uri wss://your-public-websocket-service.example/ws async with websockets.connect(uri) as websocket: # 连接成功后发送订阅消息 subscribe_cmd { cmd: subscribe, topic: room_popularity_demo, seq: 1 } await websocket.send(json.dumps(subscribe_cmd)) print(已发送订阅消息) # 持续接收服务端推送 async for raw_message in websocket: message json.loads(raw_message) if message.get(topic) room_popularity_demo: popularity message.get(data, {}).get(popularity) print(f最新人气值: {popularity}) # 收到一定数量后主动退出方便测试 # 这里只是演示实际场景会一直循环接收 asyncio.run(subscribe())这段代码的核心逻辑很简单建立连接发送一条订阅消息然后进入接收循环。真实的直播间客户端逻辑其实也是这样只是连接鉴权、签名、心跳、业务字段复杂很多。4.3 重连、心跳与消息幂等自己写客户端时会遇到三个经典问题这三个问题在真实生产环境里一个都躲不掉。第一个是心跳。WebSocket连接长时间没有数据传输中间网络设备可能会回收连接所以客户端必须定时发送心跳包典型的间隔是30秒到60秒。心跳包可以是ping帧也可以是自定义的业务心跳消息。服务器的职责是维护一个超时时间超过时间没收到心跳就主动断开。第二个是重连。长连接断线是常态客户端要准备指数退避重连从1秒开始翻倍到2秒、4秒、8秒上限通常设在30秒或60秒。如果断线是因为服务端发布重启重连还要带一个随机的抖动防止所有客户端同时重连把服务器打崩。第三个是消息幂等。网络重传、重连恢复都可能让你收到重复消息。客户端处理消息时要么带上seq去重要么保证处理逻辑本身是幂等的。比如收到人气更新直接覆盖当前值就是幂等的而如果是累加操作重复收到就会出问题。设计协议时就要分清哪些消息是幂等的哪些不是。4.4 不建议碰的东西和红线这个部分我写得很直白不要去尝试破解客户端的签名算法不要尝试模拟客户端的加密握手不要批量注册账号去拉所谓的人气接口不要做刷量外挂更不要把这些技术用在外挂和灰产上。这类行为轻则封号重则承担法律责任。做技术研究时有一个判断红线的方法如果这个操作需要主动规避平台的安全机制才能完成那它就不是技术问题了。真正的协议研究应该是基于公开文档、开放接口和自己架设的服务而不是踩在别人的生产系统上做实验。我自己在带团队时也反复强调看抖音的技术分享、学习他们的架构思想完全没问题但别动“逆向私有协议”的念头。写代码之前先想清楚合法性这是高级工程师和脚本小子的根本区别。5. 实操中的坑位记录5.1 线上消息乱序我在自研直播间推送系统时踩过最深的坑就是消息乱序。最开始消息全部进一个Kafka主题按时间戳排序表面看起来没问题但高峰期一上来消息在多个消费实例之间分发顺序就乱了。用户会看到一种诡异的现象有人进入直播间的提示条出现得比离开提示条还晚人气数字倒着跳。排查到最后发现问题出在分区策略上。Kafka只保证单个分区内的顺序如果消息没有按直播间ID做分区key顺序就无法保证。当时的修复方案是消息producer端强制按roomId路由consumer端单线程消费同一个roomId的消息。这个教训我现在还一直用任何要求顺序的实时功能第一件事就是确认消息队列的分区策略。乱序问题还延伸出另一个设计原则协议字段里凡是涉及顺序的都不要依赖时间戳因为客户端时钟和服务端时钟本来就不同步。我用单调递增的seq解决客户端缓存上一个seq发现新消息的seq比缓存小直接丢弃或者重新请求快照。5.2 客户端刷新频率陷阱做前端展示时我也吃过刷新频率的亏。后端一开始推人气值的频率是每秒一次我们觉得挺合理结果压测时发现某个大直播间有几十万在线每秒推一次全量消息给所有客户端推送通道的带宽开销非常吓人。后来做了两件事。第一服务端按房间聚合推送1分钟内的人气变化合并成几条消息下发而不是每次都推。第二客户端加入节流就算服务端每秒都推了渲染层最多500毫秒更新一次UI多余的变更只作为最终值的参考。这里有个技术细节服务端减少推送频率不代表用户看到的人气数字就更卡因为客户端可以做动画插值。你完全可以每2秒推一次客户端通过缓动函数把数字滚动的过程铺满这2秒视觉上很流畅。所以协议设计的核心不只是降低延迟还要控制消息量和客户端渲染开销。5.3 字段变化的灰度期有一次我们调整了消息体里的字段定义把原先的online_num改成了popularity客户端和对应的解析逻辑也都改了。但发布之后发现部分直播间的人气数字全部变成0查了半天才发现是灰度发布导致的字段不匹配——有些旧客户端还在用旧字段名解析服务端却已经开始推新字段。这件事之后我定了一个规矩涉及协议字段的变更必须做兼容设计。新老字段并行推送至少一个版本周期客户端先兼容读取新字段等旧客户端占比低于阈值再删除旧字段。哪怕是小字段的改名也要当作一次完整的协议变更来对待。真实生产环境里协议不是你改完就结束的而是要等到所有客户端都升级完才算结束。灰度期间最容易出现的问题就是客户端和服务端版本不匹配所以协议版本号字段最好一开始就设计好不要等出了事故再补。5.4 和后端对账的思路最后分享一个对账经验。直播间的展示人气值和后台统计报表里的数据经常对不上运营每次都来问是不是系统出bug了。绝大多数时候都不是bug而是口径不同。展示值经过加权、缩放、平滑和延迟处理后台报表里的数据则是全量的、去重后的、无缩放的真实指标。对账时不能直接把两个数字放一起比要先对齐时间窗口、用户口径、去重规则和缩放比例。我给团队的建议是所有经过加工后下发给前端的数字都要保留一份原始终端埋点日志记录下发值、下发时间、对应的事件流窗口。这样一旦运营质疑数据就能快速定位是哪一层加工导致的偏差是权重问题还是风控触发而不是互相甩锅。踩过几次坑之后我的体会是这类实时系统的难点不在某一个单一技术上而在链路太长每一环的偏差都会被放大。做协议也好做算法也好先把数据链路的对账机制想清楚能省掉后面90%的排查时间。如果你也打算自研直播间的实时数据系统我的建议是先把WebSocket的标准流程吃透再考虑复杂的算法基础协议这块稳了后面才有得谈。