Files
live-hub-py/core/huya/frame_decoder.py
T

310 lines
10 KiB
Python

"""
TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出
"""
from .taf_protocol import TafInputStream, TafType
from .wup_protocol import normalize_wup_payload
# cmd 编号 → 名称
CMD_NAMES = {
0x03: "RPC_REQ", 0x04: "RPC_RSP", 0x0a: "AUTH",
0x0b: "PUSH1", 0x10: "HB_SEND", 0x11: "HB_RECV",
0x17: "CONFIRM", 0x18: "PUSH2", 0x21: "REGISTER", 0x22: "CONFIRM_RSP",
}
# TAIL_BYTES = 2c36004c5c6600
_TAIL = bytes.fromhex("2c36004c5c6600")
def _strip_tail(body: bytes) -> bytes:
"""裁掉 body 末尾的 TAIL_BYTES"""
if body.endswith(_TAIL):
return body[:-len(_TAIL)]
# 有时 TAIL 前还有 0c (ZERO tag)
if len(body) > 1 and body[-len(_TAIL)-1:-len(_TAIL)] == b'\x0c' and body.endswith(_TAIL):
return body[:-len(_TAIL)-1]
return body
def _decode_taf_value(ins: TafInputStream, dtype: int, depth: int = 0) -> object:
"""递归解码单个 TAF 值"""
if dtype == TafType.ZERO:
return 0
if dtype in (TafType.INT8, TafType.INT16, TafType.INT32, TafType.INT64):
return ins._read_int_value(dtype)
if dtype in (TafType.FLOAT, TafType.DOUBLE):
import struct as _s
if dtype == TafType.FLOAT:
return round(_s.unpack('>f', ins.buf.read(4))[0], 4)
return round(_s.unpack('>d', ins.buf.read(8))[0], 6)
if dtype == TafType.STRING1:
ln = ins.buf.read(1)[0]
return ins.buf.read(ln).decode('utf-8', errors='replace')
if dtype == TafType.STRING4:
import struct as _s
ln = _s.unpack('>I', ins.buf.read(4))[0]
return ins.buf.read(ln).decode('utf-8', errors='replace')
if dtype == TafType.MAP:
cnt = ins._read_int_len()
m = {}
for _ in range(cnt):
_, kt = ins.read_head()
k = _decode_taf_value(ins, kt, depth + 1)
_, vt = ins.read_head()
v = _decode_taf_value(ins, vt, depth + 1)
m[k] = v
return m
if dtype == TafType.LIST:
cnt = ins._read_int_len()
items = []
for _ in range(cnt):
_, it = ins.read_head()
items.append(_decode_taf_value(ins, it, depth + 1))
return items
if dtype == TafType.SIMPLE_LIST:
ins.read_head() # element type INT8
ln = ins._read_int_len()
return f"<bytes {ln}B>"
if dtype == TafType.STRUCT_BEGIN:
return _decode_taf_struct(ins, depth + 1)
return f"<0x{dtype:02x}>"
def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict:
"""解码一个 TAF 结构体 (已消费 STRUCT_BEGIN) → 有序字典"""
fields = {}
while True:
try:
tag, dtype = ins.peek_head()
except EOFError:
break
if dtype == TafType.STRUCT_END:
ins.read_head()
break
ins.read_head()
# 限制递归深度
if depth < 5:
try:
val = _decode_taf_value(ins, dtype, depth)
except Exception:
val = f"<decode_err:0x{dtype:02x}>"
else:
val = f"<...>"
try:
ins.skip_field(dtype)
except Exception:
break
key = f"tag{tag}"
if key in fields:
key = f"{key}_2"
fields[key] = val
return fields
def _extract_wup(body: bytes) -> bytes:
"""从 WSS 响应 body 中提取干净的 WUP 包"""
if len(body) < 5:
return body
# 大包格式: [1B prefix][4B wup_len][wup_body][tail]
# prefix 可能是 0x00,必须优先于 4B total_len 判断。
wup_len = int.from_bytes(body[1:5], 'big')
if 5 + wup_len <= len(body) and body[5:7] == b'\x10\x03':
return body[5:5 + wup_len]
total_len = int.from_bytes(body[0:4], 'big')
if 8 <= total_len <= len(body) and body[4:6] == b'\x10\x03':
return body[:total_len]
if 5 + wup_len <= len(body):
return body[5:5 + wup_len]
return body
def _decode_wup_body(body: bytes) -> dict:
"""解码 WUP 封装 (请求/响应通用) → {servant, func, data}"""
result = {}
wup = normalize_wup_payload(_extract_wup(body))
# 手动解析 WUP,WSS 大帧常在 tag10 后追加校验/尾部字节
try:
ins = TafInputStream(wup)
# 读 WUP 字段 tag1~tag10,读到 tag10 后停止(忽略尾部垃圾)
sBuffer = b''
while True:
try:
tag, dtype = ins.peek_head()
except EOFError:
break
# tag10 是最后一个合法 WUP 字段
if tag > 10:
break
ins.read_head()
if tag == 1:
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag in (2, 3):
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag == 4:
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag == 5:
result["servant"] = _decode_taf_value(ins, dtype)
elif tag == 6:
result["func"] = _decode_taf_value(ins, dtype)
elif tag == 7:
if dtype == TafType.SIMPLE_LIST:
ins.read_head()
ln = ins._read_int_len()
sBuffer = ins.buf.read(ln)
else:
ins.skip_field(dtype)
elif tag == 8:
if dtype != TafType.ZERO:
ins._read_int_value(dtype)
elif tag in (9, 10):
ins.skip_field(dtype)
if tag == 10:
break
else:
ins.skip_field(dtype)
# 解析 sBuffer → newdata MAP
if sBuffer:
try:
sins = TafInputStream(sBuffer)
stag, sdt = sins.read_head()
if stag == 0 and sdt == TafType.MAP:
cnt = sins._read_int_len()
for _ in range(cnt):
_, kt = sins.read_head()
k = _decode_taf_value(sins, kt)
_, vt = sins.read_head()
if vt == TafType.SIMPLE_LIST:
sins.read_head()
vln = sins._read_int_len()
v = sins.buf.read(vln)
if v:
try:
tins = TafInputStream(v)
ttag, tdt = tins.peek_head()
if tdt == TafType.STRUCT_BEGIN:
tins.read_head()
result[k] = _decode_taf_struct(tins)
else:
result[k] = f"<{len(v)}B>"
except Exception:
result[k] = f"<{len(v)}B>"
else:
result[k] = _decode_taf_value(sins, vt)
except Exception:
pass
except Exception as e:
result["err"] = str(e)
return result
def _truncate(obj, max_str=80):
"""截断过长的值,使日志紧凑"""
if isinstance(obj, str):
if len(obj) > max_str:
return obj[:max_str] + f"...({len(obj)})"
return obj
if isinstance(obj, dict):
return {k: _truncate(v, max_str) for k, v in obj.items()}
if isinstance(obj, list):
return [_truncate(v, max_str) for v in obj[:5]]
return obj
def _fmt_fields(fields: dict) -> str:
"""把字段字典格式化为一行摘要"""
parts = []
for k, v in fields.items():
if isinstance(v, dict):
parts.append(f"{k}={{...}}")
elif isinstance(v, list):
parts.append(f"{k}=[{len(v)}项]")
else:
sv = str(v)
if len(sv) > 60:
sv = sv[:60] + "..."
parts.append(f"{k}={sv}")
return ", ".join(parts)
def format_wss_log(body: bytes, cmd: int, seq: int, direction: str) -> str:
"""格式化单条 WSS 帧的日志行
Args:
body: 帧体 (不含 6B 头)
cmd: 命令字
seq: 序列号
direction: "发" 或 "收"
Returns:
可读日志字符串
"""
prefix = f"[{direction}]"
# 心跳精简
if cmd == 0x10:
return f"{prefix} HB →"
if cmd == 0x11:
return f"{prefix} HB ←"
# RPC 请求/响应
if cmd in (0x03, 0x04):
decoded = _decode_wup_body(body)
svc = decoded.get("servant", "")
func = decoded.get("func", "")
err = decoded.get("err")
if err:
return f"{prefix} RPC ERR: {err}"
label = f"{svc}.{func}" if svc else "RPC"
# 找 tRsp/tResp/tReq
for key in ("tRsp", "tResp", "tReq"):
data = decoded.get(key)
if isinstance(data, dict):
return f"{prefix} {label} {_fmt_fields(_truncate(data))}"
return f"{prefix} {label}"
# AUTH
if cmd == 0x0a:
text = _strip_tail(body).decode('utf-8', errors='replace')
return f"{prefix} AUTH {text[:100]}{'...' if len(text) > 100 else ''}"
# REGISTER / CONFIRM / PUSH 等
clean = _strip_tail(body)
try:
ins = TafInputStream(clean)
# 看第一个 head
tag, dtype = ins.peek_head()
if dtype == TafType.STRUCT_BEGIN:
ins.read_head()
fields = _decode_taf_struct(ins)
elif dtype == TafType.MAP:
ins.read_head()
cnt = ins._read_int_len()
fields = {}
for _ in range(cnt):
_, kt = ins.read_head()
k = _decode_taf_value(ins, kt)
_, vt = ins.read_head()
v = _decode_taf_value(ins, vt)
fields[k] = v
elif dtype == TafType.LIST:
ins.read_head()
cnt = ins._read_int_len()
items = []
for _ in range(cnt):
_, it = ins.read_head()
items.append(_decode_taf_value(ins, it))
fields = {"items": items}
else:
fields = {}
cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}")
if fields:
return f"{prefix} {cmd_name} {_fmt_fields(_truncate(fields))}"
return f"{prefix} {cmd_name}"
except Exception:
cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}")
return f"{prefix} {cmd_name} ({len(body)}B)"