Created
July 19, 2025 12:28
-
-
Save abserari/68c885ab00e03c102d5151a5e18100aa to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// sendHeadRequest 发送单个HEAD请求用于连接保活 | |
// 返回值: (成功, 请求持续时间, 错误信息) | |
func (sm *Manager) sendHeadRequest( | |
ctx context.Context, | |
sessionKey string, | |
session *coreSession.UserSession, | |
httpClient *http.Client, | |
requestBuilder *httprequest.Builder, | |
requestTimeout time.Duration, | |
requestIndex int, | |
) (bool, time.Duration, string) { | |
var lastError string | |
reqStartTime := time.Now() | |
// 构建HEAD请求,使用与Book请求完全相同的路径以最大化TLS连接复用率 | |
// 这样可以确保保活的HTTP连接能被后续的Book请求重用,大幅减少TLS握手 | |
req, err := requestBuilder.BuildRequest(httprequest.RequestTypeHEAD, session.LoginDict.LastGroupID, "") | |
if err != nil { | |
lastError = fmt.Sprintf("build request error: %v", err) | |
return false, time.Since(reqStartTime), lastError | |
} | |
// 添加缓存破坏参数(避免被 CDN 缓存导致超时) | |
// 这些请求可能被 CDN 或代理服务器缓存,导致响应时间异常长 | |
// 添加时间戳参数确保每次请求都是唯一的 | |
q := req.URL.Query() | |
// 添加缓存破坏参数,模仿 utils.GetCacheBustingParams 的实现 | |
// 为每个请求添加不同的时间戳,确保3个请求都会建立新连接 | |
timestamp := time.Now().UnixMilli() + int64(requestIndex*100) | |
randomOffset := rand.Int63n(60000) + 1 // 0-60秒的随机偏移 | |
randomTimestamp := timestamp - randomOffset | |
q.Set("_", strconv.FormatInt(randomTimestamp, 10)) | |
req.URL.RawQuery = q.Encode() | |
// 设置请求超时和context | |
reqCtx, reqCancel := context.WithTimeout(ctx, requestTimeout) | |
defer reqCancel() | |
req = req.WithContext(reqCtx) | |
// 设置请求头 | |
httprequest.SetHeaders(req, session) | |
// 发送HEAD请求 | |
headStartTime := time.Now() | |
resp, err := httpClient.Do(req) | |
headDuration := time.Since(headStartTime) | |
if err != nil { | |
if errors.IsEOFError(err) { | |
// EOF 错误对于 HEAD 请求可能是正常的 | |
// 记录EOF情况下的HEAD请求信息到TLS复用收集器 | |
if metrics.GlobalTLSReuseCollector != nil && session != nil { | |
if sessionKey != "" { | |
metrics.GlobalTLSReuseCollector.RecordHeadRequest( | |
sessionKey, | |
headStartTime, headDuration, | |
) | |
} | |
} | |
return true, time.Since(reqStartTime), "" | |
} else { | |
// 区分错误类型 | |
if ctxErr := ctx.Err(); ctxErr != nil { | |
lastError = "keep-alive routine canceled" | |
} else if reqCtx.Err() == context.DeadlineExceeded { | |
lastError = fmt.Sprintf("request timeout for %s after %v", sessionKey, requestTimeout) | |
} else { | |
lastError = fmt.Sprintf("request error: %v", err) | |
} | |
if !errors.IsContextDeadlineExceededError(err) { | |
logger.Debug("保活请求失败", | |
logger.FieldUserID, sessionKey, | |
logger.FieldError, err, | |
"last_error", lastError, | |
"error_type", fmt.Sprintf("%T", err), | |
"request_index", requestIndex) | |
} | |
return false, time.Since(reqStartTime), lastError | |
} | |
} | |
// 确保响应体被完全读取以复用连接 | |
if resp != nil && resp.Body != nil { | |
// HEAD请求通常没有响应体,但仍需确保完全读取以复用连接 | |
io.Copy(io.Discard, resp.Body) | |
resp.Body.Close() | |
} | |
if resp != nil && resp.StatusCode >= 200 && resp.StatusCode < 400 { | |
// 接受所有 2xx 和 3xx 状态码作为成功的 HEAD 请求 | |
// 记录HEAD请求信息到TLS复用收集器 | |
if metrics.GlobalTLSReuseCollector != nil && session != nil { | |
// 直接从session中获取信息 | |
if sessionKey != "" { | |
metrics.GlobalTLSReuseCollector.RecordHeadRequest( | |
sessionKey, | |
headStartTime, headDuration, | |
) | |
} else { | |
logger.Debug("HEAD请求信息不完整,跳过记录", | |
logger.FieldUserID, sessionKey) | |
} | |
} else { | |
logger.Debug("无法记录HEAD请求", | |
"collector_nil", metrics.GlobalTLSReuseCollector == nil, | |
"session_nil", session == nil) | |
} | |
return true, time.Since(reqStartTime), "" | |
} else if resp != nil { | |
lastError = fmt.Sprintf("unexpected status: %d", resp.StatusCode) | |
logger.Debug("保活请求返回错误状态", | |
logger.FieldUserID, sessionKey, | |
"status", resp.StatusCode, | |
"request_index", requestIndex) | |
return false, time.Since(reqStartTime), lastError | |
} | |
return false, time.Since(reqStartTime), "no response" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment