反向代理
此示例演示如何在你的应用(如 WordPress、Node.js、Java、Python、Ruby 或 Go)前使用 Echo 作为反向代理和负载均衡器。为简单起见,这里的上游也是处理 WebSocket 的 Go 服务器。
1) 确定上游目标 URL
Section titled “1) 确定上游目标 URL”url1, err := url.Parse("http://localhost:8081")if err != nil { e.Logger.Error("failed to parse url", "error", err) return}url2, err := url.Parse("http://localhost:8082")if err != nil { e.Logger.Error("failed to parse url", "error", err) return}targets := []*middleware.ProxyTarget{ {URL: url1}, {URL: url2},}2) 使用上游目标设置代理中间件
Section titled “2) 使用上游目标设置代理中间件”下面的片段使用轮询负载均衡。你也可以使用 middleware.NewRandomBalancer()。
e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))要为子路由设置代理,请使用 Echo#Group()。
// Canonicalize the group root so relative WebSocket URLs stay under /blog/.e.Any("/blog", func(c *echo.Context) error { location := "/blog/" if query := c.Request().URL.RawQuery; query != "" { location += "?" + query } return c.Redirect(http.StatusPermanentRedirect, location)})g := e.Group("/blog")g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Balancer: middleware.NewRoundRobinBalancer(targets), Rewrite: map[string]string{"/blog/*": "/$1"},}))// Group middleware needs a matching route. The proxy handles the response.g.Any("/*", func(c *echo.Context) error { return echo.ErrNotFound })3) 启动上游服务器
Section titled “3) 启动上游服务器”在 cookbook/reverse-proxy 目录中,分别在两个终端启动上游服务器:
go run ./upstream server1 :8081go run ./upstream server2 :80824) 启动代理服务器
Section titled “4) 启动代理服务器”在第三个终端中,同样进入 cookbook/reverse-proxy 目录,启动代理:
go run .访问 http://localhost:1323,你应该会看到网页中 HTTP 请求由 “server 1” 提供,
WebSocket 请求由 “server 2” 提供。
HTTP
Hello from upstream server server1
WebSocket
Hello from upstream server server2!Hello from upstream server server2!Hello from upstream server server2!如需使用分组模式,请停止默认代理,然后运行:
go run . -grouped访问 http://localhost:1323/blog/。代理会移除 /blog 后再转发请求;页面的相对 WebSocket URL 会保持在 /blog/ 下。
package main
import ( "context" "fmt" "net/http" "os" "time"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "golang.org/x/net/websocket")
var index = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Upstream Server</title> <style> h1, p { font-weight: 300; } </style> </head> <body> <h1>HTTP</h1> <p> Hello from upstream server %s </p> <h1>WebSocket</h1> <p id="output"></p> <script> var wsURL = new URL('ws', window.location.href) wsURL.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' var ws = new WebSocket(wsURL)
ws.onmessage = function(evt) { var out = document.getElementById('output'); out.innerHTML += evt.data + '<br>'; } </script> </body> </html>`
func main() { name := os.Args[1] port := os.Args[2] e := echo.New()
e.Use(middleware.RequestLogger()) e.Use(middleware.Recover())
e.GET("/", func(c *echo.Context) error { return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) })
// WebSocket handler e.GET("/ws", func(c *echo.Context) error { websocket.Handler(func(ws *websocket.Conn) { defer ws.Close() for { // Write err := websocket.Message.Send(ws, fmt.Sprintf("Hello from upstream server %s!", name)) if err != nil { e.Logger.Error("failed to send message", "error", err) } select { case <-ws.Request().Context().Done(): return case <-time.After(1 * time.Second): continue } } }).ServeHTTP(c.Response(), c.Request()) return nil })
sc := echo.StartConfig{Address: port} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) }}package main
import ( "context" "flag" "net/http" "net/url"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware")
func main() { grouped := flag.Bool("grouped", false, "limit the proxy to the /blog route group") flag.Parse() e := echo.New() e.Use(middleware.RequestLogger()) e.Use(middleware.Recover())
// Setup proxy url1, err := url.Parse("http://localhost:8081") if err != nil { e.Logger.Error("failed to parse url", "error", err) return } url2, err := url.Parse("http://localhost:8082") if err != nil { e.Logger.Error("failed to parse url", "error", err) return } targets := []*middleware.ProxyTarget{ {URL: url1}, {URL: url2}, } if *grouped { configureBlogProxy(e, targets) } else { e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) }
sc := echo.StartConfig{Address: ":1323"} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) }}
// configureBlogProxy limits the proxy middleware to a route group.func configureBlogProxy(e *echo.Echo, targets []*middleware.ProxyTarget) { // Canonicalize the group root so relative WebSocket URLs stay under /blog/. e.Any("/blog", func(c *echo.Context) error { location := "/blog/" if query := c.Request().URL.RawQuery; query != "" { location += "?" + query } return c.Redirect(http.StatusPermanentRedirect, location) }) g := e.Group("/blog") g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Balancer: middleware.NewRoundRobinBalancer(targets), Rewrite: map[string]string{"/blog/*": "/$1"}, })) // Group middleware needs a matching route. The proxy handles the response. g.Any("/*", func(c *echo.Context) error { return echo.ErrNotFound })}Echo 安全更新 (v5.4.0 / v4.16.0)
Section titled “Echo 安全更新 (v5.4.0 / v4.16.0)”Proxy 从 Context#Scheme() 设置 X-Forwarded-Proto 并删除旧协议标头。v5 的 X-Real-IP 来自 Context#RealIP()。如果 Echo 前面还有代理,请配置协议和 IP 提取器,确保上游收到正确的值。