title: ProtoBuf3定义及数据加密和解密
author: 独孤流
date: 2024-05-29 01:12:00 +0800
categories: [加解密]
tags: [proto] # TAG names should always be lowercase
参考:
- swift-protobuf
- protobufjs
- 深入理解Protobuf3协议原理
- 精通protobuf原理之一:为什么要使用以及如何使用
- 精通protobuf原理之二:编码原理剖析
- 精通 protobuf 原理之三:一文彻底搞懂反射原理
- protobuf通信协议(proto2)
- protobuf教程(二)---核心编码原理
- Protobuf的高效编码
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安装
brew install swift-protobuf
2、生成proto文件对应的swift文件
// protoc --swift_out=生成文件的文件 proto源文件地址
// 以test.proto为例
protoc --swift_out=. test.proto
三、xxx.proto文件的写法
以test.proto
为例
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
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
// 将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.
0000 0001
如果需要多个字节表示,msb 就应该设置为 1 。例如 300,如果用 Varint 表示的话:
1010 1100 0000 0010
编码方式
1)将被编码数转换为二进制表示
2)从低位到高位按照 7位 一组进行划分
3)将大端序转为小端序,即以分组为单位进行首尾顺序交换
因为 protobuf 使用是小端序,所以需要转换一下
4)给每组加上最高有效位(最后一个字节高位补0,其余各字节高位补1)组成编码后的数据。
5)最后转成 10 进制。
[图片上传失败...(image-ba6ebe-1717428886556)]
图中对数字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 | 名称 | 说明 | 类型 |
---|---|---|---|
0 | Varint | 可变长整型 | 非ZigZag编码类型:int32, uint32, int64, uint64, bool, emum,ZigZag编码类型:sint32, sint64 |
1 | 64-bits | 固定8个字节大小 | fixed64, sfiexed64, double |
2 | Length-delimited | Length + Body | string, bytes, embedding message, packed repeated |
5 | 32-bits | 固定4个字节大小 | fixed32, sfixed32, float |
注:
wire_type
为3-Strart Group
、4-End Group
的编码类型官方已经弃用,所以这里也不在介绍。
Tag实现的结果
[7] [6] [5] [4] [3] [2] [1] [0]
|<----- field ----->|<-- wire -->|
number type