技术茶馆公告

🍵 欢迎来到技术茶馆 🍵

这里是一个分享技术、交流学习的地方

技术札记 | 茶馆周刊 | 工具书签 | 作品展示

让我们一起品茗技术,共同成长

Skip to content

拍卖实时通信三件套踩坑记

介绍:一场拍卖如何把实时通信逼到极限

在线拍卖的竞价窗口只有几十秒,一旦延迟超过 300 ms,用户就会怀疑自己被“插队”。我们最早在传统 HTTP 轮询上临时起盘,结果出现以下事故:

  • 竞价广播靠短轮询,峰值时同一个商品有 2.5 万人同时刷新,后端 QPS 直接爆掉。
  • 代理服务器误把 WebSocket 流量当成普通 HTTP,导致升级失败。
  • 移动端 4G 切换 Wi-Fi 时连接大量掉线,没有任何补偿机制。

这篇文章把整个踩坑过程拆成四步:先看技术底座,再聊通讯原理,然后用真实 demo 复盘,最后给出选型和总结。

原理:四种方案在拍卖场景中的底层逻辑

WebSocket:全双工长连接,适合“竞价 + 仲裁”双向链路

  • 握手升级:浏览器先发支持 Upgrade: websocket 的 HTTP GET,服务端回 101 Switching Protocols,二者约定后复用同一条 TCP。代理兼容性好,但必须校验 Origin 防止跨站低价抢购。
  • 数据帧:头部只占 2-14 字节,Opcode 分别代表文本 (0x01)、二进制 (0x02)、关闭 (0x08)。客户端必须掩码,服务端可直接下发毫秒级推送。
  • 连接管理:心跳靠 Ping/Pong,一旦 2 个周期(例如 10 秒)无响应,我们会标记“用户离线”并推送给竞价官。主动断开用 0x08 帧,双方确认即可释放。
  • 拍卖特有坑:高并发时需要绑定用户到特定网关节点,否则同一买家在多地登录会触发粘滞太重;还需要给每帧附带消息 ID,断线后按 ID 追回。

Server-Sent Events:只推送不回写,适合围观者

  • 长连接文本流:浏览器用 EventSource 发起 GET,Accept: text/event-stream 表明希望拿到 SSE。服务端响应 Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive
  • 事件格式:每条消息由 eventiddata 组成,\n\n 结束。我们把“当前领先价”“倒计时”“围观人数”拆成多个事件;心跳只发 :\n\n 保持链路活跃。
  • 自动重连:浏览器断线后默认 3 秒重试,并带上 Last-Event-ID,服务端据此补发缺失的事件。缺点是只能传 UTF-8 文本,二进制图片要额外 Base64。
  • 拍卖特有坑EventSource 只支持 GET,无法顺便塞鉴权头,我们只能在 URL Query 中带 token,注意及时刷新;IE 全军覆没,微信内置浏览器也有版本差异,需要 fallback。

WebRTC:P2P 低延迟,适合竞拍直播与连麦

  • 信令阶段:双方先通过信令服务交换 SDP Offer/Answer 和 ICE 候选。我们把 WebSocket 作为信令通道,避免再搭一套长轮询。
  • 连通性:浏览器先尝试主机地址,再通过 STUN 获取公网映射;对称 NAT 则交给 TURN 中继。TURN 带宽烧钱,记得只给高价值专场打开。
  • 媒体与数据通道:音视频流走 RTCPeerConnection,数据通道走 RTCDataChannel。我们把“拍品高清直播”放在媒体流里,“竞价按钮状态”通过 DataChannel 做可靠传输,保证指令有序。
  • 拍卖特有坑:Safari 不支持 VP8,只能强制 H.264;P2P 成功率不足 70% 时一定要自动降级到 CDN + WebSocket 组合。

轮询:兜底策略

  • 短轮询:固定间隔发请求,延迟高、QPS 爆炸;多年前为了兼容 IE8 不得不用。
  • 长轮询:服务器 hold 住连接 30 秒,期间有消息就立即返回,否则超时再发下一次。缺点是大量“半连接”占用线程,需要用协程或异步模型才能扛住。
  • 拍卖特有坑:在极端弱网/企业内网下依然是最可靠的 fallback,但要严格限速,例如 3 秒内最多请求 3 次。

Demo:拍卖竞价页的三种实现对比

1. WebSocket:买家实时出价

ts
const socket = new WebSocket('wss://auction.example/ws?lot_id=20241127');

socket.onopen = () => {
  socket.send(JSON.stringify({ type: 'JOIN', lotId: '20241127', token }));
};

socket.onmessage = ({ data }) => {
  const payload = JSON.parse(data);
  if (payload.type === 'BID_PUSH') {
    renderBid(payload.amount, payload.bidder);
  }
};

// 心跳 + 断线重连
setInterval(() => socket.readyState === 1 && socket.send('ping'), 5000);

后端基于 wsSocket.IO,记得把 Sec-WebSocket-Key 写入日志排查代理转发问题。

2. SSE:围观用户的流式消息

js
const stream = new EventSource(`/auction/stream?lot=20241127&token=${token}`);

stream.onmessage = (evt) => {
  const lines = JSON.parse(evt.data);
  updateTicker(lines.price, lines.countdown);
};

stream.addEventListener('orderStatus', (evt) => {
  showToast(evt.data);
});

服务端用 Node.js Readable 或 Go http.Flusher 连续写入:

event: ticker
id: 1098
data: {"price":120900,"countdown":8}

3. WebRTC:主播连麦 + 低延迟截图

ts
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });
const local = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
local.getTracks().forEach((track) => pc.addTrack(track, local));

const dataChannel = pc.createDataChannel('control', { ordered: true });
dataChannel.onmessage = (evt) => syncBidButton(JSON.parse(evt.data));

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signalServer.send({ type: 'offer', sdp: offer.sdp });

TURN 账号一定要限制 IP 和流量,否则拍卖高峰期费用可以直接打穿预算。

应用场景:按角色拆分通信栈

  • 竞价核心链路:WebSocket + 心跳保活,保障买家出价和仲裁确认;必要时使用消息队列做顺序入库。
  • 围观与公告: SSE 覆盖 80% 只读用户,自动重连省掉大量状态同步逻辑;IE/内嵌 WebView 自动降级到长轮询。
  • 连麦/直播/巡检:WebRTC 做主链路,失败后切 CDN HLS + WebSocket 指令通道;对低端机提供仅音频模式。
  • 兜底策略:所有方案都要实现统一的“重连状态机”:指数退避 (1s→2s→4s→30s)、网络类型检测 (navigator.connection.effectiveType)、基于消息 ID 的补发接口。
  • 安全合规:WebSocket/SSE 必须加 Access-Control-Allow-Origin 白名单;信令通道要做鉴权签名;WebRTC 强制开启 SRTP,录制需征得用户授权。

总结:踩坑清单与选型建议

  • 别忘了握手升级的代理配置:Nginx 需要加 proxy_set_header Upgrade $http_upgrade;Connection "Upgrade",否则 502。
  • 消息 ID / Last-Event-ID 是续命神器:所有实时流都必须带偏移量,弱网重连才能补齐竞价记录。
  • 心跳与重连策略要统一:WebSocket 的 Ping/Pong、SSE 的注释帧、WebRTC 的 oniceconnectionstatechange 都要走同一套状态机,方便监控。
  • 跨域提前规划EventSource 无法加自定义头,token 只能放 URL 或 Cookie;WebSocket 要校验 Origin,TURN 则需要独立域名。
  • 分层部署:WebSocket 负责状态同步、SSE 广播只读流、WebRTC 扛大流量媒体,轮询做最终 fallback,这样既能满足功能也能控制成本。

参考资料: