- 协议事实确认:launch.pingSignal 的 signal_ap_start/signal_ap_end 是 Unix 毫秒墙钟时间戳。真实夹具 signal_ap_start=1791891743288 对应北京时间 2026-10-13 19:42:23,与同批抓包订单号里的秒级时间戳 1791891953 同日;start 是服务端收到请求的时刻、end 是写出响应的时刻,实测相差 13ms - 新增 internal/huya/timecal.go:TimeSample/TimeCalibration/OffsetHistory/RunTimeCalibration/AlignedTriggerTime/TryCalibrateServerTime。偏移 = signal_ap_start − (SentAt + RTT/2),误差上界取 RTT/2;默认 3 轮采样取「误差上界最小、其次 RTT 最低」的样本,再对最近 3 次偏移取中位数平滑;ServerTimestamp 丢弃解析后不在 2020-2100 的时间戳,避免字段单位变化时按错误偏移挪走抢购窗口 - HTTPClient 与 WSSClient 各自实现 TimeCalibrator(PingSignalSample 共用 pingSignalSampleWith 采样逻辑),各自持有 OffsetHistory - launch.go:ServerElapsed 改用 PingSignalStartKey/PingSignalEndKey 常量 - CLI:新增 -time-sync(默认开),-at 按服务端时间理解,触发时刻 = 目标 − lead − 偏移;校准日志输出服务端时间、本机偏移、误差上界、RTT 与样本数 - GUI:Config.TimeSync(默认 true)+ 对齐触发与校准日志;前端设置面板新增「时间校准」开关并纳入配置读写 - 测试:新增 internal/huya/timecal_test.go,覆盖真实夹具时间戳换算、秒级/异常值拒绝、RTT/2 中点公式、多轮取优、中位数滤跳变、全失败降级与部分失败容错、跨钟差的触发时刻换算;README 补「时间校准」一节 - 验证:gofmt 干净、go build ./...、go test ./...、go test -race(huya/sniper/gui)、前端 npm run typecheck/build 全部通过
270 lines
6.9 KiB
Go
270 lines
6.9 KiB
Go
package huya
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"huya-go/internal/taf"
|
||
)
|
||
|
||
const (
|
||
ServantLaunch = "launch"
|
||
|
||
FuncWsLaunch = "wsLaunch"
|
||
FuncQueryHttpDns = "queryHttpDns"
|
||
FuncPingSignal = "pingSignal"
|
||
FuncGetConfig = "getConfig"
|
||
|
||
// HttpDnsDomain 是 cdnws 入口域名,launch.queryHttpDns 用它换取 WSS host 列表。
|
||
HttpDnsDomain = "cdnws.api.huya.com"
|
||
|
||
// DefaultDNSTTL 是响应没有 ttl 时的兜底缓存时间。
|
||
DefaultDNSTTL = 10 * time.Minute
|
||
// dnsFailureCooldown 是 queryHttpDns 失败后的冷却时间。
|
||
dnsFailureCooldown = 30 * time.Second
|
||
)
|
||
|
||
// QueryHttpDnsReq 对应浏览器 launch.queryHttpDns 的 tReq。
|
||
//
|
||
// 实测字段(2026-10-13 HAR):0=uid, 1=ua, 2=域名列表, 3=appSrc,
|
||
// 4="", 5=0, 6=1,iRequestId 固定 -1。
|
||
type QueryHttpDnsReq struct {
|
||
UID int64
|
||
UA string
|
||
Domains []string
|
||
AppSrc string
|
||
Extra string
|
||
FlagType int32
|
||
FlagVer int32
|
||
}
|
||
|
||
func NewQueryHttpDnsReq(uid int64, ua string, domains []string) *QueryHttpDnsReq {
|
||
if len(domains) == 0 {
|
||
domains = []string{HttpDnsDomain}
|
||
}
|
||
return &QueryHttpDnsReq{
|
||
UID: uid,
|
||
UA: ua,
|
||
Domains: domains,
|
||
AppSrc: "HUYA&ZH&2052",
|
||
FlagType: 0,
|
||
FlagVer: 1,
|
||
}
|
||
}
|
||
|
||
func (q *QueryHttpDnsReq) WriteTo(w *taf.Writer) error {
|
||
if err := w.WriteInt64(0, q.UID); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(1, q.UA); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteListString(2, q.Domains); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(3, q.AppSrc); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(4, q.Extra); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteInt32(5, int64(q.FlagType)); err != nil {
|
||
return err
|
||
}
|
||
return w.WriteInt32(6, int64(q.FlagVer))
|
||
}
|
||
|
||
func (q *QueryHttpDnsReq) ReadFrom(r *taf.Reader) error {
|
||
var err error
|
||
if q.UID, err = r.ReadInt64(0, false, q.UID); err != nil {
|
||
return err
|
||
}
|
||
if q.UA, err = r.ReadString(1, false, q.UA); err != nil {
|
||
return err
|
||
}
|
||
if q.Domains, err = r.ReadListString(2, false); err != nil {
|
||
return err
|
||
}
|
||
if q.AppSrc, err = r.ReadString(3, false, q.AppSrc); err != nil {
|
||
return err
|
||
}
|
||
if q.Extra, err = r.ReadString(4, false, q.Extra); err != nil {
|
||
return err
|
||
}
|
||
if q.FlagType, err = r.ReadInt32(5, false, q.FlagType); err != nil {
|
||
return err
|
||
}
|
||
q.FlagVer, err = r.ReadInt32(6, false, q.FlagVer)
|
||
return err
|
||
}
|
||
|
||
// HttpDnsHosts 是 tRsp tag0 里单个域名的解析结果:host 列表 + ttl 秒。
|
||
type HttpDnsHosts struct {
|
||
Hosts []string
|
||
TTL int32
|
||
Extra []string
|
||
}
|
||
|
||
func (h *HttpDnsHosts) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (h *HttpDnsHosts) ReadFrom(r *taf.Reader) error {
|
||
var err error
|
||
if h.Hosts, err = r.ReadListString(0, false); err != nil {
|
||
return err
|
||
}
|
||
if h.TTL, err = r.ReadInt32(1, false, h.TTL); err != nil {
|
||
return err
|
||
}
|
||
h.Extra, err = r.ReadListString(2, false)
|
||
return err
|
||
}
|
||
|
||
// QueryHttpDnsResp 对应 launch.queryHttpDns 的 tRsp:tag0=域名->host 列表, tag1=状态。
|
||
type QueryHttpDnsResp struct {
|
||
Hosts map[string]HttpDnsHosts
|
||
Status int32
|
||
}
|
||
|
||
func (r *QueryHttpDnsResp) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (r *QueryHttpDnsResp) ReadFrom(rd *taf.Reader) error {
|
||
items, err := rd.ReadMapStringStruct(0, false, func() taf.Unmarshaler { return &HttpDnsHosts{} })
|
||
if err != nil {
|
||
return err
|
||
}
|
||
r.Hosts = make(map[string]HttpDnsHosts, len(items))
|
||
for key, item := range items {
|
||
if hosts, ok := item.(*HttpDnsHosts); ok {
|
||
r.Hosts[key] = *hosts
|
||
}
|
||
}
|
||
r.Status, err = rd.ReadInt32(1, false, r.Status)
|
||
return err
|
||
}
|
||
|
||
// WSSHosts 返回指定域名解析到的 WSS host 列表,保持服务端下发顺序。
|
||
func (r QueryHttpDnsResp) WSSHosts(domain string) []string {
|
||
item, ok := r.Hosts[domain]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
out := make([]string, 0, len(item.Hosts))
|
||
for _, host := range item.Hosts {
|
||
if trimmed := strings.TrimSpace(host); trimmed != "" {
|
||
out = append(out, trimmed)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// TTL 返回指定域名的缓存时间。
|
||
func (r QueryHttpDnsResp) TTL(domain string) time.Duration {
|
||
item, ok := r.Hosts[domain]
|
||
if !ok || item.TTL <= 0 {
|
||
return DefaultDNSTTL
|
||
}
|
||
return time.Duration(item.TTL) * time.Second
|
||
}
|
||
|
||
// PingSignalReq 对应 launch.pingSignal:0="launch", 1="pingSignal", 2=扩展 map。
|
||
type PingSignalReq struct {
|
||
Extra map[string]string
|
||
}
|
||
|
||
func (p *PingSignalReq) WriteTo(w *taf.Writer) error {
|
||
if err := w.WriteString(0, ServantLaunch); err != nil {
|
||
return err
|
||
}
|
||
if err := w.WriteString(1, FuncPingSignal); err != nil {
|
||
return err
|
||
}
|
||
return w.WriteMapStringString(2, p.Extra)
|
||
}
|
||
|
||
func (p *PingSignalReq) ReadFrom(r *taf.Reader) error {
|
||
var err error
|
||
if _, err = r.ReadString(0, false, ServantLaunch); err != nil {
|
||
return err
|
||
}
|
||
if _, err = r.ReadString(1, false, FuncPingSignal); err != nil {
|
||
return err
|
||
}
|
||
p.Extra, err = r.ReadMapStringString(2, false)
|
||
return err
|
||
}
|
||
|
||
// PingSignalResp 对应 launch.pingSignal 的 tRsp:tag2 是 signal_ap_start/end 等服务端时间戳。
|
||
type PingSignalResp struct {
|
||
Servant string
|
||
Func string
|
||
Values map[string]int64
|
||
}
|
||
|
||
func (r *PingSignalResp) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (r *PingSignalResp) ReadFrom(rd *taf.Reader) error {
|
||
var err error
|
||
if r.Servant, err = rd.ReadString(0, false, r.Servant); err != nil {
|
||
return err
|
||
}
|
||
if r.Func, err = rd.ReadString(1, false, r.Func); err != nil {
|
||
return err
|
||
}
|
||
r.Values, err = rd.ReadMapStringInt64(2, false)
|
||
return err
|
||
}
|
||
|
||
// ServerElapsed 返回服务端 signal_ap_start/end 的差值(毫秒),不可得时返回 0。
|
||
func (r PingSignalResp) ServerElapsed() time.Duration {
|
||
start, okStart := r.Values[PingSignalStartKey]
|
||
end, okEnd := r.Values[PingSignalEndKey]
|
||
if !okStart || !okEnd || end < start {
|
||
return 0
|
||
}
|
||
return time.Duration(end-start) * time.Millisecond
|
||
}
|
||
|
||
// GetConfigResp 对应 mobileui.getConfig 的 tRsp:
|
||
// tag0=平台开关 map,tag1=按平台下发的配置版本 map。
|
||
type GetConfigResp struct {
|
||
Flags map[string]string
|
||
Exps map[string]string
|
||
}
|
||
|
||
func (r *GetConfigResp) WriteTo(w *taf.Writer) error { return nil }
|
||
|
||
func (r *GetConfigResp) ReadFrom(rd *taf.Reader) error {
|
||
var err error
|
||
if r.Flags, err = rd.ReadMapStringString(0, false); err != nil {
|
||
return err
|
||
}
|
||
r.Exps, err = rd.ReadMapStringString(1, false)
|
||
return err
|
||
}
|
||
|
||
// Exp 返回握手 baseinfo.tag5 与 UpdateUserExps 使用的版本串。
|
||
func (r GetConfigResp) Exp() string {
|
||
return FormatConfigExps(r.Exps)
|
||
}
|
||
|
||
// FormatConfigExps 把 {"58664":"107263"} 拼成 "58664.107263"。
|
||
//
|
||
// 2026-10-13 HAR 中 WSS 握手 baseinfo.tag5 与 UpdateUserExpsReq 都用这个形式;
|
||
// 多个平台时按 key 升序用 "." 连接(实测样本只有一个平台)。
|
||
func FormatConfigExps(exps map[string]string) string {
|
||
if len(exps) == 0 {
|
||
return ""
|
||
}
|
||
keys := make([]string, 0, len(exps))
|
||
for key := range exps {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
parts := make([]string, 0, len(keys))
|
||
for _, key := range keys {
|
||
parts = append(parts, key+"."+exps[key])
|
||
}
|
||
return strings.Join(parts, ".")
|
||
}
|