程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

tunm二進制協議在python上的實現

編輯:Python

tunm二進制協議在python上的實現

tunm是一種對標JSON的二進制協議, 支持JSON的所有類型的動態組合

支持的數據類型

基本支持的類型 "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "varint", "float", "string", "raw", "array", "map"

為什麼我們需要二進制協議

下圖是文本格式JSON與tunm的對比

類型可讀可編輯編碼速度解碼速度數據大小預定義JSON**慢慢大否tunmxx快快小否protobufxx快快小是

在高性能的場景下, 或者需要流量傳輸比較敏感的地方, 通常會選擇二進制來代替文本協議來做為通訊的, 如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


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved