最近對 Go 語言的反向代理使用得偏多,其實在大概兩年前就寫過 TCP 層面的代理,而且那時也是用的 Go 語言,不同之處在於之前只是偶爾嘗試一下使用,最近是因為工作需要使用的。相比較於 TCP 層面的代理,HTTP 的代理實現起來麻煩事比較多,如果我們僅僅是簡單的反向代理,OK,那還好,做個 Host 替換就差不多了。但是,很多時候我們作反向代理,那么需求就比較多樣了,例如我們可能希望對代理的響應內容做些改變,也可能希望是 HA 的反向代理。
Go 語言在自身的內置庫中就提供了一個很方便的反向代理組件,使用內置的 httputil 的組件我們可以非常快的開發出一個簡單的反向代理,稍后我們會看到。但是,一旦與我們前面提到的多樣化的需求的時候,就不能簡單得寫代碼了,我們得了解一些組件,然后根據需求對組件進行個性化,從而滿足我們的需求。下面,我就從簡入繁來看下 Go 語言內置的反向代理庫。
內置反向代理
根據 Go 語言官方文檔:Package httputil 中描述,我們可以寫這么一段簡單的代碼,這段代碼是從 Go 語言的官方文檔上抄下來的:
這里我對這段代碼解釋一下:
- Line 2-5: 創建了一個簡單的服務器,響應一些 URL,內容就是里面的字符串
- Line 7-12:這才是真正的創建反向代理的代碼,這里創建了一個反向代理的 ReverProxy 實體
- Line 14-24:這些是仿造一個客戶端請求前面的反向代理,然后獲取輸出的響應
執行一下這段代碼,應該看到的結果是:
this call was relayed by the reverse proxy
Director
看上去反向代理挺簡單的,6 行代碼就已經完成了,而且還帶錯誤處理,OK,是時候做點有意思的事情了,不妨我們就做個代理 baidu.com 的反向代理好了,看看效果。為什么是代理 baidu.com,因為它可以進行很浪的操作啊,自行發揮唄:
一個很簡單的想法就是這么代理,然后我們嘗試運行一下代碼就會發現根!本!行!不!通!那怎么辦,這個時候就是需要你去思考這中間可能的問題了,或者你該利用一些工具分析一下中間環節哪里不一樣。但是,我這里不准備將怎么去發現這個問題,我會告訴你問題就是 HTTP 請求頭不對,所以我們該更改一下請求的請求頭,所以代碼應該修改成這樣:
這里有一個關鍵的地方就是 Line 11,如果你去對比我的這段代碼和 Go 語言自帶的 defaultDirector 的代碼,你也會發現其實就是多了 Line 11 這一行,但是這一行及其關鍵,很多 Server 可能出於各種考慮,會屏蔽掉 Host 不為自己的請求,而我們作為代理需要將這個補上。
OK,運行這段代碼,然后我們就可以通過 localhost:9090 成功訪問 baidu.com 了,看上去應該已經成功了。
ModifyResponse
有時候,我這個代理可能會給別人訪問,於是乎出於版權等因素,我會做一些小手段,例如這里的響應頭:
這個 Server: BWS/1.1 不是很好,我覺得有必要換成我自己的服務器名字:Server:ProjectZoo,於是乎,又有事情可以搞了,要怎么修改這個響應頭呢,Go 語言也給我們提供了,那就是 ModifyResponse,來,嘗試一下:
在途中這個問題我加了幾行代碼,然后再訪問一遍看看:
一切正如我所期望的,它成了!這其實就是一個簡單修改響應的示例,可以做到很強大的功能,但是,這沒必要展開說了。需要強調的一點就是,如果需要修改響應體,別忘了同時更新 Header 的 Content-Length,不然,這就是留給自己的一個深坑哦。
Transport
在前面的兩個操作里面,我們對 HTTP 請求的反向代理前和后都進行了一番操作,但是,有點不爽的地方在於我們是分別在兩個地方進行處理的,這缺乏一些統一性。那么有沒有一種方法,可以在一個地方完成這些事情呢,很明顯 Go 語言為我們提供了這個一個地方,那就是 Transport,官方文檔中是這么描述 Transport 的:
// The transport used to perform proxy requests.
這似乎還有點太簡單了,不如看一下 RoundTripper 接口是如何描述的:
RoundTrip executes a single HTTP transaction, returning
a Response for the provided Request.RoundTrip should not attempt to interpret the response. In
particular, RoundTrip must return err == nil if it obtained
a response, regardless of the response's HTTP status code.
A non-nil err should be reserved for failure to obtain a
response. Similarly, RoundTrip should not attempt to
handle higher-level protocol details such as redirects,
authentication, or cookies.RoundTrip should not modify the request, except for
consuming and closing the Request's Body. RoundTrip may
read fields of the request in a separate goroutine. Callers
should not mutate the request until the Response's Body has
been closed.RoundTrip must always close the body, including on errors,
but depending on the implementation may do so in a separate
goroutine even after RoundTrip returns. This means that
callers wanting to reuse the body for subsequent requests
must arrange to wait for the Close call before doing so.
The Request's URL and Header fields must be initialized.
可以看到 Transport 的功能還是很簡單的,雖然可以修改 Request 和 Response,但是,就合理性來說是不希望在這里面修改的的,更多的需求還是在於修改 Response 的。既然如此,不妨來看下如何實現 Transport,就以內置的 DefaultTransport
來看吧:
其實 DefaultTransport
自身沒什么,無非是設定一些默認參數構建了一下 http.Transport
這個結構體,這里有一個坑就是 Line 2 的 Proxy:ProxyFromEnviroment
會使用系統默認的代理,如果你設置了環境變量 HTTP_PROXY 的話,那么默認是會使用這個代理的,切記切記,這是個坑!
小結
到 DefaultTransport
這里就不再往下深挖了,因為到這里已經可以滿足平時的大部分需求了,再往下其實就涉及到 Go 語言中 HTTP 模塊的核心功能了,這些會在后面的文章中講。在這篇文章中,雖然我只是講了三個組件,但是,這三個小組件已經很夠了,而且,在本文中,我也着重描述了可能出現坑的幾個地方,這些地方都值得我們去關注,因為在使用 ReverseProxy 的時候,一般遇到的問題都離不開這幾個。