针对不稳定连接的Golang POST代理:如何防止连接空闲超时及处理Slowloris问题
针对不稳定连接的Golang POST代理:如何防止连接空闲超时及处理Slowloris问题
哎,这种场景我太有共鸣了——移动网络的时断时续加上后端服务器卡得死死的5秒空闲超时,简直是代理开发的噩梦。用户这边信号一弱就停发数据,后端那边直接把连接掐了,等用户信号回来猛发一堆数据的时候,代理这边连不上后端,只能返回错误,体验差到爆炸。结合你提到的缓冲思路,我整理了几个在Golang里可行的方案,还顺带解决Slowloris的隐患:
一、核心思路:缓冲请求+超时兜底
核心就是在代理层做个“中间站”:先把用户的POST请求数据缓存一段,要么攒到一定大小就发给后端,要么等用户端超时没数据就把已缓存的内容发出去,绝对不让和后端的连接空闲超过5秒。同时还要防着别被Slowloris攻击把代理资源占满。
二、具体实现方案(附代码)
1. 带缓冲的代理Handler
先写一个自定义的Handler,实现请求体的缓冲和超时控制:
package main import ( "bytes" "bufio" "context" "io" "net" "net/http" "time" ) // 自定义代理Handler,配置缓冲和超时参数 type BufferedProxy struct { TargetURL string // 后端目标地址 BufferThreshold int // 缓冲达到这个大小就触发转发 ClientIdleTimeout time.Duration // 用户端空闲多久就转发已缓存数据 MaxBufferSize int // 单个连接的最大缓冲,防止OOM } func (p *BufferedProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 初始化请求体缓冲 bodyBuf := bytes.NewBuffer(make([]byte, 0, p.BufferThreshold)) clientReader := bufio.NewReader(r.Body) defer r.Body.Close() // 用context控制用户端空闲超时 ctx, cancel := context.WithCancel(r.Context()) defer cancel() // 启动空闲计时器:用户端超时没数据就转发 idleTimer := time.AfterFunc(p.ClientIdleTimeout, func() { cancel() }) defer idleTimer.Stop() // 循环读取用户端请求数据 for { select { case <-ctx.Done(): // 超时或被取消,退出循环转发数据 break default: // 设置单次读取超时,避免无限阻塞 clientReader.SetReadDeadline(time.Now().Add(1 * time.Second)) chunk, err := clientReader.ReadByte() if err != nil { if err == io.EOF { // 用户端数据发送完毕 break } // 读取出错,返回错误给用户 http.Error(w, "client connection lost", http.StatusBadRequest) return } // 检查缓冲是否超过最大值,防止OOM if bodyBuf.Len()+1 > p.MaxBufferSize { break } // 写入缓冲并重置空闲计时器 bodyBuf.WriteByte(chunk) idleTimer.Reset(p.ClientIdleTimeout) // 达到缓冲阈值,提前转发给后端 if bodyBuf.Len() >= p.BufferThreshold { break } } } // 构建转发给后端的请求 backendReq, err := http.NewRequest(r.Method, p.TargetURL, bodyBuf) if err != nil { http.Error(w, "failed to create backend request", http.StatusInternalServerError) return } // 复制原请求的所有头信息 backendReq.Header = r.Header.Clone() // 配置后端连接的Transport,控制超时和连接复用 transport := &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { conn, err := net.DialTimeout(network, addr, 3*time.Second) if err != nil { return nil, err } // 设置后端连接的超时,比后端的5秒空闲超时短一点 conn.SetDeadline(time.Now().Add(4 * time.Second)) return conn, nil }, // 复用空闲连接,但确保不会超过后端的超时限制 IdleConnTimeout: 4 * time.Second, } // 发送请求到后端 backendResp, err := transport.RoundTrip(backendReq) if err != nil { http.Error(w, "failed to connect to backend", http.StatusBadGateway) return } defer backendResp.Body.Close() // 把后端响应返回给用户 copyHeaders(w.Header(), backendResp.Header) w.WriteHeader(backendResp.StatusCode) io.Copy(w, backendResp.Body) } // 复制响应头的辅助函数 func copyHeaders(dst, src http.Header) { for k, v := range src { dst[k] = append(dst[k], v...) } }
2. Slowloris防护:限制用户端连接速率
如果只做缓冲,很容易被恶意用户用Slowloris攻击——每个连接只发一点点数据,占着代理的缓冲和连接资源。所以要在监听层加速率限制:
// 带Slowloris防护的Listener type SlowlorisProtectedListener struct { net.Listener MinBytesPerSecond int // 最低发送速率,低于这个就断开连接 CheckInterval time.Duration // 检查速率的时间间隔 } func (l *SlowlorisProtectedListener) Accept() (net.Conn, error) { conn, err := l.Listener.Accept() if err != nil { return nil, err } // 包装连接,实现速率检查逻辑 return &rateLimitedConn{ Conn: conn, minRate: l.MinBytesPerSecond, checkInterval: l.CheckInterval, lastCheck: time.Now(), totalBytes: 0, }, nil } type rateLimitedConn struct { net.Conn minRate int checkInterval time.Duration lastCheck time.Time totalBytes int } func (c *rateLimitedConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) if n > 0 { c.totalBytes += n now := time.Now() elapsed := now.Sub(c.lastCheck).Seconds() // 到达检查间隔,验证发送速率 if elapsed > c.checkInterval.Seconds() { rate := int(float64(c.totalBytes) / elapsed) if rate < c.minRate { // 速率过低,断开连接 err = io.EOF c.Conn.Close() return } // 重置统计数据 c.totalBytes = 0 c.lastCheck = now } } // 每次读取后重置连接超时,避免死连接占用资源 c.Conn.SetDeadline(time.Now().Add(c.checkInterval * 2)) return }
三、关键配置建议
- 缓冲与超时平衡:建议把
ClientIdleTimeout设为3秒,BufferThreshold设为1KB——既不会让后端连接空闲超过5秒,又能减少零碎请求的数量。 - OOM防护:一定要设置
MaxBufferSize(比如64KB),避免大量用户同时缓冲数据导致内存溢出。 - 后端连接复用:通过
http.Transport的IdleConnTimeout复用连接,但要确保超时时间比后端的空闲超时短,避免后端主动断开连接。 - 分块传输优化:如果后端支持
Transfer-Encoding: chunked,可以改用分块方式发送数据——不用等缓冲满,有数据就发一个分块,彻底解决空闲超时问题。
四、踩过的坑
- 别用全局缓冲池:每个连接用独立的缓冲,避免并发读写冲突。
- 空请求处理:要确保缓冲至少有数据再转发,不然部分后端会直接返回400错误。
- 资源监控:建议加指标监控每个连接的缓冲大小、连接存活时间,方便后续调整配置。
备注:内容来源于stack exchange,提问作者Tom Raganowicz
相关产品推荐
相关产品推荐

