Post

ProtoBuf3定义及数据加密和解密

参考:

demo: https://github.com/h42330789/StudyProto

前言

前后端数据传输格式,从远古的xml到了现在通用的json格式,但是在IM系统中,为了数据量精简以及严格约定各个字段,使用的一般都是proto

一、proto优缺点

优点:
1、传输数据精简,数量小
2、同时前后端都使用了一份源文件生成的模型对象,proto有变更时能及时知道哪里部分有修改,也方便后端往前兼容历史版本
3、传输过程中不可读,天然的多了一道加密过程
4、可以直接使用枚举值
5、不需要自己想名字定义模型,对于起名字头疼的人尤其简单轻松
缺点:
1、传输过程汇总不可读,没法查看数据
2、由于proto严格控制数据顺序、类型,只要类型和顺序修改就会解析不了,需要写相关接口的人要充分熟悉proto的特性,否则出现了解析出错很难排查问题

二、安装proto生产swift文件的环境

1、使用命令行工具Terminal使用homebrew安装

1
brew install swift-protobuf

2、生成proto文件对应的swift文件

1
2
3
// protoc --swift_out=生成文件的文件 proto源文件地址
// 以test.proto为例
protoc --swift_out=. test.proto

三、xxx.proto文件的写法

test.proto为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
syntax = "proto3";
option java_package="com.xxx.xxxx.pb";
option java_outer_classname="TestProto";
//protoc.exe  -I=. --java_out=../src test.proto

// 客户端详情
message ClientInfo {
    string     token      = 1; // 登录时的授权token
    string     macId      = 2; // 生成的唯一号
    int32      version         = 3; // 应用版本号
    int32      language        = 4; // 系统语言
}

// 公共的返回结果
message CommonResult {
    int32 	errCode       = 1; // 错误码
    string  errMsg        = 2; // 错误内容
    string  flag          = 3; // 扩展字段
}

// 性别
enum Gender {
    Male  = 0; // 男生
    Female   = 1; // 女生
}

// 咨询列表[biz/stus/students]
message StudentsReq {
    ClientInfo     clientInfo     = 1; // 对象:客户端信息
    Gender   gender  = 2;// 枚举:性别
    repeated  int32   gradeList  = 3;// 数组:年级
    string   name  = 4;// 字符串:名称
    int32 pageNum  = 5;// 数字
    int32 pageSize = 6;// 数字
}

// 学生列表
message StudentsResp {
    CommonResult   commonResult   = 1; // 结果信息
    repeated BaseStudent students = 2;// 列表
    int32 count = 3; //
}

message BaseStudent {
    int64       stuId          = 1; // 学号
    string      name       = 2; // 名字
    string      pic         = 3; // 头像
    int32       age        = 4; // 年龄
    string      desc  = 5; // 简介
    Gender      gender  = 6;// 性别
}

使用命令行生产swift后的源码test.pb.swift如下:protoc --swift_out=. test.proto

四、Proto数据内容的加密与解密

4.0 项目里引入proto类库Podfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
target 'TestProto' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for TestProto
  pod 'SwiftProtobuf', '~> 1.18.0'

  target 'TestProtoTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'TestProtoUITests' do
    # Pods for testing
  end

end

4.1 Proto结构体变成Data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 将proto转换成data, 然后再将data当成request里的body发送到服务端
var clientInfo = ClientInfo()
clientInfo.language = 2
clientInfo.version = 122
clientInfo.macID = "xxxx"
clientInfo.token = "yyyyyy"

var reqMessage = StudentsReq()
reqMessage.clientInfo = clientInfo
reqMessage.gender = .male
reqMessage.grade = 2
reqMessage.pageNum = 1
reqMessage.pageSize = 30

if let postData = try? reqMessage.serializedData() {
    var request = URLRequest(url: URL(string: "https://xxx.xxx/biz/stus/students")!)
    request.httpBody = postData
    // 发起请求
    URLSession.shared.dataTask(with: request, completionHandler: {_,_,_ in
        
    })
}

1. Varint 编码每一个字节8位=(msb位 + 7位内容)

原理 Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。

Varint 中的每个字节(最后一个字节除外)都设置了最高有效位(msb),这一位表示下一个字节(8位)是否任然为本数字的内容,只有当msb为0时表示本数字结束。每个字节的低 7 位用于以 7 位组的形式存储数字的二进制补码表示,最低有效组首位。\

最高位为1代表后面7位仍然表示数字,否则为0,后面7位用原码补齐。

如果用不到 1 个字节,那么最高有效位设为 0 ,如下面这个例子,1 用一个字节就可以表示,所以 msb 为 0.

1
0000 0001

如果需要多个字节表示,msb 就应该设置为 1 。例如 300,如果用 Varint 表示的话:

1
1010 1100 0000 0010

编码方式

1)将被编码数转换为二进制表示

2)从低位到高位按照 7位 一组进行划分

3)将大端序转为小端序,即以分组为单位进行首尾顺序交换

因为 protobuf 使用是小端序,所以需要转换一下 4)给每组加上最高有效位(最后一个字节高位补0,其余各字节高位补1)组成编码后的数据。

5)最后转成 10 进制。

image

图中对数字123456进行 varint 编码:

1)123456 用二进制表示为1 11100010 01000000
2)每次从低向高取 7位 变成111 1000100 1000000
3)大端序转为小端序,即交换字节顺序变成1000000 1000100 111
4)然后加上最高有效位(即:最后一个字节高位补0,其余各字节高位补1)变成11000000 11000100 00000111
5)最后再转成 10进制,所以经过 varint 编码后 123456 占用三个字节分别为192 196 7。 解码的过程就是将字节依次取出,去掉最高有效位,因为是小端排序所以先解码的字节要放在低位,之后解码出来的二进制位继续放在之前已经解码出来的二进制的高位最后转换为10进制数完成varint编码的解码过程。

wire-type名称说明类型
0Varint可变长整型非ZigZag编码类型:int32, uint32, int64, uint64, bool, emum,ZigZag编码类型:sint32, sint64
164-bits固定8个字节大小fixed64, sfiexed64, double
2Length-delimitedLength + Bodystring, bytes, embedding message, packed repeated
532-bits固定4个字节大小fixed32, sfixed32, float

注:wire_type3-Strart Group4-End Group 的编码类型官方已经弃用,所以这里也不在介绍。

Tag实现的结果

1
2
3
[7] [6] [5] [4] [3] [2]  [1]  [0]
|<----- field ----->|<-- wire -->|
        number           type 
This post is licensed under CC BY 4.0 by the author.