美洽WebSocket连接原理
美洽通过在浏览器与后端之间建立WebSocket持久连接,实现双向实时通信:浏览器发起HTTP升级握手,服务端接受并切换到WebSocket协议,随后双方用帧(frame)交换消息,辅以心跳和重连策略保证可靠性,集群侧通过网关、负载均衡和消息队列做路由与伸缩,并支持TLS加密、鉴权与离线消息缓存功能。

先把大概说清楚(用最简单的话)
想象一下电话线路:HTTP是每次打电话都重新拨号,WebSocket则像装了永远接通的对讲机。一旦浏览器和美洽服务端“打通”,两端就能随时互相发话,不需要重复握手。对客服这种场景来说,优点明显:推送消息延迟低、服务器能实时感知用户、操作更顺畅。
握手到底发生了什么?(把细节拆开讲)
WebSocket连接以普通HTTP请求开始,但请求里带了特殊头部,告诉服务端“我们要升级到WebSocket”。服务端如果支持,就返回一个对应的响应,然后双方把这条TCP连接的语义从HTTP切成WebSocket。
关键步骤
- 客户端发出HTTP GET并带上Upgrade与Connection头(例如 Upgrade: websocket)。
- 客户端带上Sec-WebSocket-Key,这是随机的base64,服务端用它做应答签名(SHA1+固定GUID再base64),以证明同意升级。
- 服务器返回101 Switching Protocols,之后双方在同一条TCP连接上以WebSocket协议交换帧。
示意头部(读着容易理解)
| 客户端示例 | GET /ws HTTP/1.1 Host: api.meiqia.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 |
| 服务端示例 | HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= |
帧(frame)是什么?消息是怎么传的?
WebSocket把数据切成一块块的“帧”。每个帧都有控制位和长度信息,客户端要对负载做掩码(mask),服务器则不mask。常见的有文本帧、二进制帧、Ping/Pong与Close帧。
| 字段 | 含义 |
| FIN | 表示这是消息的最后一个帧(可用于分片) |
| Opcode | 帧类型(0=继续,1=文本,2=二进制,8=关闭,9=Ping,10=Pong) |
| Mask | 客户端必须置1并提供4字节掩码,服务器置0 |
| Payload len | 负载长度,可能是7位、7+16位或7+64位扩展 |
| Masking key | 用于对负载做按位异或 |
保持连接稳定——心跳与重连策略
网络总会波动。WebSocket本身有TCP层的保活,但在应用层通常还要做心跳(application heartbeat)。美洽类系统会同时用两种心跳:
- TCP/底层:操作系统/TCP keepalive,适合发现长时间无响应的连接。
- 应用层:发送Ping或自定义心跳消息,服务端在超时内未收到则认为连接断开并清理会话。
重连通常用指数退避(exponential backoff),并结合本地缓存的会话令牌(token)实现会话恢复,避免用户频繁丢失聊天上下文。
常见实践
- 心跳间隔:通常在15~60秒之间,视业务频繁度调整。
- 超时判定:未收到心跳的2~3倍间隔则断开并尝试重连。
- 重连策略:立刻重连+若失败按指数退避(比如1s,2s,4s,8s)并上线限频。
安全与鉴权(别忽略这些)
生产环境下一定要用TLS(wss://),并在握手或消息层做鉴权。常见办法:
- 在握手时通过带有签名的Query参数或Authorization头传token(JWT、短期签名等)。
- 用Origin头校验来防止跨站WebSocket劫持(注意并不能完全替代鉴权)。
- 对关键操作做服务端权限检查,不能只依赖客户端传来的身份。
在集群里怎么扩展?(美洽会遇到的场景)
单台服务器维持大量WebSocket连接很累,典型做法是将连接接入层(Gateway/Edge)与业务处理层分离。连接层负责握手、心跳、路由,业务层处理实际的客服逻辑。
常见架构要点
- 接入层(WebSocket网关)做短连接管理与负载均衡,通常前端用Nginx或专业网关并开启Upgrade支持。
- 接入层与业务层通过消息队列(例如Redis Pub/Sub、Kafka)或RPC通信,实现任意节点间消息路由。
- 为了保证会话落到同一客服或同一工作节点,使用一致性哈希或会话路由表(session affinity)。
- 离线消息存储:未在线时,消息进队列并持久化,用户上线后再派发。
消息可靠性:确认、去重与顺序
WebSocket是基于TCP的,传输层保证字节顺序,但应用层仍需处理:服务端重发、客户端重连后如何补消息等问题。常见模式:
- 给每条消息加上sequence或messageId,客户端确认(ACK)后服务端清除持久化副本。
- 在客户端重连时使用最后已确认的sequence向服务端请求未收消息。
- 对幂等操作做设计,避免重复消费带来副作用。
协议与格式:JSON还是二进制?
多数客服消息用JSON,便于开发与调试。但在高吞吐场景下,二进制或自定义压缩格式更省带宽。还有一些扩展:permessage-deflate(帧压缩)和自定义子协议(subprotocol)来约定消息语义。
代理、负载均衡与常见坑
- 某些中间代理不支持Upgrade或会在中途关闭连接。要确保反向代理(例如Nginx、HAProxy)显式支持WebSocket。
- 使用HTTP/2时要注意:传统WebSocket基于HTTP/1.1 Upgrade,HTTP/2并不直接支持Upgrade为WebSocket(后来有RFC的变化与实现差异)。
- 负载均衡需要会话保持(sticky session)或在业务层通过消息总线路由,否则会把同一会话分散到不同节点,导致状态不一致。
给开发者的实操建议(写给实现方)
- 握手阶段验证token并返回会话ID(sessionId),便于后续路由与审计。
- 实现心跳并在日志记录心跳丢失与重连次数,这些指标能反映网络质量。
- 把消息协议设计成包含:type、id、seq、ts、payload、optional-ack。
- 对二进制大消息做分片并用FIN配合序号重组,避免单帧过大导致内存峰值。
- 在客户端实现网络切换(Wi-Fi→4G)时的平滑重连逻辑,尽量先做会话恢复再拉取遗漏消息。
一些常见问题的快速回答(像讲给同事听)
- Q:断网后怎么保证消息不丢?
A:消息先写持久化队列并标记未确认,客户端重连后拉取未确认消息。 - Q:为什么要客户端做mask?
A:这是RFC规定,主要是为了防止代理层处理时的特定安全问题,服务器端不需要mask回复。 - Q:WebSocket能穿透防火墙吗?
A:通常基于80/443端口的WebSocket很容易通过,但有些严格企业防火墙会阻断非HTTP流量,此时需fallback或隧道。
写到这儿其实还有很多实战细节:比如在高并发下的内存模型、GC对长连接的影响、如何做灰度升级不停服切换连接,还有监控指标(连接数、每秒消息、心跳丢失率、重连率)。不过我先把主线讲清楚了,读到这里如果你想要我把某一部分(比如Nginx配置范例、消息协议JSON模版或重连算法伪码)展开,我可以继续补上——就像调着话筒慢慢把细节说完一样。