协议设计目标
消息可达有序,不充不漏 ‼️:
- 消息及时可达
- 消息不能遗漏和重复
- 保重消息时序性:接收端收到的消息必须保证按照发送时间排序的
协议选择的标准
- 性能:协议的传输效率,尽可能低的端到端延迟
- 兼容:允许向前和向后兼容,以便在升级时不会影响现有的用户
- 存储:减少消息包的大小,降低空间占用率
- 计算:减少编码时造成的CPU使用率的权衡
- 网络:尽可能减少网络带宽的消耗
- 安全:消息安全性,防止协议被破解
- 迭代:协议的可扩展性,以便在未来支持IM复杂业务的演进
- 通用:跨平台性,支持多种开发语言和操作系统
- 透明:协议的可读性,方便调试和维护
协议的基本架构
纵向分为三层:应用层/安全层/传输层
应用层
文本协议:可读性强,方便调试和维护,性能差(xml,Json)
二进制协议:性能好,空间占用小,可读性差(Protobuf,Thrift)
1 | type MsgHeader struct { |
1 | message LoginReq { |
安全层
基于密钥的生命周期可以分为:
- TLS/SSL:加密效果好,但是证书管理相对复杂
- 固定加密:通信前客户端和服务端约定好密钥,通信时使用密钥加密,但是密钥的安全性难以保证
- 一人一密:在通信前客户端回先向服务端请求密钥,服务端会用用户特有属性生成密钥进行加密,然后进行加密通信
- 一次一密:创建链接简历一次会话时,双方进行加密三次握手,使用非堆成加密握手,对称加密传输,类似TSL握手过程
加密会消耗CPU计算资源,安全性也要考虑在服务端存储的安全性和合规性要求,要做出取舍
网关对数据包进行TLS3.0协议的密钥协商握手,加密解密操作,这会消耗大量CPU资源,所以对于加密解密操作可以使用GPU加速,提高效率

传输层
- TCP:面向连接的可靠传输写,仅能保证数据到达传输层,维护状态消耗资源,网络不稳定时频繁重连性能差。
- UDP:无状态的传输协议,弱网环境下,消息丢失率高,但是性能好,适合实时性要求高的场景

TCP 保证数据可靠传输到服务器,减少复杂度,使用epoll技术以及应用层设计,可以克服有状态链接到弊端

开源协议
IMPP 协议
特性:
- RFC2798 | RFC2799
- 这只是一个协议标准,并没有具体的实现
- 适用于IM系统的基本功能
取舍:
- 抽象模型,可读性差
XMPP 协议
特性:
- 一种基于XML应用层协议
- XML可以跨平台,跨IM服务传输
- 适用于一些邮箱的应用例如Spark
取舍:
- 文本协议性能差,信息冗余压缩率低
- 解析dom极其耗时,性能极差
- 很难保证消息可靠性
SIMPLE协议
特性:
- SIP协议RFC,应用于流媒体,音视频场景,这是对其的扩展
- 针对于IM聊天场景的扩展,类似于HTTP
取舍:
- 文本协议,压缩率低,占用网络带宽
- 没有找到直接相关的SIMPLE,SIP/SDP都需要巨大成本进行改造
- 不满足性能和可迭代性
- 难以保证消息的可靠性
MQTT 协议
特性:
- 一种基于发布/订阅模式的消息协议,适合推送场景
- 适用于物联网场景,轻量级,适合弱网环境
- 支持消息可靠性
- 代码量少,适合嵌入式设备
- 详细介绍
取舍:
- 需要增加可变头并加一些改造,才能支持时序性
- 基于IM需要定制化开发的场景很多,扩展性差
websocket 协议
特性:
- 客户端和服务端之间的仅需一次握手就可以创建链接
- 使用简单,支持全双工通信
- 复用HTTP通信,在HTTP基础上进行协议升级
- 基于TCP,支持二进制和文本数据传输
- 详细介绍
取舍:
- 需要业务层自己实现消息的时序性
- 需要业务处理断线重连等场景,扩展性差
- 简历长链接时,需要通过HTTP协议升级,简历和重连都很慢
- 数据帧格式定制化能力差,信息冗余
- 原生客户端难以扩展,需要二次开发
- websocket的协议还是字符流协议,信息压缩率差,浪费宽带差
my IM 协议设计
对于传输层,我们选择TCP协议,安全层选择TLS协议,应用层使用自研二进制协议+开元序列化协议
- TCP协议保障了消息可靠的传输到网关服务上,相对于udp来说会少很多消息丢失的情况,简化开发成本,同时我们可以在业务层实现断线重连等弱网优化手段,来应对网络不稳定的情况tcp频繁断链等情况
- TLS3.0协议,优化了握手的速度提升了性能,同时可以较好的兼顾性能和安全性是一个高性价选择,但是如果在gateway server实现,由于TLS的握手/加密都是CPU密集型操作,极端情况下会拉高gateway server的CPU使用率使其造成性能抖动,为此我们选择在L7层负载均衡器上实现TLS终止,使用L7层负载均衡器会增加一跳的数据包的转发这会造成性能损耗,不过可以使用TLS加速卡等硬件加速技术解决,对于IM场景,如果仅考虑性能等话,可以在L4负载均衡器上实现TLS终止,减少对L7负载均衡器等依赖,因为gateway server本身也工作在L7层
- 对于应用层一个简单灵活的二进制协议实现可以分为固定消息头,变长消息头,消息体三部分

Encoder pesudo code:
1 | type Message struct { |
Decoder pesudo code:
TCP是基于字节流的传输协议,所以没有物理上的消息边界,这就会导致数据包传输过程中存在下面的情况:
假设发送方发送了两个数据包p1和p2:
- p1的部分数据发送到接收端
- p1的后半部分数据和p2的前半段数据发送到接收端
- p2的后半段数据发送到了接收端
- p1和p2合并大一起发送到接收端

1 | func Accept() { |