跳转到内容

反向代理

此示例演示如何在你的应用(如 WordPress、Node.js、Java、Python、Ruby 或 Go)前使用 Echo 作为反向代理和负载均衡器。为简单起见,这里的上游也是处理 WebSocket 的 Go 服务器。

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 })

在 cookbook/reverse-proxy 目录中,分别在两个终端启动上游服务器:

终端窗口
go run ./upstream server1 :8081
终端窗口
go run ./upstream server2 :8082

在第三个终端中,同样进入 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 })
}

Proxy 从 Context#Scheme() 设置 X-Forwarded-Proto 并删除旧协议标头。v5 的 X-Real-IP 来自 Context#RealIP()。如果 Echo 前面还有代理,请配置协议和 IP 提取器,确保上游收到正确的值。