gin 无法限制单个 ip 的 tcp 连接数,仅能通过中间件限制每 ip 并发请求数;需正确提取真实 ip、标准化 ip 字符串、配对原子计数,并避免误用 limitlistener。

Go 语言 Gin 框架本身不提供“单个 IP 最大连接数”控制能力——limit_conn 是 Nginx 的功能,Gin 运行在应用层,看到的是 HTTP 请求,不是 TCP 连接。你真正能控的,是每个 IP 的并发请求数(即同一时刻正在处理的 handler 数),而非底层连接数。
为什么 Gin 无法限制“连接数”
Gin 基于 net/http,而 http.Server 不暴露连接来源的细粒度生命周期钩子;它只在 Handler 执行时给你一个 *http.Request。TCP 连接可能复用(keep-alive)、可能承载多个请求(HTTP/2),也可能早已建立但空闲。Gin 中间件根本收不到“连接建立/断开”事件,所以做不到 Nginx 那种基于 ESTABLISHED 状态的连接计数。
- 想限连接数 → 必须在 TCP 层或反向代理层做,比如 Nginx 的
limit_conn、netutil.LimitListener - 想限 Gin 中每个 IP 的并发请求数 → 只能靠中间件 +
sync.Map+atomic计数 - 混淆这两者会导致策略失效:比如设了“每 IP 最多 3 个连接”,但客户端用 HTTP/2 发起 10 个并行流,Gin 中间件却只看到 1 个连接、10 个请求,完全不受控
用中间件实现每 IP 并发请求数限制
这是 Gin 场景下最贴近需求的可行方案:统计每个 IP 当前正在执行的 handler 数量,超限即拒绝。关键点不在“怎么写”,而在“怎么写对”:
- 真实 IP 提取必须校验可信代理:
X-Forwarded-For可伪造,需配合set_real_ip_from类逻辑(如检查req.Header.Get("X-Real-IP")或白名单内网地址 fallback) - IP 字符串要标准化:IPv6 地址带方括号(
[::1])或端口(192.168.1.1:12345),提取时得去掉端口部分,否则同一 IP 多个连接被当成不同 key - 计数必须严格配对:
atomic.AddInt32(..., 1)进入 handler,defer atomic.AddInt32(..., -1)退出;且退出时若值 ≤ 0,应主动ips.Delete(ip)避免内存泄漏 -
sync.Map的LoadOrStore返回的是interface{},必须类型断言为*int32,否则运行时 panic
别误用 golang.org/x/net/netutil.LimitListener
这个包只能限制整个服务的总并发连接数,和“单 IP”无关:
-
netutil.LimitListener(lis, 100)表示最多接受 100 个 TCP 连接,不管来源 IP - 它作用在
http.Server.ListenAndServe()底层,Gin 完全感知不到,也无法按 IP 区分 - 在 HTTP/2 下更失真:1 个连接可承载上百请求,此时“限连接数”几乎等价于“不限制请求”
生产环境建议组合使用
单一手段很难兜住所有场景:
- Nginx 层用
limit_conn控连接总数(防连接耗尽) +limit_rate控单连接带宽(防慢速攻击) - Gin 层用 IP 并发请求数中间件控 handler 并发(防后端业务阻塞)
- 敏感接口再叠加
rate.Limiter控请求速率(如登录接口每分钟最多 5 次) - 所有限流都设
Retry-After和明确状态码(如429),避免前端重试雪崩
最容易被忽略的是 IP 提取逻辑和端口截断——没做这步,本地测试全 OK,一上 CDN 就发现所有流量被算成同一个 IP 或频繁误判。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











