Files

352 lines
8.1 KiB
Go

package wup
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"sort"
"strings"
"huya-go/internal/taf"
)
// Request 是 WUP v3 请求封装。
type Request struct {
Version int16
PacketType int8
Message int32
RequestID int32
Servant string
Func string
Timeout int32
Context map[string]string
Status map[string]string
NewData map[string][]byte
}
func NewRequest(servant, fn string, requestID int32) *Request {
return &Request{
Version: 3,
Servant: servant,
Func: fn,
RequestID: requestID,
Timeout: 3000,
Context: map[string]string{},
Status: map[string]string{},
NewData: map[string][]byte{},
}
}
func (r *Request) WriteStruct(key string, value taf.Marshaler) error {
w := taf.NewWriter()
if err := w.WriteStruct(0, value); err != nil {
return err
}
r.NewData[key] = w.Bytes()
return nil
}
func (r *Request) Encode() ([]byte, error) {
data := taf.NewWriter()
if err := data.WriteMapStringBytes(0, r.NewData); err != nil {
return nil, err
}
body := taf.NewWriter()
if err := body.WriteInt16(1, int64(r.Version)); err != nil {
return nil, err
}
if err := body.WriteInt8(2, int64(r.PacketType)); err != nil {
return nil, err
}
if err := body.WriteInt32(3, int64(r.Message)); err != nil {
return nil, err
}
if err := body.WriteInt32(4, int64(r.RequestID)); err != nil {
return nil, err
}
if err := body.WriteString(5, r.Servant); err != nil {
return nil, err
}
if err := body.WriteString(6, r.Func); err != nil {
return nil, err
}
if err := body.WriteBytes(7, data.Bytes()); err != nil {
return nil, err
}
if err := body.WriteInt32(8, int64(r.Timeout)); err != nil {
return nil, err
}
if err := body.WriteMapStringString(9, r.Context); err != nil {
return nil, err
}
if err := body.WriteMapStringString(10, r.Status); err != nil {
return nil, err
}
rawBody := body.Bytes()
out := bytes.NewBuffer(make([]byte, 0, len(rawBody)+4))
var lenBuf [4]byte
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(rawBody)+4))
out.Write(lenBuf[:])
out.Write(rawBody)
return out.Bytes(), nil
}
// Response 是 WUP 响应封装。
type Response struct {
Version int16
PacketType int8
Message int32
RequestID int32
Servant string
Func string
Buffer []byte
Timeout int32
Context map[string]string
Status map[string]string
NewData map[string][]byte
}
func (r *Response) Decode(data []byte) error {
body := NormalizePayload(data)
reader := taf.NewReader(body)
r.Context = map[string]string{}
r.Status = map[string]string{}
r.NewData = map[string][]byte{}
for {
tag, typ, err := reader.ReadHead()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if tag > 10 {
return nil
}
switch tag {
case 1:
v, err := reader.ReadIntValue(typ)
r.Version = int16(v)
if err != nil {
return err
}
case 2:
v, err := reader.ReadIntValue(typ)
r.PacketType = int8(v)
if err != nil {
return err
}
case 3:
v, err := reader.ReadIntValue(typ)
r.Message = int32(v)
if err != nil {
return err
}
case 4:
v, err := reader.ReadIntValue(typ)
r.RequestID = int32(v)
if err != nil {
return err
}
case 5:
v, err := reader.ReadStringValue(typ)
r.Servant = v
if err != nil {
return err
}
case 6:
v, err := reader.ReadStringValue(typ)
r.Func = v
if err != nil {
return err
}
case 7:
v, err := reader.ReadBytesValue(typ)
if err != nil {
return err
}
r.Buffer = v
if err := r.decodeBuffer(); err != nil {
return err
}
case 8:
v, err := reader.ReadIntValue(typ)
r.Timeout = int32(v)
if err != nil {
return err
}
case 9:
if typ != taf.TypeMap {
if err := reader.Skip(typ); err != nil {
return err
}
continue
}
_ = reader.Seek(reader.Position() - 1)
v, err := reader.ReadMapStringString(9, false)
r.Context = v
if err != nil {
return err
}
case 10:
if typ != taf.TypeMap {
if err := reader.Skip(typ); err != nil {
return err
}
return nil
}
_ = reader.Seek(reader.Position() - 1)
v, err := reader.ReadMapStringString(10, false)
r.Status = v
if err != nil {
return err
}
return nil
default:
if err := reader.Skip(typ); err != nil {
return err
}
}
}
}
func (r *Response) decodeBuffer() error {
if len(r.Buffer) == 0 {
return nil
}
reader := taf.NewReader(r.Buffer)
m, err := reader.ReadMapStringBytes(0, false)
if err != nil {
return err
}
r.NewData = m
return nil
}
func (r *Response) ReadStruct(key string, obj taf.Unmarshaler) (bool, error) {
data := r.NewData[key]
if len(data) == 0 && key == "tRsp" {
data = r.NewData["tResp"]
}
if len(data) == 0 && key == "tResp" {
data = r.NewData["tRsp"]
}
if len(data) == 0 {
return false, nil
}
reader := taf.NewReader(data)
_, typ, err := reader.ReadHead()
if err != nil {
return false, err
}
if typ != taf.TypeStructBegin {
return false, fmt.Errorf("wup: %s 不是结构体响应: 0x%02x", key, byte(typ))
}
if err := obj.ReadFrom(reader); err != nil {
return false, err
}
return true, reader.SkipToStructEnd()
}
func NormalizePayload(data []byte) []byte {
if len(data) < 4 {
return data
}
n := int(binary.BigEndian.Uint32(data[:4]))
if n == len(data) || n+4 == len(data) {
return data[4:]
}
return data
}
// 诊断串里的取值上限:服务端错误信息可能很长,日志里只需要够判断原因的前缀。
const (
diagnoseValueLimit = 80
diagnoseFieldLimit = 6
)
// Diagnose 汇总一次 WUP 响应里可用于排障的字段。
//
// 「响应缺少 tRsp/tResp」这种报错过去只有一句文案,服务端的 iRet / status /
// context 全被丢掉,事后只能靠抓包复现(2026-10-16 pc-3 日志里同一账号连续 13 次
// 命中该错误,却看不出任何原因)。这里把 tag3(iRet)、tag9(context)、tag10(status)
// 与 tResp 数据体的 key 一并带出来,一次日志就能定性。
func (r *Response) Diagnose() string {
if r == nil {
return "响应为空"
}
parts := []string{fmt.Sprintf("iRet=%d", r.Message)}
if r.Servant != "" || r.Func != "" {
parts = append(parts, fmt.Sprintf("servant=%s.%s", r.Servant, r.Func))
}
if text := formatDiagnoseMap(r.Status); text != "" {
parts = append(parts, "status="+text)
}
if text := formatDiagnoseMap(r.Context); text != "" {
parts = append(parts, "context="+text)
}
if len(r.NewData) == 0 {
parts = append(parts, "keys=空")
} else {
parts = append(parts, "keys="+formatDiagnoseKeys(r.NewData))
}
parts = append(parts, fmt.Sprintf("payload=%dB", len(r.Buffer)))
return strings.Join(parts, " ")
}
// formatDiagnoseMap 按键名排序输出 map,值过长时截断,保证日志稳定且不刷屏。
func formatDiagnoseMap(values map[string]string) string {
if len(values) == 0 {
return ""
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for index, key := range keys {
if index >= diagnoseFieldLimit {
parts = append(parts, fmt.Sprintf("…(共%d项)", len(keys)))
break
}
parts = append(parts, fmt.Sprintf("%s=%s", key, truncateDiagnoseValue(values[key])))
}
return "{" + strings.Join(parts, " ") + "}"
}
// formatDiagnoseKeys 输出 tResp 数据体的键名(值可能是任意结构,只列键)。
func formatDiagnoseKeys(values map[string][]byte) string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, fmt.Sprintf("%s(%dB)", key, len(values[key])))
}
sort.Strings(keys)
return "{" + strings.Join(keys, " ") + "}"
}
func truncateDiagnoseValue(value string) string {
trimmed := strings.TrimSpace(value)
if len(trimmed) <= diagnoseValueLimit {
return trimmed
}
return trimmed[:diagnoseValueLimit] + "…"
}
// HexHead 把二进制帧的前 n 字节转成十六进制,用于 debug 级日志定位协议问题。
func HexHead(data []byte, n int) string {
if n <= 0 || len(data) == 0 {
return ""
}
if n > len(data) {
n = len(data)
}
return hex.EncodeToString(data[:n])
}