如何保证消息可靠传递

什么是 ACK

有了 TCP 协议本身的 ACK 机制为什么还需要业务层的 ACK 机制?

tcp的ack只能保证连接层的“不丢数据”和“不乱序”,但数据在接收时给到应用层的时候还可能出现丢失的情况。 解决丢失的方案:业务层 ACK 机制 我们先解释一下 ACK,ACK 全称 Acknowledge,是确认的意思。在 TCP 协议中,默认提供了 ACK 机制,通过一个协议自带的标准的 ACK 数据包,来对通信方接收的数据进行确认,告知通信发送方已经确认成功接收了数据。

服务端发消息给客户端,没收到ack重试有最大次数,重试多次仍然失败服务端可以主动断连来避免资源消耗。

image-20210817133502953

ACK 机制中的消息重传

如果消息推给用户 B 的过程中丢失了怎么办?比如:

  1. 网络实际已经不可达,但 IM 服务器还没有感知到;
  2. 用户 B 的设备还没从内核缓冲区取完数据就崩溃了;
  3. 消息在中间网络途中被某些中间设备丢掉了,TCP 层还一直重传不成功等。

解决这个问题的常用策略其实也是参考了 TCP 协议的重传机制

解决这个问题的常用策略其实也是参考了 TCP 协议的重传机制。类似的,IM 服务器的“等待 ACK 队列”一般都会维护一个超时计时器,一定时间内如果没有收到用户 B 回的 ACK 包,会从“等待 ACK 队列”中重新取出那条消息进行重推。

sequenceID 的作用

由于网络的问题,可能会出现数据的重复推送

  1. 对于推送的消息,如果在一定时间内没有收到 ACK 包,就会触发服务端的重传。收不到 ACK 的情况有两种,除了推送的消息真正丢失导致用户 B 不回 ACK 外,还可能是用户 B 回的 ACK 包本身丢了。

  2. 对于第二种情况,ACK 包丢失导致的服务端重传,可能会让接收方收到重复推送的消息。

解决方法:

服务端推送消息时携带一个 Sequence ID,Sequence ID 在本次连接会话中需要唯一,针对同一条重推的消息 Sequence ID 不变,接收方根据这个唯一的 Sequence ID 来进行业务层的去重,这样经过去重后,对于用户 B 来说,看到的还是接收到一条消息,不影响使用体验。

丢消息的其他坑

过“ACK+ 超时重传 + 去重”的组合机制,能解决大部分用户在线时消息推送丢失的问题,

但是 还会有其他问题【消息完整性】。

假设一台 IM 服务器在推送出消息后,由于硬件原因宕机了,这种情况下,如果这条消息真的丢了,由于负责的 IM 服务器宕机了无法触发重传,导致接收方 B 收不到这条消息。当用户 B 再次重连上线后,可能并不知道之前有一条消息丢失的情况。对于这种重传失效的情况该如何处理?

这种问题,我们称为 消息完整性问题

消息完整性检查

针对服务器宕机可能导致的重传失效的问题我们来分析一下,这里的问题在于:服务器机器宕机,重传这条路走不通了。

那如果在用户 B 在重新上线时,让服务端有能力进行完整性检查,发现用户 B“有消息丢失”的情况,就可以重新同步或者修复丢失的数据。

比较常见的消息完整性检查的实现机制有“时间戳比对”,具体的实现如下图:

image-20210817141342824

下面我们来看一下“时间戳机制是如何对消息进行完整性检查的,我用这个例子来解释一下这个过程。

  • IM 服务器给接收方 B 推送 msg1,顺便带上一个最新的时间戳 timestamp1,接收方 B 收到 msg1 后,更新本地最新消息的时间戳为 timestamp1。
  • IM 服务器推送第二条消息 msg2,带上一个当前最新的时间戳 timestamp2,msg2 在推送过程中由于某种原因接收方 B 和 IM 服务器连接断开,导致 msg2 没有成功送达到接收方 B。
  • 用户 B 重新连上线,携带本地最新的时间戳 timestamp1,IM 服务器将用户 B 暂存的消息中时间戳大于 timestamp1 的所有消息返回给用户 B,其中就包括之前没有成功的 msg2。
  • 用户 B 收到 msg2 后,更新本地最新消息的时间戳为 timestamp2。

通过上面的时间戳机制,用户 B 可以成功地让丢失的 msg2 进行补偿发送。

消息安全性

对于即时消息服务,一般都会提供一个公网的“接入服务”,作为用户消息收发的出入口,并通过域名的方式提供给客户端。对于这个出入口的访问,经常也会由于各种原因导致“访问不了”“地址错误”的问题。

比较常见的问题就是 DNS 劫持

  1. 路由器 DNS 设置被非法侵入篡改了

    • 一些家用宽带路由器,由于安全性设置不够(比如使用默认密码),导致路由器被黑客或者木马修改了,DNS 设置为恶意的 DNS 地址,这些有问题的 DNS 服务器会在你访问某些网站时返回仿冒内容,或者植入弹窗广告等
  2. 运营商 LocalDNS 可能会导致接入域名的解析被劫持。

    • LocalDNS 是部分运营商为了降低跨网流量,缓存部分域名的指向内容,把域名强行指向自己的内容缓存服务器的 IP 地址。
    • 运营商可能会修改 DNS 的 TTL(Time-To-Live,DNS 缓存时间),导致 DNS 的变更生效延迟,影响服务可用性。我们之前一个线上业务域名的 TTL 在某些省市能达到 24 小时
    • 一些小运营商为了减轻自身的资源压力,把 DNS 请求转发给其他运营商去解析,这样分配的 IP 地址可能存在跨运营商访问的问题,导致请求变慢甚至不可用。

防止 DNS 劫持的方法

  1. 对于宽带路由器的 DNS 设置被篡改的问题,一般,我们会重置一下路由器的配置,然后修改默认的路由管理登录密码,基本上都能解决
  2. 解决运营商 LocalDNS 的域名劫持和调度错误,业界比较常用的方案有 HttpDNS。HttpDNS 绕开了运营商的 LocalDNS,通过 HTTP 协议(而不是基于 UDP 的 DNS 标准协议)来直接和 DNS 服务器交互,能有效防止域名被运营商劫持的问题。

http DNS 相关的博客

由于 HttpDNS 服务器能获取到真实的用户出口 IP,所以能选择离用户更近的节点进行接入,或者一次返回多个接入 IP,让客户端通过测速等方式选择速度更快的接入 IP,因此整体上接入调度也更精准。

当然,调度精准的另一个前提是 HttpDNS 服务自身需要有比较全的 IP 库来支持。目前很多大厂也基本都支持 HttpDNS 为主,运营商 LocalDNS 为辅的模式了,像很多第三方云厂商也提供对外的 HttpDNS 解析服务。HttpDNS 的实现架构如下图:

image-20210817142005122

保证传输链路安全:TLS 传输层加密协议

DNS 解析有安全隐患【解析】,消息传输也有安全隐患【链路】

  1. 中断,攻击者破坏或者切断网络,破坏服务可用性。
  2. 截获,攻击者非法窃取传输的消息内容,属于被动攻击。
  3. 篡改,攻击者非法篡改传输的消息内容,破坏消息完整性和真实语义。
  4. 伪造,攻击者伪造正常的通讯消息来模拟正常用户或者模拟 IM 服务端。

解决网络中断问题

在即时消息系统中,对于“中断传输“这种主动攻击,破坏服务可用性的行为,一般可以采取多通道方式来提升链路可用性,比如很多 IM 系统的实现中,如果主链路连接不通或者连接不稳定,就会尝试自动切换到 failover 通道,这个 failover 通道可以是:

  1. 从 HttpDNS 服务返回的多个“接入 IP”中选择性进行切换,防止某一个“接入 IP”的中间链路被破坏。
  2. 从当前数据传输协议切换到其他传输协议,比如从基于 UDP 协议的 QUIC 协议切换到基于 TCP 协议的私有协议;或者针对 TCP 的私有协议提供 HTTP Tunnel 来对数据进行二次封装(微博目前支持这种方式),防止某些针对特定协议的中断攻击。【在tcp上做了进一步的封装】

关于消息传输过程被截获、篡改、伪造,我们则利用私有协议和 TLS 的技术,来进行防控。

对于消息传输过程中被第三方截获消息内容、消息内容被恶意篡改,以及第三方伪造 IM 服务端或者伪造客户端来获取消息或者执行恶意操作的情况,业界也有很多应对策略来进行防护。

  • 私有协议

对于采用二进制私有协议的即时消息系统本身由于编码问题天然具备一定的防窃取和防篡改的能力,相对于使用 JSON、XML、HTML 等明文传输系统,被第三方截获后在内容破解上相对成本更高,因此安全性上会更好一些。

  • TLS

消息内容加密传输也能保证被截获后无法获取到消息明文,同样也不能对加密的内容进行篡改,但问题的关键加密秘钥的协商本身需要较高的安全性保障。

比如双方约定好一个固定秘钥来进行加密,但由于客户端代码被反编译等原因,可能导致秘钥泄露;或者双方在连接建立时再协商好一个临时秘钥,但这个临时秘钥在传输上本身就可能被截获,从而导致后续的密文都能被破解。

为了解决上面一系列的安全问题,业界一般采用 TLS 协议来对业务数据进行保护,TLS 巧妙地把“对称加密算法”“非对称加密算法”“秘钥交换算法”“消息认证码算法”“数字签名证书”“CA 认证”进行结合,有效地解决了消息传输过程中的截获、篡改、伪造问题。

  1. 非对称加密算法和秘钥交换算法用于保证消息加密的密钥不被破解和泄露。
  2. 对称加密算法对消息进行加密,保证业务数据传输过程被截获后无法破解,也无法篡改消息。
  3. 数字签名和 CA 认证能验证证书持有者的公钥有效性,防止服务端身份的伪造。

账号密码存储安全:“单向散列”算法

针对账号密码的存储安全一般比较多的采用“高强度单向散列算法”(比如:SHA、MD5 算法)和每个账号独享的“盐”(这里的“盐”是一个很长的随机字符串)结合来对密码原文进行加密存储。

“单向散列”算法在非暴力破解下,很难从密文反推出密码明文,通过“加盐”进一步增加逆向破解的难度。当然,如果“密文”和“盐”都被黑客获取到,这些方式也只是提升破解成本,并不能完全保证密码的安全性。因此还需要综合从网络隔离、DB 访问权限、存储分离等多方位综合防治。【加盐加密算法】

消息内容存储安全:端到端加密

针对消息内容的存储安全,如果存储在服务端,不管消息内容的明文或者密文都存在泄漏的风险。因此保证消息内容存储安全的最好方式是:

  1. 消息内容采用“端到端加密”(E2EE),中间任何链路环节都不对消息进行解密。
  2. 消息内容不在服务端存储。

采用“端到端加密”方式进行通信,除了收发双方外,其他任何中间环节都无法获取消息原文内容,即使是研发者也做不到“破解”并且获取数据,顶多停止这种加密方式。

业界很多聊天软件如 WhatsApp、Telegram 就采用了“端到端加密”方式来保证消息内容的安全。但国内的大部分即时消息软件如 QQ、微信等由于网络安全要求,目前暂时还没有采用“端到端加密”。

“端到端加密”之所以更加安全是因为:是由于和服务端 TLS 加密不一样,“端到端加密”的通信双方各自生成秘钥对并进行公钥的交换,私钥各自保存在本地不给到 IM 服务端。发送方的消息使用接收方的公钥来进行加密,因此即使是 IM 服务端拿到了加密信息,由于没有接收方的私钥,也无法解密消息。

消息内容安全性

内容安全性主要是指针对消息内容的识别和传播的控制,比如一些恶意的链接通过即时消息下发到直播间或者群,可能会导致点击的用户被引诱到一些钓鱼网站;另外一些反政、淫秽的图片、视频等消息的传播会引起不良的负面影响,需要进行识别处置并避免二次传播。

针对消息内容的安全性一般都依托于第三方的内容识别服务来进行”风险内容“的防范。

比如下面的几种方案:

  1. 建立敏感词库,针对文字内容进行安全识别。
  2. 依托图片识别技术来对色情图片和视频、广告图片、涉政图片等进行识别处置。
  3. 使用“语音转文字”和 OCR(图片文本识别)来辅助对图片和语音的进一步挖掘识别。
  4. 通过爬虫技术来对链接内容进行进一步分析,识别“风险外链”。

一般来说,针对内容安全的识别的方式和途径很多,也有很多成熟的第三方 SaaS 服务可以接入使用。

对于 IM 服务端来说,更多要做的是要建立和“识别”配套的各种惩罚处置机制,比如:识别到群里有个别人发色情视频或者图片,可以联动针对该用户进行“禁言处理”,如果一个群里出现多人发违规视频,可以针对该群“禁止发多媒体消息”或者进行“解散群”等操作。具体处置可以根据业务需要灵活处理。

小结

即时消息中,消息安全性是各种私密社交场景的核心需求,一般可以从三个维度来对安全性进行评价。

  1. 消息传输安全性。“访问入口安全”和“传输链路安全”是基于互联网的即时消息场景下的重要防范点。针对“访问入口安全”可以通过 HttpDNS 来解决路由器被恶意篡改和运营商的 LocalDNS 问题;而 TLS 传输层加密协议是保证消息传输过程中被截获、篡改、伪造的常用手段。
  2. 消息存储安全性。针对账号密码的存储安全可以通过“高强度单向散列算法”和“加盐”机制来提升加密密码可逆性;对于追求极致安全性的即时消息场景并且政策允许的情况下,服务端应该尽量不存储消息内容,并且采用“端到端加密”方式来提供更加安全的消息传输保护。
  3. 消息内容安全性。针对消息内容的安全识别可以依托“敏感词库”“图片识别”“OCR 和语音转文字”“外链爬虫抓取分析”等多种手段,并且配合“联动惩罚处置”来进行风险识别的后置闭环。