01 - 从零开发一个微信,私有协议设计

656 阅读4分钟

1.协议格式的选择

常见的tcp协议格式有文本协议和二进制协议

  • 文本协议 由一串ACSII字符组成的数据,文本协议容易被人肉眼解读,如http协议
  • 二进制协议 就是一串的字节流,结构比较紧凑。通常会包含一个固定长度的包头和可变长度的包体。二进制协议由于是字节流,可读性较差。由于比较紧凑的特点天生占用较小空间。

通过对比我们可以发现,虽然二进制的开发难度要高一些,可正是这些特点提高了协议的破解的门槛。同时由于该通信协议用在移动端,我们要考虑相同的数据包占用的流量大小。
相比于文本协议来说,二进制协议是紧凑的字节流,没有如xml、json冗余的表达格式的符号,占用流量相对也会少很多

2.包含的内容

我们分析一下自定义的协议应该包括哪些内容

  • 为了解决tcp粘包的问题,需要有一个表示包大小的字段packet length
  • 作为与客户端交互的通信协议,不可避免现在设计的这套协议在将来时间不在适用,需要考虑协议升级的过度阶段不影响用户的使用。基于这种考虑我们需要一个表达版本号的字段 version 用作协议升级时的兼容处理
  • 在与客户端交互中,某些场景下可能要对数据包做压缩而有的场景可能不需要。除了压缩标记以外,可能还会用到表达其他含义的标记。为了更好的扩展我们需要预留一部分空间,但又不能因为未发生的事情无端浪费客户端流量 我们可以用一个short字段bitset的 16个bit位来表达不同场景的标记枚举集合

除了以上协议中的基础字段外,还需要表达ackcmd消息id等相关信息,下面我们看下一个协议的完整结构

3.结构定义

3.1 用户消息包结构


数据包整体结构
|--------|----------------------------packet--------------------------------|
|pkg len |   version   |   flag set   | header lth   | header     | payload |
|--------|-------------|--------------|--------------|------------|---------|
|    4   |       2     |       2      |       4      |    bytes   | bytes   |
|--------|-------------|--------------|--------------|------------|---------|

public class ZeroMessage {
    private int packetLength;
    private short version;
    private short flagSet;
    private int headerLength;
    private ZeroHeader header;
    private ByteBuf payload;
}

下面解释一下每个字段的含义

packetLength : 数据包的大小 int类型 (4 byte)
version : 协议的版本 short类型 (2 byte)
flagSet : 用来表达标记字段的集合 short类型 2byte 16个bit , 每个bit表达不同的含义 具体见下文
headerLength : header的大小 int类型 4byte
header : 消息头,包括指令类型等 详细结构见下文
payload : 消息体,与业务相关 具体结构信息与业务关联


flagSet 包结构
 |----------------flag set---------------|
 |  unused  |   encrypt    | compressed  |
 |----------|--------------|-------------|
 | 14bit    |    1bit      |    1bit     |
 |----------|--------------|-------------|

unused : 预留位
encrypt: 加密标记
compressed : 压缩标记


header 结构

为了方便的阅读header的结构,header默认采用json作为序列化协议

public class ZeroHeader {
    private String from;
    private String to;
    private String uuid;
    private int cmdCode;
    private Integer seqId;
    private Integer ackId;
    private Long ts;
}

from : 消息发送方id
to : 消息接收方id
uuid: 消息id
cmdCode : 指令
seqId : 请求序列号
ackId : 回执序列号
ts: 服务端时间戳

payload 结构

payload 为消息数据的原始信息,一般会选择密文传输。服务端针对消息的原始信息,一般有几种处理方式:
1> 由于政策需要会对原始信息做敏感信息监控的,这时服务器就需要解密原始信息,需要和客户端约定好协议结构
2> 对于用户信息保密严格的应用如TG,客户端会采用e2ee的方式加密,只有接收方可以解密服务器是无法解密的。这时服务器的作用就只有消息存储和转发到目标客户端。 所以payload的结构可以是二进制结构(byte数组),服务器不做任何处理,由客户端来定义


以上即为一个im应用正常的用户交互需要用到的结构,除了满足用户之间交互外,我们还需要提供一种精简的结构用来满足客户端与服务器链接的探测。


3.2 心跳包结构

 |--------|--packet--|
 |pkg len |   num    |
 |--------|----------|
 |    4   |    1     |
 |--------|----------|

pkg len : 包的大小 int 4byte
num : 心跳序号 unsigned byte