net/http 默认不控制 ip 并发数,因 servemux 和底层 http.server 无 ip 感知能力;需用中间件 + sync.map 实现每 ip 计数,并注意真实 ip 提取、端口截断、原子操作及定时清理零值项。

为什么 net/http 默认不控制 IP 并发数
Go 的标准库 http.ServeMux 和大多数轻量框架(如 gin、echo)本身不内置 IP 级别并发限制——它们只管路由分发和 handler 执行,连接管理由底层 net.Listener 和 http.Server 负责,而后者对“IP 来源”无感知。直接靠 http.Server.MaxConns 或 MaxIdleConns 只能控全局连接数,无法按 IP 区分。
用中间件 + sync.Map 实现每 IP 并发计数
最常用且可控的方式是在 handler 前加一层中间件,用 sync.Map 记录每个 RemoteAddr 当前活跃请求数。注意:真实 IP 需从 X-Forwarded-For 提取(若服务在反向代理后),否则拿到的是代理地址。
- 提取 IP 时优先检查
X-Forwarded-For头,但必须校验可信代理列表,避免伪造;否则 fallback 到r.RemoteAddr - 使用
sync.Map而非普通map,避免并发读写 panic;但注意sync.Map不支持原子增减,需用LoadOrStore+ 循环 CAS 模拟 - 计数必须配对:进入 handler 时 +1,退出时 -1(用
defer确保执行) - 超限时返回
http.StatusTooManyRequests,并设Retry-After头
// 示例:Gin 中间件
func IPConcurrencyLimit(max int) gin.HandlerFunc {
ips := sync.Map{} // key: ip string, value: *int32
return func(c *gin.Context) {
ip := realIP(c.Request)
countPtr, _ := ips.LoadOrStore(ip, new(int32))
count := atomic.AddInt32(countPtr.(*int32), 1)
if count > max {
atomic.AddInt32(countPtr.(*int32), -1)
c.Header("Retry-After", "60")
c.AbortWithStatus(http.StatusTooManyRequests)
return
}
defer func() {
atomic.AddInt32(countPtr.(*int32), -1)
if atomic.LoadInt32(countPtr.(*int32))
<h3>
<code>golang.org/x/net/netutil.LimitListener</code> 只能限总连接数</h3>
<p>这个包提供的 <code>LimitListener</code> 是对底层 <code>net.Listener</code> 做并发连接数限制,比如 <code>http.Server{Listener: netutil.LimitListener(lis, 100)}</code>,但它统计的是所有来源的 TCP 连接总数,不是每个 IP 的<a style="color:#f60; text-decoration:underline;" title="并发请求" href="https://m.php.cn/zt/46006.html" target="_blank">并发请求</a>数。HTTP/2 多路复用下,一个连接可承载多个请求,此时它更不匹配“每 IP 并发请求数”的语义。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill6460" title="Golang Naming"><img
src="https://img.php.cn/upload/skill/000/000/081/179094616043400.jpg" alt="Golang Naming" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill6460" title="Golang Naming" class="overflowclass">Golang Naming</a>
<p class="overflowclass">Go(Golang)命名规范 — 包括包、构造函数、结构体、接口、常量、枚举、错误、布尔值、接收器、getter/setter、函数等。</p>
</div>
<a rel="nofollow" href="/xiazai/skill6460" title="Golang Naming" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>
- 适用场景:防止服务器被大量 TCP 握手打爆,而非防单 IP 高频请求
- 无法区分 IP,也无法感知 HTTP 请求生命周期(只管连接建立)
- 与中间件方案不冲突,可叠加使用:前者控连接层,后者控应用层
生产环境要注意 realIP 的可信边界和内存泄漏
高频访问下,sync.Map 中未归零的 IP 计数器可能长期驻留,尤其当客户端断连不规范(如直接 kill 连接),导致 count 卡在 >0 状态无法清理。这不是理论问题,是真实发生过的内存缓慢增长现象。
- 必须设置定时清理逻辑:比如每分钟扫描
sync.Map,删除值为 0 的项(Load后判断再Delete) -
realIP函数不能盲目信任X-Forwarded-For—— 若没配 Nginx 的set_real_ip_from或 Cloudflare 的真实 IP header,就可能被伪造 - IPv6 地址带端口(如
[2001:db8::1]:12345)需截掉端口部分,只保留 IP 段用于计数
真正难的不是写几行计数代码,而是让这套逻辑在长周期、混合代理、偶发网络异常的环境下不漏判、不误杀、不涨内存。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










