去年帮一家制造企业做内部IM系统迁移的时候,遇到一个特别典型的场景:总部和三个工厂分布在不同的城市,两地之间既有专线又有公网接入,员工出差途中还得用手机连总部系统。消息服务在测试环境跑了两个月一切正常,一上生产,上海工厂那边不断报连接断开,厂长办公室的纸质对讲机都比我们的IM好用。抓包一看,问题全出在网络链路上,TLS握手卡了8秒,WebSocket连接撑不过3分钟,文件传输更是重试到怀疑人生。
这个场景几乎所有做企业IM开发的人都懂。真正让IM系统难部署的,往往不是业务逻辑和数据库,而是复杂网络环境下的三条链路:承载实时消息的WebSocket链路、保护数据安全的TLS链路、以及对带宽和稳定性要求极高的文件链路。这三条链路各管一摊,又是同一套部署架构里的三根支柱,任何一根出问题,整个产品的可用性就会被直接拉低。
1. 复杂网络环境:企业IM部署时真正要面对的"敌人"
1.1 复杂网络到底复杂在哪里
我们说的复杂网络,不是指拓扑有多花哨,而是指用户的接入环境不可控。企业IM的使用场景少说也有七八种:总部办公区是标准的园区网,有防火墙出口;分公司可能是专线也可能是普通宽带;员工出差住酒店,Wi-Fi做了一层又一层NAT;在高铁上切换基站,IP随时变;还有客户在生产环境里要求所有流量必须走自己的HTTP代理出去。
这些场景叠加下来,会稳定遇到四类典型网络问题:
- 长连接被中间设备静默回收。NAT设备或防火墙对空闲连接有超时机制,默认可能从60秒到5分钟不等,一旦超过这个时间没有数据交互,连接表项就被丢掉,客户端却毫不知情,直到发下一条消息才发现连接已经死了。
- TLS握手被拦截或干扰。有些中间设备会做SSL卸载或内容检测,因为SNI识别、证书链校验不完整导致握手失败;还有老旧的终端默认只支持TLS 1.0或1.1,跟服务端的安全基线配置完全不兼容。
- 文件传输在弱网环境下频繁超时。大文件上传时一旦链路抖动,整个HTTP请求直接失败,又得从头传一遍,用户心态直接崩。
- 域名解析在不同网络环境下结果不一致。内网DNS和公网DNS对同一个域名的解析结果不同,客户端拿到错误IP后连接超时,这种问题排查起来特别费劲。
1.2 三条链路决定了IM系统的可用性天花板
把企业IM拆开看,本质上就是三件事:
- 消息链路:一条消息从A用户到B用户,客户端怎么实时收到。业界方案基本收敛到WebSocket,原因后面细说。
- 安全链路:传输过程中怎么保证数据不被窃听和篡改。这个在企业环境里尤其重要,安全合规部门会拿扫描报告来找你,TLS是唯一的事实标准。
- 文件链路:图片、附件、视频、临时文件怎么上传下载,而且要保证在弱网环境下不让用户崩溃。
这三条链路在物理层共用同一条网络,但在协议层是独立的。部署策略上我的观点很明确:消息链路走WebSocket长连接,TLS负责给这条长连接和HTTP API做加密,文件链路单独走HTTP/HTTPS短连接,用独立的域名和带宽分配。为什么不让文件也走WebSocket?因为文件传输需要分片、断点续传、进度上报,这些全是HTTP的舒适区,硬塞到WebSocket里只会把长连接的稳定性拖垮。
2. WebSocket长连接:从选型到集群部署的完整路径
2.1 为什么实时消息场景必须选WebSocket
早期IM系统有用轮询的,客户端每隔几秒问一次服务器有没有新消息。问题很明显,消息延迟高、服务器压力大。后来流行过长轮询,比轮询好一些,但本质上每次还是新建HTTP连接,在TLS握手开销面前有点惨。
WebSocket解决的核心问题是:一条TCP连接建立后,服务端可以主动往客户端推数据,这正好是IM最需要的模型。而且它走的是HTTP Upgrade机制,在传统网络设备眼里它就是一次普通的HTTP请求,兼容性比自定义TCP长连接好得多,不需要额外开放端口,也不容易被出口策略拦掉。
举一个真实数据对比:同样是1000个在线用户、每人每秒一条消息的业务模型,HTTP轮询每个用户每3秒一次空请求,光请求量就是每秒333个,其中九成以上没有任何新消息;WebSocket建立连接后,只有真正有消息时才推数据,服务端和网络的空闲开销几乎为零。
2.2 网关层的WebSocket代理配置,三个参数决定生死
绝大多数企业IM架构里,客户端不是直连后端服务,而是先经过Nginx或API网关。这个中间层是WebSocket最容易出问题的地方。
先列一个Nginx代理WebSocket的基础配置:
map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream im_ws_backend { server 192.168.10.11:8080; server 192.168.10.12:8080; keepalive 32; } server { listen 443 ssl; server_name im.example.com; ssl_certificate /etc/nginx/certs/im_fullchain.pem; ssl_certificate_key /etc/nginx/certs/im.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256; location /ws { proxy_pass http://im_ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 10s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; } }这里有几个点特别关键,我在不同项目里反复踩过:
第一,proxy_set_header Upgrade $http_upgrade;和Connection $connection_upgrade这两行是WebSocket代理的命根子。Nginx默认会把Connection头设成close,如果不显式覆盖,WebSocket握手到了后端就变成普通HTTP请求,协议升级根本完成不了。
第二,proxy_read_timeout和proxy_send_timeout在HTTP默认配置下是60秒,对WebSocket来说这就是断连大杀器。服务端和客户端如果超过60秒没有数据交互,Nginx会主动掐断这条连接。一般做法是设置成比应用层心跳周期更长的时间,比如心跳30秒一次,超时就设3600秒。
第三,upstream后面的keepalive 32很多人会漏掉。这配置在长连接场景下直接影响Nginx与后端之间的连接复用效率,不设的话,每次新WebSocket连接进来,Nginx到后端都要重新建TCP,后端节点的句柄浪费非常严重。
2.3 心跳、重连与连接状态机:网络不可靠时的自救手段
网络层再稳,也绕不过NAT超时和无线信号切换,客户端必须有心跳机制,服务端同样要有。
心跳有两种主流做法:
- 应用层心跳:客户端每隔30秒发一个自定义JSON心跳包,服务端返回pong。优点是可以顺带携带业务信息,比如客户端当前状态、最后一条消息的序号,方便服务端补推离线消息。
- WebSocket协议层Ping/Pong帧:这是协议自带的控制帧,发送方发一个Ping,接收方回Pong,在Wireshark里可以直接看到这类Frame。优点是不占业务带宽,处理逻辑简单,不用解析JSON。
我的建议是两层都做:协议层Ping/Pong负责判断TCP链路是否活着,应用层心跳负责同步业务状态。比如服务端在5分钟里没收到任何数据,就主动断开连接,让客户端走重连流程;客户端在连续3个Ping周期内没收到Pong,就触发重连。
客户端重连策略必须用指数退避。我第一次失败后等1秒重连,第二次2秒,第三次4秒,最大不超过30秒,避免服务端被重连风暴打挂。这里有个细节:重连时建议带上上次连接的服务端节点信息,让客户端尽量重连到同一个节点,节省会话重建的开销。
连接状态机要覆盖这些状态:connecting、connected、reconnecting、disconnected、closed。很多客户端只做了连接和关闭两个状态,断网切换时既没有及时进入reconnecting,也没有给UI任何反馈,用户看到的现象就是"消息一直转圈发不出去"。
2.4 连接鉴权与后端框架选型
WebSocket的鉴权经常被忽略,因为很多人觉得"反正握手就是一个HTTP GET请求"。恰恰因为它是HTTP请求,所以完全可以在握手阶段完成鉴权,一旦握手成功,后续帧就不再带鉴权信息了。
常见做法是在客户端发起WebSocket连接时,在URL上带一个短期token参数,或者在Header里带Authorization字段。服务端在握手处理器里校验token,校验失败直接返回403,客户端日志里会出现类似stream disconnected before completion: failed to send websocket request的错误。
后端框架选型上,Java技术栈我推荐Spring WebSocket或Netty。Spring WebSocket配合STOMP协议,天然支持广播、群组、设置用户属性这些IM场景,业务代码写起来快;但如果对连接数、内存占用、底层控制要求高,Netty更合适,它把握手、心跳、拆包粘包都暴露给你,出问题时能控制得更细。Go技术栈推荐gorilla/websocket或nhooyr.io/websocket,配合gin框架做HTTP层非常顺手;音频实时传输这类场景,优先考虑WebRTC over DTLS,不要硬用WebSocket承载音视频流,拥塞控制和延迟会很难看。
2.5 横向扩展:连接数天花板与会话保持方案
WebSocket是长连接,意味着每个用户会一直占着一个后端进程的文件描述符。单机内存8G、配置还算可以的服务器,撑住5万到10万连接没问题,但业务上不能只算连接的账,每条连接背后还有消息推送队列、离线消息存储、会话上下文,这些内存开销比连接本身大得多。
横向扩展时最难解决的问题是会话保持。用户A连在Node1上,用户B连在Node2上,A给B发消息,消息入了MQ,消费者把消息取出来之后怎么找到B在哪个节点?
业界有三种常见解法:
- 全局会话路由表,用Redis或etcd维护userId到节点ID的映射,消息服务查表转发。连接数大的时候,注意路由表的更新延迟,节点宕机时要能快速摘除。
- 广播式推送,消息发给所有IM节点,各节点自己判断有没有目标用户的连接。连接数少的时候简单粗暴,节点多了浪费严重,广播风暴会拖垮内网。
- 一致性哈希,按userId把用户固定映射到节点。节点扩缩容时迁移成本高,适合连接数非常稳定的场景。
大部分中小团队选第一种,Redis查一次也就零点几毫秒,性价比最高。但一定要给路由表加版本号或过期时间,不然客户端重连到新节点后,旧节点上的会话残留会导致消息重复推送。
3. TLS安全链路:版本策略、证书链与性能平衡
3.1 版本选择:从TLS 1.2/1.3到老旧客户端的兼容博弈
企业IM的TLS配置,第一件事就是别再用TLS 1.0和1.1。这两个老版本已经是公认的不安全,主流浏览器和操作系统都开始直接报错,Firefox甚至会明确提示"该网站使用了已弃用的TLS版本。请升级到TLS 1.2或1.3"。企业内网里如果还有老旧的Windows 7或IE客户端,大概率会遇到这个提示,这属于需要推动对方升级终端,而不是服务端降级去兼容。
推荐配置是直接上TLS 1.2和TLS 1.3,Nginx里可以这样写:
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 24h; ssl_session_tickets on; ssl_stapling on; ssl_stapling_verify on;禁用某些旧加密套件不是洁癖,是实打实的安全需求。CVE-2016-2183就是针对3DES和Blowfish这类分组密码算法的攻击,原理是利用生日攻击在足够多的密文样本里碰撞出密钥。安全扫描工具报这个漏洞时,修复方式就是禁用CBC模式的3DES套件,用上面配置里的GCM套件替代。
关于客户端兼容性的处理,我的建议是定一张终端支持矩阵,明确什么操作系统版本、什么浏览器版本、支持到TLS哪个版本,再根据矩阵定服务端的最低策略。遇到实在无法升级的老设备,单独划一个兼容域名走TLS 1.2,其余的流量全部强制TLS 1.3,而不是一刀切全兼容。
3.2 证书链不完整:自建服务最容易踩的坑
证书链不完整是企业自建服务最容易踩的坑。买证书时签发机构一般会给你三个文件:服务器证书、中间证书、私钥。Nginx里配置的fullchain文件需要包含服务器证书加所有中间证书,顺序不能乱。如果缺了中间证书,客户端的证书校验会失败,报"unable to verify the first certificate"。
排查这个问题的命令很简单:
openssl s_client -connect im.example.com:443 -showcerts如果输出里只有服务器证书而没有中间证书,基本可以断定就是证书链不完整。用openssl verify也能提前发现:
openssl verify -CAfile root.pem -untrusted intermediate.pem server.pem还有一类隐蔽问题:域名解析到的IP和证书里的域名不一致。比如客户端走了内网DNS,把im.example.com解析到内网负载均衡的IP,但内网LB上挂的证书是颁发给公网域名的,这一样会握手失败。排查时要确认客户端实际访问的域名和证书里的SAN(Subject Alternative Name)完全匹配。
3.3 代理层TLS终结与性能优化
企业IM架构里,TLS终结通常放在Nginx或专门的负载均衡上,内网后端服务走HTTP,避免每一跳都做加解密。这样设计的好处是证书统一管理、可以统一做HTTP/2、可以集中配置安全策略。但这个架构有几个坑必须提前规避。
第一个坑:X-Forwarded-Proto没设置。后端应用会以为请求是HTTP明文,如果代码里用它来拼URL或者做HSTS重定向,就会出现重定向死循环。
第二个坑:大量短连接加TLS会显著增加CPU消耗。TLS握手过程涉及非对称加密,开销很大。有个实际测试数据,2核4G的Nginx节点,普通HTTP短连接场景勉强能扛住,如果大量客户端频繁断开重连,每次都做完整TLS握手,CPU会直接飙到80%以上。解决办法是开启会话复用,ssl_session_cache和ssl_session_tickets要同时开,让同一个客户端后续握手复用之前的会话密钥,省掉一次完整的非对称握手。
第三个坑:HTTP/2和WebSocket的关系。HTTP/2支持多路复用,但WebSocket在HTTP/2里有专门的定义。用Nginx做TLS终结并开启HTTP/2后,客户端的连接会被协商到h2,WebSocket升级请求在大多数实现里能正常工作,但一些老版本的客户端库对h2下的WebSocket支持不完善,抓包时会看到握手一直卡在101 Switching Protocols之前。稳妥的做法是先关掉HTTP/2跑一段时间,确认客户端兼容性没问题再开。
3.4 Wireshark解密:TLS握手排错的完整套路
TLS出问题,光看两边日志往往不够,直接在客户端抓包,用Wireshark分析最有效。
抓包后先看TCP三次握手是否完成。如果SYN发出去了没人回,那就是网络层的锅,安全组没放行端口、NAT端口映射配错、或者中间设备直接丢弃了目的端口。三次握手完成后,再看TLS ClientHello。
ClientHello里重点看三样东西:
- 支持的TLS版本列表,看客户端是否带了TLS 1.3。如果客户端只支持TLS 1.0,服务端配置了1.2/1.3,握手会在ServerHello之前直接失败。
- SNI字段,看客户端申请的是哪个域名。SNI为空或错误,服务端在证书回调时就会选错证书。
- supported_groups和加密套件,如果客户端带的全是弱套件,服务端可能直接拒绝。
服务端返回ServerHello和Certificate后,Wireshark里证书那块会有认证提示。展开证书链,能看到是不是缺了中间证书。很多情况下报错显示"TLS握手失败",本质却是证书链问题。
想在Wireshark里解密TLS流量,需要拿到客户端会话主密钥。可以设置环境变量SSLKEYLOGFILE,让浏览器或OpenSSL程序把密钥日志写到文件里,然后在Wireshark的Preferences -> Protocols -> TLS里指定这个文件。自研客户端只要TLS库支持keylog回调,也能导出同样的日志。
注意,如果企业网络里有SSL卸载设备对流量做了检测,SSLKEYLOGFILE抓到的是客户端到卸载设备之间这段链路的密钥,卸载设备到服务器那段的密钥拿不到。要排查全链路,得在服务端同步抓包,或者临时绕开卸载设备做一条测试路径。
4. 文件传输链路:分片、路由与带宽控制
4.1 为什么必须把文件链路和消息链路拆开
企业IM里最容易被低估的是文件链路。用户天天用的图片、附件、聊天记录导出文件,流量远大于消息本身。如果把文件流量直接和WebSocket长连接挤在同一个域名和同一批节点上,后果就是有人在传2GB的视频时,长连接被大流量堵塞,所有人收消息都变得卡顿。
我推荐把文件链路独立出去,单独用一个域名,比如file.example.com,跟消息域名im.example.com完全分离。这样做有三个实际好处:
- 消息链路不会被文件传输拖垮。即使有人传超大文件,也只是占用文件服务的带宽,不影响别人收发消息。
- 文件服务可以单独扩容。早会高峰期文档上传下载量翻倍,只需要加文件节点,不需要动消息集群。
- 可以针对文件场景做专门的网络优化,比如对象存储、CDN、分片上传、断点续传,这些策略跟消息长连接互不干扰。
4.2 分片上传、断点续传与秒传的工程实现
设计文件上传时最容易踩的坑,是直接用HTTP POST把整个文件丢上去。在复杂网络环境里,一个200MB的文件传了一半断掉,客户端只能从头再来,用户心态直接崩溃。整理一个相对成熟的上传流程:
- 客户端先请求一个上传凭证(upload ticket),带上文件大小、哈希值(比如MD5或SHA256)、期望的分片大小。
- 服务端检查哈希值,如果已经存在相同文件,直接返回秒传成功,客户端不用再传任何数据。
- 如果没有,返回uploadId和分片列表,客户端按顺序或并发上传每个分片。
- 每个分片传完后服务端记录状态,客户端可以通过查询接口知道哪些分片已经上传过,断点续传时只补传缺失的部分。
- 全部分片上传完毕后,客户端调用complete接口,服务端合并分片并做完整性校验。
接口设计大致是这样的:
POST /upload/init -> { uploadId, chunkSize, chunkIds } POST /upload/{uploadId}/{chunkIndex} 请求体为分片二进制数据 POST /upload/complete -> { fileId, downloadUrl }分片大小要根据网络状况动态调整。公网环境下我一般建议4MB到8MB一个分片,太小了请求次数太多,太大了失败重传代价太大。企业内网可以放宽到16MB,但如果走专线或跨地域传输,最好还是保守一点。
分片并发也要控制,不建议一次性把所有分片同时传。我习惯的做法是并发4到6个分片,每个分片完成后再拉取下一个任务。这样既能把带宽用起来,又不会因为并发过高导致中间设备限流。
4.3 内网与公网混布的文件路由策略
集团型企业的IM经常是内外网混布:园区内走内网访问文件服务,移动端在外面走公网访问同一个服务。这里有个隐蔽的问题:DNS解析结果决定了用户是走内网还是走公网。
建议用DNS分流:内网DNS把file.example.com解析到内网IP,公网DNS解析到公网负载均衡。但要注意DNS缓存。用户早上在家连公网,缓存了公网IP,下午到了公司,如果DNS的TTL还没过期,他依然会连公网地址绕一大圈。解决办法是把文件域名的TTL调短到60秒,或者客户端主动做网络切换检测,检测到Wi-Fi或网段变化后立刻刷新DNS缓存。
另一个更可靠的方案是在客户端做智能链路选择。服务端下发一份配置,包含内网网段和对应的文件服务地址,客户端判断自己当前网络在内网网段就直连内网地址,否则走公网。这个方案不依赖DNS,但需要客户端配合做网络判断。
4.4 带宽限流:别让文件传输拖垮整个IM
还有一个容易被忽视的点:文件服务要限流。曾经遇到过一个客户,200个员工上班后同时上传各自的邮件归档文件,每个1GB,直接把出口带宽打满。聊天消息虽然走另一个域名,但TLS握手和文件路由共用同一个公网出口,结果消息延迟飙到十几秒。
文件服务要按用户、按IP、按全局三个维度做限流:
- 单用户上传带宽限5MB/s
- 单IP并发连接数限10
- 全局上传带宽限200MB/s
Nginx层可以用limit_conn和limit_rate做基础限制:
limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn perip 10; limit_rate 5m;更精细的限流可以在应用层或对象存储层实现。总之,文件链路必须有限流策略,否则它就是压垮整条网络的那根稻草。
4.5 安全检测与生命周期管理
企业IM的文件链路还有一道绕不开的工序:安全检测。图片和文档类附件在存储前要过一遍病毒扫描和内容敏感信息检测,不能用户传什么就直接对外可访问。对象存储加一个异步扫描队列,检测通过后回调应用层更新文件状态为可用,检测不通过直接标记违规并通知上传者。
文件生命周期管理也要提前设计,包括保留周期、自动清理策略、以及离职员工的文件权限回收。很多IM系统上线半年后,对象存储里堆了几TB没人管的临时文件,成本全是白烧的。建议在上传接口里就带上文件类型字段,比如chat_image、chat_file、avatar,按类型设置不同的保留周期和清理策略。
5. 上线后真实网络环境中的排错实录
5.1 TLS版本不匹配:老客户端与安全基线的冲突
有一次上线,运维报告某分公司的客户端全部连不上服务器。抓包显示,ClientHello里带的是TLS 1.0。查下来发现分公司有一台旧版Windows的文件共享服务器,上面跑着老版本IM客户端,系统自带的TLS协议栈默认只支持到TLS 1.0。而服务端已经按安全基线禁用了TLS 1.0和1.1。
处理办法不是回退服务端,而是推动终端升级。那台机器本身已经不支持新的TLS协议栈,继续在它上面跑企业IM客户端,安全上就是个漏洞。后来IT部门把客户端迁移到了新的虚拟机,问题彻底解决。
这个案例说明,TLS版本策略不是纯技术决策,它还牵动企业资产盘点。上线前就应该把客户端支持矩阵列清楚,什么系统、什么版本、支持到TLS几,然后按矩阵定TLS策略。
5.2 WebSocket 1006断连:NAT超时与心跳周期怎么拉扯
WebSocket的1006是异常关闭码,客户端日志里看到[websocket] onclose, code: 1006, reason:, reconnect: true,基本可以确定连接是被网络中间层强制断开的,不是正常关闭。
完整的排查链路是这样走的:
第一步,先看断连时间是否有规律。我们当时发现断连时间非常规律,每3分钟断一次,基本锁定是NAT会话超时。
第二步,抓包确认。客户端发出的Ping帧中间设备没有响应,TCP层也没有RST,就是被静默丢弃了。
第三步,对比服务端日志。服务端在断开前几分钟内确实收到过Ping,说明链路确实活着,但中间设备认为这条连接空闲太久,把映射表项清掉了。
根因是心跳周期和NAT超时之间没有对齐。当时客户端心跳设置为180秒,而中间设备的TCP会话超时只有120秒。解决办法是把心跳改为30秒,并根据不同网络场景做自适应。还有一个细节:网络切换时,比如Wi-Fi切4G,IP会变,老连接必然断,客户端必须等物理层网络恢复后主动重连,而不是傻等Pong超时。
5.3 文件上传卡在99%:MTU不一致的排查链路
有一次客户反馈,同一个文件在分公司内部传输没问题,但从总部上传到分公司的文件服务就卡在99%不结束。
排查过程:
第一步看网络抓包,发现重传包非常多。TCP重传意味着链路丢包或者MTU不一致。
第二步查两端MTU。总部的出口MTU是1500,分公司那条出口链路走的是一条SD-WAN线路,封装头占掉了100多字节,实际可用MTU降到1400。而链路没有开启PMTUD,导致大于1400的IP包被静默丢弃。
第三步看应用层日志,客户端显示99%是因为最后一个分片一直没收到服务端的确认。
解决办法有两层:应用层上,把客户端上传分片大小动态下调到2MB,并在分片上传前先发一个探测包检测当前链路的最大可用大小,避免大包被丢弃;网络层上,建议网络团队在SD-WAN设备上开启PMTUD或者做TCP MSS钳制。这个问题让我们意识到,文件链路必须有独立的MTU探测和分片自适应机制,不能拿内网直连的参数直接套用到跨地域链路上。
5.4 H5正常但打包App连接不了:客户端差异排查
还有一个出现频率很高的问题:同样的WebSocket服务,在H5页面里连接正常,打包成App后就死活连不上。这类问题通常不在服务端,而在客户端的原生网络栈上。
常见原因有三个:
- Android 9及以上默认禁止明文流量,如果App里的WebSocket地址用的不是wss而是ws,或者TLS证书校验失败,连接会被系统直接拦掉。
- 原生App的TLS证书校验策略通常比浏览器更严格,中间设备做了SSL卸载但卸载证书没有安装到App信任链时,浏览器因为信任系统根证书而正常,App则会直接拒绝。
- 部分App打包框架对HTTP头处理不完整,导致Origin头或者Upgrade头没有传全,服务端校验Origin时直接拒绝握手。
排查这类问题,要同时抓App端和H5端的包做对比,重点看ClientHello里的证书信息、扩展字段和HTTP头差异。不是所有的"打包后连不上"都是服务端问题,先怀疑客户端网络栈,再用抓包说话。
6. 部署前后的一些务实经验
6.1 上线前做一轮网络体检
企业IM上线前,强烈建议做一轮网络体检,拿一份标准清单去要求IT和网络部门配合验证:
- 关键域名解析是否正常,TTL是否合理,内网和公网解析结果是否符合预期。
- 443端口对客户端是否可达,TLS握手耗时是否在500ms以内。
- 是否有中间设备做SSL卸载,卸载后证书链是否完整,客户端是否信任对应根证书。
- 现有NAT会话超时是多长,倒推客户端心跳周期。
- 大文件传输链路的MTU是否一致,专线或SD-WAN线路的实际可用包大小能不能撑住分片。
这些问题上线前发现,改配置就行;上线后发现,改配置的同时还要背故障责任。
6.2 三套监控指标,分位数比平均值更重要
监控体系要按三条链路分别设计指标:
WebSocket链路:活跃连接数、每秒新连接数、每秒断连数、1006事件频率、连接建立耗时、消息推送延迟。
TLS链路:握手失败率、握手耗时、会话复用率、证书过期剩余天数、弱加密套件使用比例。
文件链路:上传成功率、平均上传速度、分片重传率、断点续传恢复成功率、配额使用率。
这些指标不能只看平均值,要看P95和P99分位数。平均上传速度会被几个万兆专线用户拉得很高,P95分位数才是真实用户体验的反映。告警阈值要按分位数去设定,比如P95握手耗时超过800ms就告警,而不是等到平均值异常才发现问题。
6.3 灰度发布与快速回滚
任何部署都要有灰度思路。见过太多团队把所有用户一把迁到新IM,结果出问题后全员失联、业务停摆。建议按这个节奏走:
- 先让IT部和行政部这类内测用户上,他们既是真实用户又是问题反馈的快速通道。
- 再按分公司灰度,每个分公司用一到两周观察,重点看断连率、消息延迟和文件上传成功率。
- 线上配置全部走配置中心,TLS策略、心跳周期、分片大小随时可调,不需要发版本。
- 设一个一键回滚机制,把客户端域名重新指向旧集群。DNS TTL要提前调短到60秒,否则出了问题想回滚,DNS缓存不生效,回滚等于没做。
最后再分享一个我个人的体会:复杂网络环境下做企业IM,永远不要相信测试环境的网络是可靠的,也永远不要假设用户所在网络是"正常"的。把WebSocket、TLS、文件链路当成三个独立的系统去设计、去监控、去做降级预案,上线后才不会半夜被电话叫醒。