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}]

  1. 那么数据将先压缩协议名cmd_test_op, 将先写下可变长度(varint)值为11占用1字节, 然后再写入cmd_test_op的utf8的字节数
  2. 接下来准备写入字符串索引区, 索引数据用到的字符串为["tunm_proto", "name"]两个字符串, 即将写入可变长度(varint)值为2占用一字节, 然后分别写入字符串tunm_proto和name两个字符串, 这样子字符串相接近有利于压缩, 且如果有相同的字符串可以更好的进行复用
  3. 接下来准备写入数据区,

    首先判断为一个数组, 写入类型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):
# 每个字节的最高位来表示有没有下一位, 若最高位为0, 则已完毕
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

tunm二进制协议在python上的实现的更多相关文章

  1. REST RPC HTTP vs 高性能二进制协议 序列化和通信协议

    edisonchou https://mp.weixin.qq.com/s/-XZXqXawR-NxJMPCeiNsmg .NET Core微服务之服务间的调用方式(REST and RPC) Edi ...

  2. Thrift的TBinaryProtocol二进制协议分析

    先上张图,说明一下thrift的二进制协议是什么东东. 报文格式编码: bool类型: 一个字节的类型,两个字节的字段编号,一个字节的值(true:1,false:0). Byte类型: 一个字节的类 ...

  3. 轻量级通信引擎StriveEngine —— C/S通信demo(2) —— 使用二进制协议 (附源码)

    在网络上,交互的双方基于TCP或UDP进行通信,通信协议的格式通常分为两类:文本消息.二进制消息. 文本协议相对简单,通常使用一个特殊的标记符作为一个消息的结束. 二进制协议,通常是由消息头(Head ...

  4. HTTP与私有二进制协议之间的区别

    简单的文本协议.二进制协议 写网络程序躲不过协议,协议其实就是定义了消息的格式,以及消息是如何交换的.协议可简单可复杂,复杂精密如TCP协议,简单奔放如HTTP的协议.这里将我所接触到的协议稍微总结一 ...

  5. C#轻量级通通讯组件StriveEngine —— C/S通信开源demo(2) —— 使用二进制协议 (附源码)

    前段时间,有几个研究ESFramework通信框架的朋友对我说,ESFramework有点庞大,对于他们目前的项目来说有点“杀鸡用牛刀”的意思,因为他们的项目不需要文件传送.不需要P2P.不存在好友关 ...

  6. arp协议分析&python编程实现arp欺骗抓图片

    arp协议分析&python编程实现arp欺骗抓图片 序 学校tcp/ip协议分析课程老师布置的任务,要求分析一种网络协议并且研究安全问题并编程实现,于是我选择了研究arp协议,并且利用pyt ...

  7. 二进制协议gob及msgpack介绍

    本文主要介绍二进制协议gob及msgpack的基本使用. 最近在写一个gin框架的session服务时遇到了一个问题,Go语言中的json包在序列化空接口存放的数字类型(整型.浮点型等)都序列化成fl ...

  8. SSL/TLS协议详解(上):密码套件,哈希,加密,密钥交换算法

    本文转载自SSL/TLS协议详解(上):密码套件,哈希,加密,密钥交换算法 导语 作为一名安全爱好者,我一向很喜欢SSL(目前是TLS)的运作原理.理解这个复杂协议的基本原理花了我好几天的时间,但只要 ...

  9. HTTP协议一次上传多个文件的方法

    如何通过HTTP协议一次上传多个文件呢?在这里有两个思路,是同一个方法的两种实现.具体程序还需自己去设计 1. 在form中设置多个文件输入框,用数组命名他们的名字,如下: < form act ...

  10. 用c++开发基于tcp协议的文件上传功能

    用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学 ...

随机推荐

  1. Sentieon | 每周文献-Tumor Sequencing-第三期

    肿瘤测序系列文章-1 标题(英文):The relationship between genetic characteristics and clinical characteristics and ...

  2. IoTOS-v1.5.3 新增 智能诊断&会话记录导出

    IoTOS v1.5.3 一.新增智能诊断 智能诊断功能: 智能诊断会根据不同上游接口能力开放提供接近官方甚至比官方更加完善的智能诊断功能. 目前还原OneLink官方智能诊断功能包括动效.诊断建议等 ...

  3. 轻松理解Java中的public、private、static和final

    一.概念 1.public和private 两个都是访问权限修饰符,用于控制外界对类内部成员的访问. public:表明对象成员是完全共有的,外界可以随意访问.用public修饰的数据成员.成员函数是 ...

  4. zabbix 中 net.if.out 值来源及persecond的计算

    使用脚本记录每秒的net.if.out值,与zabbix中的lastdata值做对比,发现对不上. #!/bin/bash dev=eth0 get_dev_net_speed() { dev_inf ...

  5. go项目实现在配置文件实现配置项统一管理

    转载请注明出处: go项目中实现配置项统一管理,实现逻辑:将 配置项整理为一个json的数据结构,并保存到go.conf文件中,然后在go项目启动main方法中加载 go.conf 文件,读取go.c ...

  6. Django:TypeError: view must be a callable or a list/tuple in the case of include().

    错误: path("uploads/(?P<path>.*)$", 'django.views.static.serve', {"document_root& ...

  7. C#性能优化-树形结构递归优化

    前言 大家好,我是wacky,最近在工作中遇到一个有趣的问题,同事反馈说WPF中有一个树形结构的集合,在加载时会直接报堆栈溢出,一直没时间(懒得)看,导致很久了也没人解决掉.于是,组长就把这个&quo ...

  8. 如何爆破js加密后的密码?

    如何爆破js加密后的密码? 1.首先burp中安装插件: https://github.com/whwlsfb/BurpCrypto 安装插件完毕后,分析进行js加密的算法. 2.分析加密过程: 找到 ...

  9. sqli-labs全通关payload

    less-1: less-2: less-3: less-4: less-5: less-6: less-7: outfile,dumpfile,load_file函数的用法如下所示: less-8: ...

  10. 【opencv】传统图像识别:hog+svm实现图像识别详解

    图像识别技术是信息时代的一门重要的技术,其产生目的是为了让计算机代替人类去处理大量的物理信息.传统图像识别技术的过程分为信息的获取.预处理.特征抽取和选择.分类器设计和分类决策.本文也是从这四点出发进 ...