352 lines
8.1 KiB
Go
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])
|
|
}
|