You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

针对不稳定连接的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 16:57:58