net/http 默认机制无法限制每个IP的并发连接数,因其只管理TCP连接建立与HTTP请求生命周期,不聚合统计单IP的活跃连接;Gin虽基于net/http,但需借助sync.Map+时间窗口实现IP级限流,并谨慎处理X-Forwarded-For可信代理校验与异常路径下的计数清理。

为什么 net/http 默认机制拦不住并发连接数?
Gin 基于 net/http,而后者本身不跟踪“每个 IP 当前有多少活跃连接”——它只管 TCP 连接建立和 HTTP 请求生命周期。一个 IP 可能复用连接发 10 个请求(HTTP/1.1 Keep-Alive),也可能用 10 个不同端口建 10 个连接,net/http 都不会主动聚合统计。所以不能靠 http.Server.ReadTimeout 或中间件里简单计数请求就完事。
用 sync.Map + 时间窗口做轻量级 IP 连接计数
核心思路:在请求进入时加锁记录 IP,响应写出后释放;配合过期清理,避免内存无限增长。不用 Redis 是因为多数场景下单机限流已够用,且避免引入外部依赖和延迟。
-
sync.Map存储map[string]*ipState,key 是客户端 IP(注意用strings.Split(r.RemoteAddr, ":")[0]提取,但更稳妥用r.Header.Get("X-Forwarded-For")配合信任代理) -
ipState包含原子计数器count int64和lastSeen time.Time - 每次请求前调用
atomic.AddInt64(&s.count, 1),defer 中减 1;同时定期(比如每 30 秒)遍历sync.Map清理lastSeen超过 60 秒的条目 - 若
count > 5(阈值可配),直接c.AbortWithStatus(http.StatusTooManyRequests)
注意 X-Forwarded-For 的信任链问题
如果你的 Gin 服务前面有 Nginx、Cloudflare 或其他反向代理,r.RemoteAddr 拿到的是代理的 IP,不是真实用户。必须靠 X-Forwarded-For,但不能无条件信任——否则攻击者伪造头就能绕过。
- 只从可信代理列表中提取最后一段(例如 Nginx 配置了
proxy_set_header X-Forwarded-For $remote_addr;,那它的X-Forwarded-For就是可信的) - 推荐做法:维护一个
trustedProxies = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},检查c.ClientIP()是否在其中;若是,再取r.Header.Get("X-Forwarded-For")最右非空项 - Gin 自带
c.ClientIP()已做基础解析,但默认不校验代理可信性,需手动补这一层
并发计数器被误重置的典型场景
最常踩的坑是把计数逻辑写在中间件里,却忘了 HTTP/2 或长连接下,一次连接可能承载多个请求,而 ResponseWriter 的 WriteHeader 不一定触发(比如 panic、超时、流式响应未结束)。这时候 defer 减计数会漏掉,导致该 IP 永远卡在高位。
- 解决方案:不要依赖
defer清理,改用gin.ResponseWriter的包装器,在WriteHeader和Write方法里统一处理增减 - 或者更简单:用
context.WithTimeout包裹 handler,并在context.Done()时强制清理——但要注意Done()可能晚于连接关闭 - 上线前务必压测模拟断连、超时、大文件上传等异常路径,验证计数是否归零
http.Server.MaxConns)配合使用;单独控 IP 并发,容易被端口扫描类工具绕过,也掩盖不了后端服务真正的吞吐瓶颈。


















