tunm二进制协议在python上的实现
tunm是一种对标JSON的二进制协议, 支持JSON的所有类型的动态组合
支持的数据类型
基本支持的类型 "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "varint", "float", "string", "raw", "array", "map"
为什么我们需要二进制协议
下图是文本格式JSON与tunm的对比
类型 |
可读 |
可编辑 |
编码速度 |
解码速度 |
数据大小 |
预定义 |
JSON |
✓ |
✓ |
慢 |
慢 |
大 |
否 |
tunm |
x |
x |
快 |
快 |
小 |
否 |
protobuf |
x |
x |
快 |
快 |
小 |
是 |
在高性能的场景下, 或者需要流量传输比较敏感的地方, 通常会选择二进制来代替文本协议来做为通讯的, 如RPC, REST, 游戏等情况。
相对于google protobuf, 它需要比较完善的预定义过程, 就比如客户端版本1, 服务端版本2, 就有比较大的可能造成不兼容, 对需求经常变化的就会比较难与同步。
tunm相对于JSON, 若第一版是
| { |
| "name": "tunm", "version": 1 |
| } |
此时第二版需要加入用户的id, 就可以很方便的变成
| { |
| "name": "tunm", "version": 2, "id": 1 |
| } |
而对客户端1来说, 只是多一个id的字段, 不会有任何的破坏, 做到版本升级而无影响
协议的二进制格式
数据协议分为三部分(协议名称, 字符串索引区, 数据区(默认为数组))
如数据协议名为cmd_test_op, 数据为["tunm_proto", {"name": "tunm_proto", "tunm_proto": 1}]
- 那么数据将先压缩协议名cmd_test_op, 将先写下可变长度(varint)值为11占用1字节, 然后再写入cmd_test_op的utf8的字节数
- 接下来准备写入字符串索引区, 索引数据用到的字符串为["tunm_proto", "name"]两个字符串, 即将写入可变长度(varint)值为2占用一字节, 然后分别写入字符串tunm_proto和name两个字符串, 这样子字符串相接近有利于压缩, 且如果有相同的字符串可以更好的进行复用
- 接下来准备写入数据区,
首先判断为一个数组, 写入类型u8(TYPE_ARR=16), 写入数组长度varint(2), 准备开始写第一个数据, 字符串tunm_proto, 已转成id, 则写入类型u8(TYPE_STR_IDX=14), 查索引号0, 则写入varint(0), 第一个字段写入完毕, 接下来第二个字段是一个map数据, 写入map长度varint(2), 然后进行遍历得到key值为name, 则写入写入类型u8(TYPE_STR_IDX=14),查索引号1, 则写入varint(1), 然后开始写name对应的值tunm_proto, 写入TYPE_STR_IDX类型的0值, 则这组key写入完毕, 依此类推写入第二组数据
协议的实现(小端对齐)
ByteBuffer的实现
ByteBuffer具有组装字节流的功能, 比如写入字符串, 写入int, 还有里面存储字符串索引区
| class ByteBuffer(object): |
| def __init__(self): |
| |
| self.buffer = bytearray([00]*1024) |
| |
| self.wpos = 0 |
| |
| self.rpos = 0 |
| |
| self.endianness = "little" |
| |
| self.str_arr = [] |
| self.str_map = {} |
ByteBuffer源码地址
类型的定义
| @enum.unique |
| class TP_DATA_TYPE(IntEnum): |
| TYPE_NIL = 0, |
| TYPE_BOOL = 1, |
| TYPE_U8 = 2, |
| TYPE_I8 = 3, |
| TYPE_U16 = 4, |
| TYPE_I16 = 5, |
| TYPE_U32 = 6, |
| TYPE_I32 = 7, |
| TYPE_U64 = 8, |
| TYPE_I64 = 9, |
| TYPE_VARINT = 10, |
| TYPE_FLOAT = 11, |
| TYPE_DOUBLE = 12, |
| TYPE_STR = 13, |
| TYPE_STR_IDX = 14, |
| TYPE_RAW = 15, |
| TYPE_ARR = 16, |
| TYPE_MAP = 17, |
数据的组装
变长的int类型, 用来写入string长度, 数组长度, map长度, 部分数值类型
| @staticmethod |
| def encode_varint(buffer: ByteBuffer, value): |
| ''' |
| 如果原数值是正数则将原数值变成value*2 |
| 如果原数值是负数则将原数值变成-(value + 1) * 2 + 1 |
| 相当于0->0, -1->1, 1->2,-2->3,2->4来做处理 |
| 因为小数值是常用的, 所以保证小数值及负数的小数值尽可能的占少位 |
| ''' |
| if type(value) == bool: |
| value = 1 if value else 0 |
| real = value * 2 |
| if value < 0: |
| real = -(value + 1) * 2 + 1 |
| |
| for _i in range(12): |
| |
| b = real & 0x7F |
| real >>= 7 |
| if real > 0: |
| buffer.write_u8(b | 0x80) |
| else: |
| buffer.write_u8(b) |
| break |
写入字符串, 把字符串变成索引值, 如果协议里有大量重复的字符串可大大的节约协议的长度
| @staticmethod |
| def encode_str_idx(buffer: ByteBuffer, value): |
| ''' |
| 写入字符串索引值, 在数值区里的所有字符串默认会被写成索引值 |
| 如果重复的字符串则会返回相同的索引值(varint) |
| ''' |
| idx = buffer.add_str(value) |
| TPPacker.encode_type(buffer, TP_DATA_TYPE.TYPE_STR_IDX) |
| TPPacker.encode_varint(buffer, idx) |
写入各种对应的类型
| @staticmethod |
| def encode_field(buffer: ByteBuffer, value, pattern=None): |
| ''' |
| 先写入类型的值(u8), 则根据类型写入类型对应的的数据 |
| ''' |
| if not pattern: |
| pattern = TPPacker.get_type_by_ref(value) |
| if pattern == TP_DATA_TYPE.TYPE_NIL: |
| return None |
| elif pattern == TP_DATA_TYPE.TYPE_BOOL: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_bool(buffer, value) |
| elif pattern >= TP_DATA_TYPE.TYPE_U8 and pattern <= TP_DATA_TYPE.TYPE_I8: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_number(buffer, value, pattern) |
| elif pattern >= TP_DATA_TYPE.TYPE_U16 and pattern <= TP_DATA_TYPE.TYPE_I64: |
| TPPacker.encode_type(buffer, TP_DATA_TYPE.TYPE_VARINT) |
| TPPacker.encode_varint(buffer, value) |
| elif pattern == TP_DATA_TYPE.TYPE_FLOAT: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_number(buffer, value, pattern) |
| elif pattern == TP_DATA_TYPE.TYPE_DOUBLE: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_number(buffer, value, pattern) |
| elif pattern == TP_DATA_TYPE.TYPE_STR: |
| TPPacker.encode_str_idx(buffer, value) |
| elif pattern == TP_DATA_TYPE.TYPE_RAW: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_str_raw(buffer, value) |
| elif pattern == TP_DATA_TYPE.TYPE_ARR: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_arr(buffer, value) |
| elif pattern == TP_DATA_TYPE.TYPE_MAP: |
| TPPacker.encode_type(buffer, pattern) |
| TPPacker.encode_map(buffer, value) |
| else: |
| raise Exception("unknow type") |
| |
| @staticmethod |
| def encode_arr(buffer: ByteBuffer, value): |
| ''' |
| 写入数组的长度, 再写入各各元素的值 |
| ''' |
| TPPacker.encode_varint(buffer, len(value)) |
| for v in value: |
| TPPacker.encode_field(buffer, v) |
| |
| @staticmethod |
| def encode_map(buffer: ByteBuffer, value): |
| ''' |
| 写入map的长度, 再分别写入map各元素的key, value值 |
| ''' |
| TPPacker.encode_varint(buffer, len(value)) |
| for k in value: |
| TPPacker.encode_field(buffer, k) |
| TPPacker.encode_field(buffer, value[k]) |
写入一条协议
| @staticmethod |
| def encode_proto(buffer: ByteBuffer, name, infos): |
| ''' |
| 写入协议名称, 然后写入字符串索引区(即字符串数组), 然后再写入协议的详细数据 |
| ''' |
| sub_buffer = ByteBuffer() |
| TPPacker.encode_field(sub_buffer, infos) |
| |
| TPPacker.encode_str_raw(buffer, name, TP_DATA_TYPE.TYPE_STR) |
| TPPacker.encode_varint(buffer, len(sub_buffer.str_arr)) |
| for val in sub_buffer.str_arr: |
| TPPacker.encode_str_raw(buffer, val, TP_DATA_TYPE.TYPE_STR) |
| |
| buffer.write_bytes(sub_buffer.all_bytes()) |
解码与编码的过程相反, 类似的过程
tunm源码地址
相关连接
协议地址https://github.com/tickbh/TunmProto
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!