Golang中使用gorilla mux实现限流器
Golang中使用gorilla mux实现限流器 我正在尝试实现一个HTTP请求限制器,以允许每秒10个请求。 以下是我参考rate-limit所实现的代码。
// Run a background goroutine to remove old entries from the visitors map.
func init() {
fmt.Println("This will get called on main initialization")
go cleanupVisitors()
}
func getVisitor(username string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()
v, exists := visitors[username]
if !exists {
//rt := rate.Every(1 * time.Minute)
//limiter := rate.NewLimiter(rt, 1)
limiter := rate.NewLimiter(5, 3)
// Include the current time when creating a new visitor.
visitors[username] = &visitor{limiter, time.Now()}
return limiter
}
// Update the last seen time for the visitor.
v.lastSeen = time.Now()
return v.limiter
}
// Every minute check the map for visitors that haven't been seen for
// more than 3 minutes and delete the entries.
func cleanupVisitors() {
for {
time.Sleep(time.Minute)
mu.Lock()
for username, v := range visitors {
if time.Since(v.lastSeen) > 1*time.Minute {
delete(visitors, username)
}
}
mu.Unlock()
}
}
func limit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mappedArray := hotelapi.SearchResponse{}
mappedArray.StartTime = time.Now().Format("2006-02-01 15:04:05.000000")
mappedArray.EndTime = time.Now().Format("2006-02-01 15:04:05.000000")
userName := r.FormValue("username")
limiter := getVisitor(userName)
if !limiter.Allow() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
mappedArray.MessageInfo = http.StatusText(http.StatusTooManyRequests)
mappedArray.ErrorCode = strconv.Itoa(http.StatusTooManyRequests)
json.NewEncoder(w).Encode(mappedArray)
return
}
next.ServeHTTP(w, r)
})
}
func route() {
r := mux.NewRouter()
r.PathPrefix("/hello").HandlerFunc(api.ProcessSupplier).Methods("GET")
ws := r.PathPrefix("/index.php").HandlerFunc(api.ProcessWebService).Methods("GET", "POST").Subrouter()
r.Use(panicRecovery)
ws.Use(limit)
http.HandleFunc("/favicon.ico", faviconHandler)
if config.HTTPSEnabled {
err := http.ListenAndServeTLS(":"+config.Port, config.HTTPSCertificateFilePath, config.HTTPSKeyFilePath, handlers.CompressHandlerLevel(r, gzip.BestSpeed))
if err != nil {
fmt.Println(err)
log.Println(err)
}
} else {
err := http.ListenAndServe(":"+config.Port, limit(handlers.CompressHandler(r)))
if err != nil {
fmt.Println(err)
log.Println(err)
}
}
}
我在这里有几个疑问。
- 我只想对 /index.php 路径进行限制,而不是 /hello。我使用了子路由来实现。这种方式正确吗?
- 限制中间件并没有按我预期的那样工作。它只允许1个成功的请求,其他所有请求都返回了“请求过多”的错误。
我在这里遗漏了什么吗?
更多关于Golang中使用gorilla mux实现限流器的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的。在前10个请求内,它返回了过多的请求。 让我尝试使用LimitByIP。 同时,我将分享路由器的设置。
更多关于Golang中使用gorilla mux实现限流器的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
但是你在哪里应用了为某些用户跳过限制器的逻辑?我没有看到。你应该在 WithKeyFuncs 中实现这个功能。
那么,你必须使用那个效果不佳的第一种方法,调试问题所在,或者最终向 go-chi/httprate 报告一个 issue。同时,检查那里的示例和测试。
你的意思是,即使在最初的10个请求内,它也返回了过多的请求吗?如果是这样,请提供路由器的完整配置。你也可以尝试一个简单的配置:
ws.Use(httprate.LimitByIP(3, 5*time.Second))
而不是上面那个。对于上面那个,尝试设置为每10秒100个请求,这可能会产生一些不同。
最好使用惯用的中间件。你不需要使用 chi,你可以直接使用 http 包或 Gorilla mux。
GitHub - go-chi/httprate: net/http 速率限制器中间件
net/http 速率限制器中间件。通过在 GitHub 上创建帐户为 go-chi/httprate 的开发做出贡献。
https://raw.githubusercontent.com/go-chi/httprate/master/_example/main.go
r.Use(httprate.Limit(
10,
10*time.Second,
httprate.WithKeyFuncs(httprate.KeyByIP, func(r *http.Request) (string, error) {
token := r.Context().Value("userID").(string)
return token, nil
}),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
// We can send custom responses for the rate limited requests, e.g. a JSON message
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"error": "Too many requests"}`))
}),
))
你需要修改 httprate.WithKeyFuncs 来使用你自己的方式获取用户ID(例如从请求头、Cookie、查询参数或其他地方)。
我这样做了。出于测试目的,我传递了一个随机文本作为令牌(不是实际的用户名),但奇怪的是,限流器仍然对其生效。 我的实际用户名是:ganesh,位于 r.FormValue(“username”) 中。
ws.Use(httprate.Limit(
5,
1*time.Second,
httprate.WithKeyFuncs(func(r *http.Request) (string, error) {
return "test", nil
}),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"error": "Too many requests"}`))
}),
))
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 1
X-Ratelimit-Limit: 5
X-Ratelimit-Remaining: 0
X-Ratelimit-Reset: 1634128918
Date: Wed, 13 Oct 2021 12:41:57 GMT
Content-Length: 271
你的实现有几个关键问题需要解决。让我分析并提供修正方案:
1. 路由配置问题
你的子路由配置是正确的,但中间件应用方式需要调整:
func route() {
r := mux.NewRouter()
// /hello 路径 - 无限制
r.PathPrefix("/hello").HandlerFunc(api.ProcessSupplier).Methods("GET")
// /index.php 路径 - 应用限流中间件
ws := r.PathPrefix("/index.php").Subrouter()
ws.Use(limit) // 只在子路由应用限流
ws.HandleFunc("", api.ProcessWebService).Methods("GET", "POST")
r.Use(panicRecovery)
http.HandleFunc("/favicon.ico", faviconHandler)
// 注意:不要在全局应用 limit 中间件
if config.HTTPSEnabled {
err := http.ListenAndServeTLS(":"+config.Port,
config.HTTPSCertificateFilePath,
config.HTTPSKeyFilePath,
handlers.CompressHandlerLevel(r, gzip.BestSpeed))
if err != nil {
log.Println(err)
}
} else {
err := http.ListenAndServe(":"+config.Port,
handlers.CompressHandler(r)) // 移除全局的 limit()
if err != nil {
log.Println(err)
}
}
}
2. 限流器配置问题
你的主要问题是 rate.NewLimiter(5, 3) 配置。这创建了一个令牌桶:
- 速率:每秒5个令牌
- 桶容量:3个令牌
要实现每秒10个请求,应该使用:
func getVisitor(username string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()
v, exists := visitors[username]
if !exists {
// 创建每秒10个请求的限流器
limiter := rate.NewLimiter(10, 10) // 每秒10个,桶容量10
visitors[username] = &visitor{
limiter: limiter,
lastSeen: time.Now(),
}
return limiter
}
v.lastSeen = time.Now()
return v.limiter
}
3. 完整修正示例
package main
import (
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
"github.com/gorilla/mux"
)
var (
visitors = make(map[string]*visitor)
mu sync.Mutex
)
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}
func getVisitor(username string) *rate.Limiter {
mu.Lock()
defer mu.Unlock()
v, exists := visitors[username]
if !exists {
// 每秒10个请求,桶容量10
limiter := rate.NewLimiter(10, 10)
visitors[username] = &visitor{
limiter: limiter,
lastSeen: time.Now(),
}
return limiter
}
v.lastSeen = time.Now()
return v.limiter
}
func cleanupVisitors() {
for {
time.Sleep(time.Minute)
mu.Lock()
for username, v := range visitors {
if time.Since(v.lastSeen) > 3*time.Minute {
delete(visitors, username)
}
}
mu.Unlock()
}
}
func limit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求中获取用户名
userName := r.FormValue("username")
if userName == "" {
// 如果没有用户名,使用IP地址
userName = r.RemoteAddr
}
limiter := getVisitor(userName)
if !limiter.Allow() {
http.Error(w, http.StatusText(http.StatusTooManyRequests),
http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
go cleanupVisitors()
r := mux.NewRouter()
// 不受限的路由
r.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, no rate limit here!"))
})
// 受限的子路由
ws := r.PathPrefix("/index.php").Subrouter()
ws.Use(limit)
ws.HandleFunc("", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This endpoint is rate limited"))
}).Methods("GET", "POST")
http.ListenAndServe(":8080", r)
}
关键修正点:
- 路由配置:使用
Subrouter().Use(limit)只在特定路径应用限流 - 限流器参数:
rate.NewLimiter(10, 10)实现每秒10个请求 - 中间件位置:确保不在全局路由应用限流中间件
- 清理逻辑:修正了清理函数中的时间判断(3分钟清理)
这样配置后,/index.php 路径会按用户名/IP进行限流(每秒10个请求),而 /hello 路径不受限制。


