用 base64 压状态:2C2G 机器上的实时同步

用 base64 压状态:2C2G 机器上的实时同步

实时协作类应用最难的不是逻辑,是带宽。当上行只有 3Mbps、单帧状态又有几十个对象时,JSON 会把你压死——不是性能,是流量。

第一步:把 JSON 去掉

每帧的字段名完全可以省略。约定一个固定顺序,用数组代替对象:

// 之前:每帧 240 字节
{ "players": [{ "id": "a1", "x": 12, "y": 33, "len": 7 }] }

// 之后:每帧 14 字节
[1, 12, 33, 7]

字段名在协议里只出现一次,之后都是位置约定。这一项直接砍掉 90% 的体积。

第二步:只发变化

全量推送没有必要。客户端保留上一帧,服务端只发 diff:

def diff(prev, cur):
    out = []
    for pid, obj in cur.items():
        if prev.get(pid) != obj:
            out.append(pack(pid, obj))
    return out

安静的时候,一帧就是几个字节。热闹的时候才会涨,但热闹的时候用户注意力也在别处,掉几帧没人会发现。

第三步:二进制 + base64

SSE 是文本协议,二进制要先转 base64。看起来会膨胀 33%,但因为前面已经砍掉了 90%,最终结果仍然远小于原始 JSON。

方案单帧10 人 20fps
原始 JSON2.4 KB480 KB/s
数组化 JSON210 B42 KB/s
二进制 + base6496 B19 KB/s

只要协议是你自己定的,压缩永远有空间。真正的成本不在带宽,在于你愿不愿意写一个自定义的编解码器。

客户端插值

服务端 20fps,屏幕 60fps,中间的空档靠客户端补。简单线性插值加一点缓动,肉眼完全看不出差别,但服务端压力降到三分之一。

const t = (now - lastFrameAt) / FRAME_MS;
render(lerp(prevState, nextState, Math.min(t, 1)));

整个方案跑在两核机器上,CPU 常年在 8% 以下。省下来的不是钱,是那种"够用"的踏实感。

收到1条评论
avatar
阿岚 13 小时前
base64 那 33% 膨胀我一直以为不能接受,看来是前面的压缩够狠才行。