Golang 中的反向代理(ReverseProxy) 介紹與使用


Golang 中反向代理的實現主要使用了標准庫的 net/http/httputil 包。
當讀完這篇文章之后,你會學到:

  • 如何響應 HTTP 請求
  • 如何解析請求體
  • 如何通過反向代理將流量轉發到另一台服務器

反向代理的概念

反向代理是什么?有個很棒的說法是流量轉發。我獲取到客戶端來的請求,將它發往另一個服務器,從服務器獲取到響應再回給原先的客戶端。反向的意義簡單來說在於這個代理自身決定了何時將流量發往何處。

它們讓你可以控制來自客戶端的請求和來自服務器的響應,然后我們利用這個特點,可以增加緩存、做一些提高網站的安全性措施等。

正向代理和反向代理的區別

在我們深入了解有關反向代理之前,讓我們快速看普通代理(也稱為正向代理)和反向代理之間的區別。

正向代理中,代理代表原始客戶端從另一個網站檢索數據。 它位於客戶端(瀏覽器)前面,並確保沒有后端服務器直接與客戶端通信。 所有客戶端的請求都通過代理被轉發,因此服務器只與這個代理通信(服務器會認為代理是它的客戶端)。 在這種情況下,代理可以隱藏真正的客戶端。

另一方面,反向代理位於后端服務器的前面,確保沒有客戶端直接與服務器通信。 所有客戶端請求都會通過反向代理發送到服務器,因此客戶端始終只與反向代理通信, 而從不會直接與實際服務器通信。 在這種情況下,代理可以隱藏后端服務器。 幾個常見的反向代理有 Nginx, HAProxy。

反向代理使用場景

負載均衡(Load balancing): 反向代理可以提供負載均衡解決方案,將傳入的流量均勻地分布在不同的服務器之間,以防止單個服務器過載。

防止安全攻擊: 由於真正的后端服務器永遠不需要暴露公共 IP,所以 DDoS 等攻擊只能針對反向代理進行, 這能確保在網絡攻擊中盡量多的保護你的資源,真正的后端服務器始終是安全的。

緩存: 假設你的實際服務器與用戶所在的地區距離比較遠,那么你可以在當地部署反向代理,它可以緩存網站內容並為當地用戶提供服務。

SSL 加密: 由於與每個客戶端的 SSL 通信會耗費大量的計算資源,因此可以使用反向代理處理所有與 SSL 相關的內容, 然后釋放你真正服務器上的寶貴資源。

編寫一個反向代理的案例

我們來實際寫一下案例。我們需要一個 Web 服務器(http://localhost:1330)能夠提供以下功能:

  1. 獲取到請求
  2. 讀取請求體,特別是 proxy_condition 字段(也叫做代理域)
  3. 如果 proxy_condition 字段的值為 A,則轉發到 http://localhost:1331
  4. 如果 proxy_condition 字段的值為 B,則轉發到 http://localhost:1332
  5. 否則,則轉發到默認的 URL (http://localhost:1333)

在這里,http://localhost:1330 就相當於一個反向代理服務器,發送到 1330 的請求將會根據 request body 中的 proxy_condition 字段對應的值,被轉發到對應的后端服務器中。

環境准備

  1. Go 語言環境。
  2. http-server,用來創建簡單的 HTTP 服務器。安裝教程可以看這篇

基礎工作

我們創建 main.go 文件做如下事情:

  1. PORTA_CONDITION_URLB_CONDITION_URLDEFAULT_CONDITION_URL 變量通過日志打印到控制台。
  2. / 路徑上監聽請求。

注:原文中的這幾個變量是寫入在系統環境變量中的,后續從系統環境變量中讀取。這里為了精簡,將其寫成了常量的形式。

package main

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
)

const PORT = "1330"
const A_CONDITION_URL = "http://localhost:1331"
const B_CONDITION_URL = "http://localhost:1332"
const DEFAULT_CONDITION_URL = "http://localhost:1333"

type requestPayloadStruct struct {
    ProxyCondition string `json:"proxy_condition"`
}

// Get the port to listen on
func getListenAddress() string {
    return ":" + PORT
}

// Log the env variables required for a reverse proxy
func logSetup() {
    a_condtion_url := A_CONDITION_URL
    b_condtion_url := B_CONDITION_URL
    default_condtion_url := DEFAULT_CONDITION_URL

    log.Printf("Server will run on: %s\n", getListenAddress())
    log.Printf("Redirecting to A url: %s\n", a_condtion_url)
    log.Printf("Redirecting to B url: %s\n", b_condtion_url)
    log.Printf("Redirecting to Default url: %s\n", default_condtion_url)
}

// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
  // We will get to this...
}

func main() {
    // Log setup values
    logSetup()

    // start server
    http.HandleFunc("/", handleRequestAndRedirect)
    if err := http.ListenAndServe(getListenAddress(), nil); err != nil {
        panic(err)
    }
}

現在你就可以運行代碼了。

解析請求體

有了項目的基本骨架之后,我們需要添加邏輯來處理解析請求的請求體部分。更新 handleRequestAndRedirect 函數來從請求體中解析出 proxy_condition 字段。

type requestPayloadStruct struct {
    ProxyCondition string `json:"proxy_condition"`
}

// Get a json decoder for a given requests body
func requestBodyDecoder(request *http.Request) *json.Decoder {
    // Read body to buffer
    body, err := ioutil.ReadAll(request.Body)
    if err != nil {
        log.Printf("Error reading body: %v", err)
        panic(err)
    }

    // Because go lang is a pain in the ass if you read the body then any susequent calls
    // are unable to read the body again....
    request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

    return json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
}

// Parse the requests body
func parseRequestBody(request *http.Request) requestPayloadStruct {
    decoder := requestBodyDecoder(request)

    var requestPayload requestPayloadStruct
    err := decoder.Decode(&requestPayload)

    if err != nil {
        panic(err)
    }

    return requestPayload
}

// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
    requestPayload := parseRequestBody(req)
      // ... more to come
}

通過 proxy_condition 判斷將流量發往何處

現在我們從請求中取得了 proxy_condition 的值,可以根據它來判斷我們要反向代理到何處。記住上文我們提到的三種情形:

如果 proxy_condition 值為 A,我們將流量發送到 A_CONDITION_URL
如果 proxy_condition 值為 B,我們將流量發送到 B_CONDITION_URL
其他情況將流量發送到 DEFAULT_CONDITION_URL

// Log the typeform payload and redirect url
func logRequestPayload(requestionPayload requestPayloadStruct, proxyUrl string) {
    log.Printf("proxy_condition: %s, proxy_url: %s\n", requestionPayload.ProxyCondition, proxyUrl)
}

// Get the url for a given proxy condition
func getProxyUrl(proxyConditionRaw string) string {
    proxyCondition := strings.ToUpper(proxyConditionRaw)

    a_condtion_url := A_CONDITION_URL
    b_condtion_url := B_CONDITION_URL
    default_condtion_url := DEFAULT_CONDITION_URL

    if proxyCondition == "A" {
        return a_condtion_url
    }

    if proxyCondition == "B" {
        return b_condtion_url
    }

    return default_condtion_url
}

// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
    requestPayload := parseRequestBody(req)
    url := getProxyUrl(requestPayload.ProxyCondition)
    logRequestPayload(requestPayload, url)
  // more still to come...
}

反向代理到 URL

最終我們來到了實際的反向代理部分。在如此多的語言中要編寫一個反向代理需要考慮很多東西,寫大段的代碼。或者至少引入一個復雜的外部庫。

然而 Go 的標准庫使得創建一個反向代理非常簡單以至於你都不敢相信。下面就是你所需要的最關鍵的一行代碼:

httputil.NewSingleHostReverseProxy(url).ServeHTTP(res, req)

注意下面代碼中我們做了些許修改來讓它能完整地支持 SSL 重定向(雖然不是必須的,只使用上面那一句代碼也是可以的)。

// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
    // parse the url
    url, _ := url.Parse(target)

    // create the reverse proxy
    proxy := httputil.NewSingleHostReverseProxy(url)

    // Update the headers to allow for SSL redirection
    req.URL.Host = url.Host
    req.URL.Scheme = url.Scheme
    req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
    req.Host = url.Host

    // Note that ServeHttp is non blocking and uses a go routine under the hood
    proxy.ServeHTTP(res, req)
}

// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
    requestPayload := parseRequestBody(req)
    url := getProxyUrl(requestPayload.ProxyCondition)

    logRequestPayload(requestPayload, url)

    serveReverseProxy(url, res, req)
}

完整代碼

現在再次給出完整代碼:

package main

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
    "strings"
)

const PORT = "1330"
const A_CONDITION_URL = "http://localhost:1331"
const B_CONDITION_URL = "http://localhost:1332"
const DEFAULT_CONDITION_URL = "http://localhost:1333"

type requestPayloadStruct struct {
    ProxyCondition string `json:"proxy_condition"`
}

// Get the port to listen on
func getListenAddress() string {
    return ":" + PORT
}

// Log the env variables required for a reverse proxy
func logSetup() {
    a_condtion_url := A_CONDITION_URL
    b_condtion_url := B_CONDITION_URL
    default_condtion_url := DEFAULT_CONDITION_URL

    log.Printf("Server will run on: %s\n", getListenAddress())
    log.Printf("Redirecting to A url: %s\n", a_condtion_url)
    log.Printf("Redirecting to B url: %s\n", b_condtion_url)
    log.Printf("Redirecting to Default url: %s\n", default_condtion_url)
}

// Get a json decoder for a given requests body
func requestBodyDecoder(request *http.Request) *json.Decoder {
    // Read body to buffer
    body, err := ioutil.ReadAll(request.Body)
    if err != nil {
        log.Printf("Error reading body: %v", err)
        panic(err)
    }

    // Because go lang is a pain in the ass if you read the body then any susequent calls
    // are unable to read the body again....
    request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

    return json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
}

// Parse the requests body
func parseRequestBody(request *http.Request) requestPayloadStruct {
    decoder := requestBodyDecoder(request)

    var requestPayload requestPayloadStruct
    err := decoder.Decode(&requestPayload)

    if err != nil {
        panic(err)
    }

    return requestPayload
}

// Log the typeform payload and redirect url
func logRequestPayload(requestionPayload requestPayloadStruct, proxyUrl string) {
    log.Printf("proxy_condition: %s, proxy_url: %s\n", requestionPayload.ProxyCondition, proxyUrl)
}

// Get the url for a given proxy condition
func getProxyUrl(proxyConditionRaw string) string {
    proxyCondition := strings.ToUpper(proxyConditionRaw)

    a_condtion_url := A_CONDITION_URL
    b_condtion_url := B_CONDITION_URL
    default_condtion_url := DEFAULT_CONDITION_URL

    if proxyCondition == "A" {
        return a_condtion_url
    }

    if proxyCondition == "B" {
        return b_condtion_url
    }

    return default_condtion_url
}

// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
    // parse the url
    url, _ := url.Parse(target)

    // create the reverse proxy
    proxy := httputil.NewSingleHostReverseProxy(url)

    // Update the headers to allow for SSL redirection
    //req.URL.Host = url.Host
    //req.URL.Scheme = url.Scheme
    //req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
    //req.Host = url.Host

    // Note that ServeHttp is non blocking and uses a go routine under the hood
    proxy.ServeHTTP(res, req)
}

// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
    requestPayload := parseRequestBody(req)
    url := getProxyUrl(requestPayload.ProxyCondition)

    logRequestPayload(requestPayload, url)

    serveReverseProxy(url, res, req)
}

func main() {
    // Log setup values
    logSetup()

    // start server
    http.HandleFunc("/", handleRequestAndRedirect)
    if err := http.ListenAndServe(getListenAddress(), nil); err != nil {
        panic(err)
    }
}

全部啟動

好了,現在啟動我們的反向代理程序讓其監聽 1330 端口。讓其他的 3 個簡單的服務分別監聽 1331–1333 端口(在各自的終端中)。

# 終端1:運行代碼,監聽 1330 端口
$ go run main.go

# 終端2:啟動一個 http 服務器,監聽 1331 端口
$ http-server -p 1331

# 終端3:啟動一個 http 服務器,監聽 1332 端口
$ http-server -p 1332

# 終端4:啟動一個 http 服務器,監聽 1333 端口
$ http-server -p 1333

使用 curl 命令進行測試

這些服務都啟動之后,我們就可以在另一個終端中像下面這樣開始發送帶有 JSON 體的請求了:

$ curl --request GET \
  --url http://localhost:1330/ \
  --header 'content-type: application/json' \
  --data '{
    "proxy_condition": "a"
  }'

我們就會看到我們的反向代理將流量轉發給了我們根據 proxy_condition 字段配置的 3 台服務中的其中一台。

上述測試代碼可以在終端1的輸出中看到被轉發到的后端服務器的地址:

image

同時,在終端2看到發來的請求的輸出:

image

如果你在找一個好用的 HTTP 請求客戶端,我極力推薦 Insomnia

使用 Insomnia 進行測試

Insomnia 的安裝教程可以看這篇:Ubuntu 安裝 Insomnia

測試過程如圖所示:

我們可以看到我們的反向代理將流量轉發給了我們根據 proxy_condition 字段配置的 3 台服務中的其中一台。

使用進階

修改 Response 信息

httputil 反向代理為我們提供了一種非常簡單的機制來修改我們從服務器獲得的響應,可以根據你的應用場景來緩存或更改此響應,讓我們看看應該如何實現:

// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
    // parse the url
    url, _ := url.Parse(target)

    // create the reverse proxy
    proxy := httputil.NewSingleHostReverseProxy(url)

    // modify the response
    proxy.ModifyResponse = modifyResponse

    // Note that ServeHttp is non blocking and uses a go routine under the hood
    proxy.ServeHTTP(res, req)
}

func modifyResponse(resp *http.Response) error {
    resp.Header.Set("X-Proxy", "Magical")
    return nil
}

在 modifyResponse() 方法中可以看到 ,我們設置了自定義 Header 頭 X-Proxy。 同樣,你也可以讀取響應體正文,並對其進行更改或緩存,然后將其設置回客戶端。

ReverseProxy.ServeHTTP() 方法的底層會調用 ReverseProxy對象的 modifyResponse() 方法。

現在我們來測試一下,下圖是還未添加 modifyResponse 時的輸出結果,可以看到此時的響應頭中沒有 X-Proxy 的信息。

image

添加 modifyResponse 后,重新運行程序,輸出結果如下圖所示,可以看到 Response Header 中成功被添加了 X-Proxy: Magical

image

也可以在右側的 Timeline 窗口看到請求和響應信息,如下圖所示:

image

modifyResponse 中的錯誤處理

在 modifyResponse 中,可以返回一個錯誤(如果你在處理響應發生了錯誤),如果你設置了 proxy.ErrorHandler, modifyResponse 返回錯誤時會自動調用 ErrorHandler 進行錯誤處理。

// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
    // parse the url
    url, _ := url.Parse(target)

    // create the reverse proxy
    proxy := httputil.NewSingleHostReverseProxy(url)

    // modify the response
    proxy.ModifyResponse = modifyResponse
    proxy.ErrorHandler = errorHandler

    // Note that ServeHttp is non blocking and uses a go routine under the hood
    proxy.ServeHTTP(res, req)
}

func modifyResponse(*http.Response) error {
    return errors.New("response body is invalid")
}

func errorHandler(resp http.ResponseWriter, req *http.Request, err error) {
    fmt.Printf("Got error while modifying response: %v \n", err)
    return
}

測試結果:

image

修改 Request 信息

你也可以在將請求發送到服務器之前對其進行修改。在下面的例子中,我們將會在請求發送到服務器之前添加了一個 Header 頭。同樣的,你可以在請求發送之前對其進行任何更改。

// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
    // parse the url
    url, _ := url.Parse(target)

    // create the reverse proxy
    proxy := httputil.NewSingleHostReverseProxy(url)

    // modify the request
    originalDirector := proxy.Director
    proxy.Director = func(req *http.Request) {
        originalDirector(req)
        modifyRequest(req)
    }

    // Note that ServeHttp is non blocking and uses a go routine under the hood
    proxy.ServeHTTP(res, req)
}

func modifyRequest(req *http.Request) {
    req.Header.Set("X-Proxy", "Simple-Reverse-Proxy")
}

修改請求頭之后,在測試的時候看不到修改成功后的請求頭信息,這個問題暫時放着,以后真正遇到后再來這里補充。

反向代理非常強大,如文章之前所說,它有很多應用場景。你可以根據你的情況對其進行自定義。

推薦閱讀

golang 反向代理reverseproxy源碼分析 - 拾月凄辰 - 博客園 (cnblogs.com)

參考文章

Go 簡單而強大的反向代理(Reverse Proxy) | 東隅已逝/桑榆非晚 (h1z3y3.me)
Go 代碼實現反向代理 - Go語言中文網 - Golang中文社區 (studygolang.com)
http-server (github.com)
Insomnia (github.com)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM