Files
live-hub-py/core/huya/frame_decoder.py
T
2026-08-31 10:55:44 +08:00

323 lines
11 KiB
Python

"""
TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出
"""
from typing import Any, cast
from loguru import logger
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: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
val = f"<decode_err:0x{dtype:02x}>"
else:
val = "<...>"
try:
ins.skip_field(dtype)
except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
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 or tag in (2, 3) or 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 as exc: # noqa: BLE001
logger.debug(f"TAF 嵌套结构解码失败: {exc}")
result[k] = f"<{len(v)}B>"
else:
result[k] = _decode_taf_value(sins, vt)
except Exception as exc: # noqa: BLE001
logger.debug(f"TAF 字段解码失败: {exc}")
except Exception as e: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
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(cast(dict[str, Any], _truncate(fields)))}"
return f"{prefix} {cmd_name}"
except Exception: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}")
return f"{prefix} {cmd_name} ({len(body)}B)"