暫且放下你的程式設計語言來瞻仰下我所見過的最棒的標準庫。![This is all the code you actually require…](https://raw.githubusercontent.com/studygolang/gctt-images/master/reverse-proxy/1_y3GxXdKfZlqa95bl19Rytg.png)為項目選擇程式設計語言和挑選你最愛的球隊不一樣。應該從實用主義出發,根據特定的工作選擇合適的工具。在這篇文章中我會告訴你從何時開始並且為什麼我認為 Go 語言如此閃耀,具體來說是它的標準庫對於基本的網路編程來說顯得非常穩固。更具體一點,我們將要編寫一個反向 Proxy程式。> **Go 為此提供了很多,但真正支撐起它的在於這些低級的網路管道任務,沒有更好的語言了。**反向 Proxy是什嗎?**有個很棒的說法是流量轉寄**。我擷取到用戶端來的請求,將它發往另一個伺服器,從伺服器擷取到響應再回給原先的用戶端。反向的意義簡單來說在於這個代理自身決定了何時將流量發往何處。![Just beautiful](https://raw.githubusercontent.com/studygolang/gctt-images/master/reverse-proxy/0_R_W7P1UV4jQEf1j5.gif)為什麼這很有用?因為反向 Proxy的概念是如此簡單以至於它可以被應用於許多不同的情境:負載平衡,A/B 測試,快取,驗證等等。當讀完這篇文章之後,你會學到:* 如何響應 HTTP 要求* 如何解析請求體* 如何通過反向 Proxy將流量轉寄到另一台伺服器## 我們的反向 Proxy項目我們來實際寫一下項目。我們需要一個 Web 服務器能夠提供以下功能:1. 擷取到請求2. 讀取請求體,特別是 `proxy_condition` 欄位3. 如果代理域為 `A`,則轉寄到 URL 14. 如果代理域為 `B`,則轉寄到 URL 25. 如果代理域都不是以上,則轉寄到預設的 URL### 準備工作* [Go](https://golang.org) 語言環境。* [http-server](https://www.npmjs.com/package/http-server) 用來建立簡單的服務。### 環境配置我們要做的第一件事是將我們的配置資訊寫入環境變數,如此就可以使用它們而不必寫死在我們的原始碼中。我發現最好的方式是建立一個包含所需環境變數的 `.env` 檔案。以下就是我為特定項目編寫的檔案內容:```bashexport PORT=1330export A_CONDITION_URL="http://localhost:1331"export B_CONDITION_URL="http://localhost:1332"export DEFAULT_CONDITION_URL="http://localhost:1333"```> 這是我從 [12 Factor App](https://12factor.net/config) 項目中獲得的技巧。儲存完 `.env` 檔案之後就可以運行:```bashsource .env```在任何時候都可以運行該指令來將配置載入進環境變數。### 項目基礎工作接著我們建立 `main.go` 檔案做如下事情:1. 將 `PORT`,`A_CONDITION_URL`,`B_CONDITION_URL` 和 `DEFAULT_CONDITION_URL` 變數通過日誌列印到控制台。2. 在 `/` 路徑上監聽請求:```gopackage mainimport ("bytes""encoding/json""io/ioutil""log""net/http""net/http/httputil""net/url""os""strings")// Get env var or defaultfunc getEnv(key, fallback string) string {if value, ok := os.LookupEnv(key); ok {return value}return fallback}// Get the port to listen onfunc getListenAddress() string {port := getEnv("PORT", "1338")return ":" + port}// Log the env variables required for a reverse proxyfunc logSetup() {a_condtion_url := os.Getenv("A_CONDITION_URL")b_condtion_url := os.Getenv("B_CONDITION_URL")default_condtion_url := os.Getenv("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 urlfunc handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) { // We will get to this...}func main() {// Log setup valueslogSetup()// start serverhttp.HandleFunc("/", handleRequestAndRedirect)if err := http.ListenAndServe(getListenAddress(), nil); err != nil {panic(err)}}```現在你就可以運行代碼了。### 解析請求體有了項目的基本骨架之後,我們需要添加邏輯來處理解析請求的請求體部分。更新 `handleRequestAndRedirect` 函數來從請求體中解析出 `proxy_condition` 欄位。```gotype requestPayloadStruct struct {ProxyCondition string `json:"proxy_condition"`}// Get a json decoder for a given requests bodyfunc requestBodyDecoder(request *http.Request) *json.Decoder {// Read body to bufferbody, 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 bodyfunc parseRequestBody(request *http.Request) requestPayloadStruct {decoder := requestBodyDecoder(request)var requestPayload requestPayloadStructerr := decoder.Decode(&requestPayload)if err != nil {panic(err)}return requestPayload}// Given a request send it to the appropriate urlfunc handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {requestPayload := parseRequestBody(req) // ... more to come}```### 通過 proxy_condition 判斷將流量發往何處現在我們從請求中取得了 `proxy_condition` 的值,可以根據它來判斷我們要反向 Proxy到何處。記住上文我們提到的三種情形:1. 如果 `proxy_condition` 值為 `A`,我們將流量發送到 `A_CONDITION_URL`2. 如果 `proxy_condition` 值為 `B`,我們將流量發送到 `B_CONDITION_URL`3. 其他情況將流量發送到 `DEFAULT_CONDITION_URL````go// Log the typeform payload and redirect urlfunc logRequestPayload(requestionPayload requestPayloadStruct, proxyUrl string) {log.Printf("proxy_condition: %s, proxy_url: %s\n", requestionPayload.ProxyCondition, proxyUrl)}// Get the url for a given proxy conditionfunc getProxyUrl(proxyConditionRaw string) string {proxyCondition := strings.ToUpper(proxyConditionRaw)a_condtion_url := os.Getenv("A_CONDITION_URL")b_condtion_url := os.Getenv("B_CONDITION_URL")default_condtion_url := os.Getenv("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 urlfunc handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {requestPayload := parseRequestBody(req)url := getProxyUrl(requestPayload.ProxyCondition)logRequestPayload(requestPayload, url) // more still to come...}```### 反向 Proxy到 URL最終我們來到了實際的反向 Proxy部分。在如此多的語言中要編寫一個反向 Proxy需要考慮很多東西,寫大段的代碼。或者至少引入一個複雜的外部庫。然而 Go 的標準庫使得建立一個反向 Proxy非常簡單以至於你都不敢相信。下面就是你所需要的最關鍵的一行代碼:```gohttputil.NewSingleHostReverseProxy(url).ServeHTTP(res, req)```注意下面代碼中我們做了些許修改來讓它能完整地支援 SSL 重新導向(雖然不是必須的)。```go// Serve a reverse proxy for a given urlfunc serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {// parse the urlurl, _ := url.Parse(target)// create the reverse proxyproxy := httputil.NewSingleHostReverseProxy(url)// Update the headers to allow for SSL redirectionreq.URL.Host = url.Hostreq.URL.Scheme = url.Schemereq.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 hoodproxy.ServeHTTP(res, req)}// Given a request send it to the appropriate urlfunc handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {requestPayload := parseRequestBody(req)url := getProxyUrl(requestPayload.ProxyCondition)logRequestPayload(requestPayload, url)serveReverseProxy(url, res, req)}```### 全部啟動好了,現在啟動我們的反向 Proxy程式讓其監聽 `1330` 連接埠。讓其他的 3 個簡單的服務分別監聽 `1331–1333` 連接埠(在各自的終端中)。1. `source .env && go install && $GOPATH/bin/reverse-proxy-demo`2. `http-server -p 1331`3. `http-server -p 1332`4. `http-server -p 1333`這些服務都啟動之後,我們就可以在另一個終端中像下面這樣開始發送帶有 JSON 體的請求了:```bashcurl --request GET \ --url http://localhost:1330/ \ --header 'content-type: application/json' \ --data '{ "proxy_condition": "a" }'```> 如果你在找一個好用的 HTTP 要求用戶端,我極力推薦 [Insomnia](https://insomnia.rest)。然後我們就會看到我們的反向 Proxy將流量轉寄給了我們根據 `proxy_condition` 欄位配置的 3 台服務中的其中一台。![Its alive!!!](https://raw.githubusercontent.com/studygolang/gctt-images/master/reverse-proxy/1_TcyJh0qtYv2N3UOBVVfd0Q.gif)### 總結Go 為此提供了很多,但真正支撐起它的在於這些低級的網路管道任務,沒有更好的語言了。我們寫的這個程式簡單,高效能,可靠並且隨時可用於生產環境。我能看到在以後我會經常使用 Go 來編寫簡單的服務。> 代碼是開源的,你可以在 [Github](https://github.com/bechurch/reverse-proxy-demo) 上找到。> ️ 在 [Twitter](https://www.twitter.com/bnchrch) 上我只聊關於編程和遠程工作相關的東西。如果關注我,你不會後悔的。
via: https://hackernoon.com/writing-a-reverse-proxy-in-just-one-line-with-go-c1edfa78c84b
作者:Ben Church 譯者:alfred-zhong 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
373 次點擊 ∙ 2 贊