概念定义
Keep-alive 是 HTTP/1.1 的默认行为:响应结束后 TCP 连接保持打开,同源的下一个请求直接复用。它省掉 TCP 三次握手、TLS 重新协商,并保留已成长的拥塞窗口。空闲连接由超时关闭。
工作机制
服务器在响应后保持套接字打开,直到空闲超时或显式关闭。客户端跟踪可复用连接,任何一方可发出关闭信号,半开连接必须被检测并清理。连接池在此之上增加最大数量、空闲驱逐与健康检查等策略。
使用场景
服务器间 API 调用必须复用连接,每次握手纯属浪费;浏览器已自动处理。关键在超时协同:客户端空闲超时应短于上游超时,否则复用时会撞上对端刚关闭的连接。
常见误区
keep-alive 复用是每连接严格串行的,并发需要多条连接,HTTP/2 多路复用才移除该限制。复用到刚被对端关闭的连接会在下一个请求上出现竞态,健壮的客户端会对幂等请求重试一次。无上限的空闲池会耗尽服务端内存与接收槽位;负载均衡器悄悄丢弃空闲 TCP 状态而不发 RST 时,会造成貌似应用卡死的间歇性停顿。
代码示例
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
type Shipment struct {
ID string `json:"id"`
Status string `json:"status"`
}
func queryShipment(ctx context.Context, baseURL, id string) (*Shipment, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() // 必须释放计时器资源
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/shipments/"+id, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, fmt.Errorf("shipment %s timed out after 3s: %w", id, err)
}
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
var shipment Shipment
if err := json.NewDecoder(resp.Body).Decode(&shipment); err != nil {
return nil, fmt.Errorf("decode body: %w", err)
}
return &shipment, nil
}
func main() {
s, err := queryShipment(context.Background(), "https://api.example.com", "sh-1")
fmt.Println(s, err)
}
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"time"
)
type fetchResult struct {
Status int
Body string
}
func fetchWithDeadline(ctx context.Context, client *http.Client, url string, timeout time.Duration) (fetchResult, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fetchResult{}, fmt.Errorf("build request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fetchResult{}, fmt.Errorf("fetch %s: deadline exceeded after %s: %w", url, timeout, context.DeadlineExceeded)
}
return fetchResult{}, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return fetchResult{}, fmt.Errorf("read body: %w", err)
}
return fetchResult{Status: resp.StatusCode, Body: string(body)}, nil
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/fast", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
})
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(200 * time.Millisecond):
fmt.Fprint(w, "late")
case <-r.Context().Done():
}
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
ctx := context.Background()
result, err := fetchWithDeadline(ctx, client, server.URL+"/fast", 500*time.Millisecond)
fmt.Printf("fast: status=%d body=%q err=%v\n", result.Status, result.Body, err)
result, err = fetchWithDeadline(ctx, client, server.URL+"/slow", 50*time.Millisecond)
fmt.Printf("slow: status=%d body=%q err=%v\n", result.Status, result.Body, err)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("slow request classified as a deadline error")
}
}