0%

消息协议设计(1)

协议设计目标

消息可达有序,不充不漏 ‼️:
  • 消息及时可达
  • 消息不能遗漏和重复
  • 保重消息时序性:接收端收到的消息必须保证按照发送时间排序的

协议选择的标准

  1. 性能:协议的传输效率,尽可能低的端到端延迟
  2. 兼容:允许向前和向后兼容,以便在升级时不会影响现有的用户
  3. 存储:减少消息包的大小,降低空间占用率
  4. 计算:减少编码时造成的CPU使用率的权衡
  5. 网络:尽可能减少网络带宽的消耗
  6. 安全:消息安全性,防止协议被破解
  7. 迭代:协议的可扩展性,以便在未来支持IM复杂业务的演进
  8. 通用:跨平台性,支持多种开发语言和操作系统
  9. 透明:协议的可读性,方便调试和维护

协议的基本架构

纵向分为三层:应用层/安全层/传输层

应用层

文本协议:可读性强,方便调试和维护,性能差(xml,Json)

二进制协议:性能好,空间占用小,可读性差(Protobuf,Thrift)

1
2
3
4
5
6
type MsgHeader struct {
Version uint32 // 版本号
Cmd uint32 // 命令号
Length uint32 // 消息长度
Data []byte // 数据
}
1
2
3
4
5
6
7
message LoginReq {
optional string username = 1;
optional string password = 2;
}
message LoginResp {
optional uint64 uid = 1;
}

安全层

基于密钥的生命周期可以分为:

  1. TLS/SSL:加密效果好,但是证书管理相对复杂
  2. 固定加密:通信前客户端和服务端约定好密钥,通信时使用密钥加密,但是密钥的安全性难以保证
  3. 一人一密:在通信前客户端回先向服务端请求密钥,服务端会用用户特有属性生成密钥进行加密,然后进行加密通信
  4. 一次一密:创建链接简历一次会话时,双方进行加密三次握手,使用非堆成加密握手,对称加密传输,类似TSL握手过程

加密会消耗CPU计算资源,安全性也要考虑在服务端存储的安全性和合规性要求,要做出取舍
网关对数据包进行TLS3.0协议的密钥协商握手,加密解密操作,这会消耗大量CPU资源,所以对于加密解密操作可以使用GPU加速,提高效率

传输层

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

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

开源协议

IMPP 协议

特性:

  1. RFC2798 | RFC2799
  2. 这只是一个协议标准,并没有具体的实现
  3. 适用于IM系统的基本功能

取舍:

  1. 抽象模型,可读性差

XMPP 协议

特性:

  1. 一种基于XML应用层协议
  2. XML可以跨平台,跨IM服务传输
  3. 适用于一些邮箱的应用例如Spark

取舍:

  1. 文本协议性能差,信息冗余压缩率低
  2. 解析dom极其耗时,性能极差
  3. 很难保证消息可靠性

SIMPLE协议

特性:

  1. SIP协议RFC,应用于流媒体,音视频场景,这是对其的扩展
  2. 针对于IM聊天场景的扩展,类似于HTTP

取舍:

  1. 文本协议,压缩率低,占用网络带宽
  2. 没有找到直接相关的SIMPLE,SIP/SDP都需要巨大成本进行改造
  3. 不满足性能和可迭代性
  4. 难以保证消息的可靠性

MQTT 协议

特性:

  1. 一种基于发布/订阅模式的消息协议,适合推送场景
  2. 适用于物联网场景,轻量级,适合弱网环境
  3. 支持消息可靠性
  4. 代码量少,适合嵌入式设备
  5. 详细介绍

取舍:

  1. 需要增加可变头并加一些改造,才能支持时序性
  2. 基于IM需要定制化开发的场景很多,扩展性差

websocket 协议

特性:

  1. 客户端和服务端之间的仅需一次握手就可以创建链接
  2. 使用简单,支持全双工通信
  3. 复用HTTP通信,在HTTP基础上进行协议升级
  4. 基于TCP,支持二进制和文本数据传输
  5. 详细介绍

取舍:

  1. 需要业务层自己实现消息的时序性
  2. 需要业务处理断线重连等场景,扩展性差
  3. 简历长链接时,需要通过HTTP协议升级,简历和重连都很慢
  4. 数据帧格式定制化能力差,信息冗余
  5. 原生客户端难以扩展,需要二次开发
  6. websocket的协议还是字符流协议,信息压缩率差,浪费宽带差

my IM 协议设计

对于传输层,我们选择TCP协议,安全层选择TLS协议,应用层使用自研二进制协议+开元序列化协议

  1. TCP协议保障了消息可靠的传输到网关服务上,相对于udp来说会少很多消息丢失的情况,简化开发成本,同时我们可以在业务层实现断线重连等弱网优化手段,来应对网络不稳定的情况tcp频繁断链等情况
  2. TLS3.0协议,优化了握手的速度提升了性能,同时可以较好的兼顾性能和安全性是一个高性价选择,但是如果在gateway server实现,由于TLS的握手/加密都是CPU密集型操作,极端情况下会拉高gateway server的CPU使用率使其造成性能抖动,为此我们选择在L7层负载均衡器上实现TLS终止,使用L7层负载均衡器会增加一跳的数据包的转发这会造成性能损耗,不过可以使用TLS加速卡等硬件加速技术解决,对于IM场景,如果仅考虑性能等话,可以在L4负载均衡器上实现TLS终止,减少对L7负载均衡器等依赖,因为gateway server本身也工作在L7层
  3. 对于应用层一个简单灵活的二进制协议实现可以分为固定消息头,变长消息头,消息体三部分

Encoder pesudo code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Message struct {
fixedHeader *FixedHeader
varHeader PBData
msgBody PBData
}
func (msg *Message) Encode() []byte {
buf := new([]byte,14)
buf[0] = msg.fixedHeader.Version
buf[1] = msg.fixedHeader.msgType
copy(buf[2:6], uint32ToBytes(msg.fixedHeader.MsgLen))
copy(buf[6:10], uint32ToBytes(msg.crc32Sum))
buf = append(buf, msg.varHeader.Bytes())
buf = append(buf, msg.msgBody.Bytes())
return buf
}
func send(data []byte) {
conn.Send(data)
....
}

Decoder pesudo code:
TCP是基于字节流的传输协议,所以没有物理上的消息边界,这就会导致数据包传输过程中存在下面的情况:
假设发送方发送了两个数据包p1和p2:

  1. p1的部分数据发送到接收端
  2. p1的后半部分数据和p2的前半段数据发送到接收端
  3. p2的后半段数据发送到了接收端
  4. p1和p2合并大一起发送到接收端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func Accept() {
buf := make([]byte, 14)
for conn.Read(buf) {
msg := &Message{}
msg.fixedHeader.Version = buf[0]
msg.fixedHeader.msgType = buf[1]
msg.fixedHeader.MsgLen = bytesToUint32(buf[2:6])
msg.crc32Sum = bytesToUint32(buf[6:10])
varHeaBuf := make([]byte, msg.fixedHeader.MsgLen)
conn.Read(varHeaBuf)
msg.varHeader = pb.Data(varHeaBuf)
bodyBuf := make([]byte, msg.fixedHeader.MsgLen)
conn.Read(bodyBuf)
msg.msgBody = pb.Data(bodyBuf)
header.Pool(msg)
}
}