Skip to content

Instantly share code, notes, and snippets.

@abserari
Created July 19, 2025 12:28
Show Gist options
  • Save abserari/68c885ab00e03c102d5151a5e18100aa to your computer and use it in GitHub Desktop.
Save abserari/68c885ab00e03c102d5151a5e18100aa to your computer and use it in GitHub Desktop.
// 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