golang 如何實現(xiàn)HTTP代理和反向代理
代理的核心功能可以用一句話概括:接受客戶端的請求,轉(zhuǎn)發(fā)到后端服務(wù)器,獲得應(yīng)答之后返回給客戶端。
代理的功能有很多,事實上整個互聯(lián)網(wǎng)到處都充斥著代理服務(wù)器。如果所有的 HTTP 訪問都是客戶端和服務(wù)器端直接進(jìn)行的話,我們的網(wǎng)絡(luò)不僅會變得緩慢,而且性能會大打折扣。
代理服務(wù)器根據(jù)不同的配置和使用,可能會有不同的功能,這些功能主要包括:
內(nèi)容過濾:代理可以根據(jù)一定的規(guī)則限制某些請求的連接。比如有些公司會設(shè)置內(nèi)部網(wǎng)絡(luò)無法訪問某些購物、游戲網(wǎng)站,或者學(xué)校的網(wǎng)絡(luò)不讓學(xué)生訪問色情暴力的網(wǎng)站等
節(jié)省成本:代理服務(wù)器可以作為緩存使用,對于某些資源只需要第一次訪問的時候去下載,以后代理直接把緩存的結(jié)果返回給客戶端,節(jié)約網(wǎng)絡(luò)帶寬的開銷
提高性能:通過代理服務(wù)器的緩存(比如 CDN)和負(fù)載均衡(比如 nginx lb)功能,服務(wù)器端可以加速請求的訪問,在更快的時間內(nèi)返回結(jié)果)
增加安全性:公司可以在內(nèi)網(wǎng)和外網(wǎng)之間通過代理進(jìn)行轉(zhuǎn)發(fā),這樣不僅對外隱藏了實現(xiàn)的細(xì)節(jié),而且可以在代理層對爬蟲、病毒性請求進(jìn)行過濾,保護內(nèi)部服務(wù)
所有的這些功能的實現(xiàn)都依賴于代理的特性,它可以在客戶端和服務(wù)器端做一些事情,根據(jù)代理做的事情不同,它的角色和功能也就不同。那么,代理具體可以做哪些事情呢?比如:
修改 HTTP 請求:url、header、body
過濾請求:根據(jù)一定的規(guī)則丟棄、過濾請求
決定轉(zhuǎn)發(fā)到哪個后端(可以是靜態(tài)定義的,也可以是動態(tài)決定)
保存服務(wù)器的應(yīng)答,后續(xù)的請求可以直接使用保存的應(yīng)答
修改應(yīng)答:對應(yīng)答做一些格式的轉(zhuǎn)換,修改數(shù)據(jù),甚至返回完全不一樣的應(yīng)答數(shù)據(jù)
重試機制,如果后端服務(wù)器暫時無法響應(yīng),隔一段時間重試
……
正向代理和反向代理
代理可以分為正向代理和反向代理兩種。
正向代理需要客戶端來配置,一般來說我們會通過瀏覽器或者操作系統(tǒng)提供的工具或者界面來配置。這個時候,代理對客戶端不是透明的,客戶端需要知道代理的地址并且手動配置。配置了代理,瀏覽器在發(fā)送請求的時候會對報文做特殊的修改。
反向代理對客戶端是透明的,也就是說客戶端一般不知道代理的存在,認(rèn)為自己是直接和服務(wù)器通信。我們大部分訪問的網(wǎng)站就是反向代理服務(wù)器,反向代理服務(wù)器會轉(zhuǎn)發(fā)到真正的服務(wù)器,一般在反向代理這一層實現(xiàn)負(fù)載均衡和高可用的功能。而且這里也可以看到,客戶端是不會知道真正服務(wù)器端的 ip 地址和端口的,這在一定程度上起到了安全保護的作用。
代理服務(wù)器怎么知道目的服務(wù)器的地址?
在反向代理中,代理服務(wù)器要轉(zhuǎn)發(fā)的服務(wù)器地址都是事先知道的(包括靜態(tài)配置和動態(tài)配置)。比如 使用 nginx 來配置負(fù)載均衡 。
而對于正向代理來說,客戶端可能訪問的服務(wù)器地址是無法事先知道的。因為HTTP 協(xié)議活動在應(yīng)用層,它無法獲取網(wǎng)絡(luò)層(IP層)信息,那么該協(xié)議要有一個地方可以拿到這個信息。HTTP 中可能保存這個信息的地方有兩個:URL 和 header。默認(rèn)情況下,HTTP 請求的 status line 有三部分組成:方法、uri 和協(xié)議版本,比如:
GET /index.html HTTP/1.0 User-Agent: gohttp 1.0
如果客戶端(比如瀏覽器)知道自己在通過正向代理進(jìn)行報文傳輸,那么它會在 status line 加上要訪問服務(wù)器的真實地址。這個時候發(fā)送的報文是:
GET http://www.marys-antiques.com/index.html HTTP/1.0 User-Agent: gohttp 1.0
代理路徑
客戶端不管是通過代理服務(wù)器,還是直接訪問后端服務(wù)器對于最終的結(jié)果是沒有區(qū)別的,也就是說大多數(shù)情況下客戶端根本不關(guān)心它訪問的到底是什么,只需要(準(zhǔn)確快速地)拿到想要的信息就夠了。但是有時候,我們還是希望知道請求到底在中間經(jīng)歷了哪些代理,比如用來調(diào)試網(wǎng)絡(luò)異常,或者做數(shù)據(jù)統(tǒng)計,而 HTTP 協(xié)議也提供了響應(yīng)的功能。
雖然 RFC 2616 定義了 Via 頭部字段來跟蹤 HTTP 請求經(jīng)過的代理路徑,但在實際中用的更多的還是 X-Forwarded-For 字段, X-Forwarded-For 是 Squid 緩存代理服務(wù)軟件引入的,目前已經(jīng)在規(guī)范化在 RFC 7239 文檔。
X-Forwarded-For 頭部格式也比較簡單,比如某個服務(wù)器接受到請求的對應(yīng)頭部可能是:
X-Forwarded-For: client, proxy1, proxy2
對應(yīng)的值有多個字段,每個字段代表中間的一個節(jié)點,它們之間由逗號和空格隔開,從左到右距離當(dāng)前節(jié)點越來越近。
每個代理服務(wù)器會在 X-Forwarded-For 頭部填上前一個節(jié)點的 ip 地址,這個地址可以通過 TCP 請求的 remote address 獲取。為什么每個代理服務(wù)器不填寫自己的 ip 地址呢?有兩個原因,如果由代理服務(wù)器填寫自己的 ip 地址,那么代理可以很簡單地偽造這個地址,而上一個節(jié)點的 remote address 是根據(jù) TCP 連接獲取的(如果不建立正確的 TCP 連接是無法進(jìn)行 HTTP 通信的);另外一個原因是如果由當(dāng)前節(jié)點填寫 X-Forwarded-For ,那么很多情況客戶端無法判斷自己是否會通過代理的。
NOTE:
1、最終客戶端或者服務(wù)器端接受的請求, X-Forwarded-For 是沒有最鄰近節(jié)點的 ip 地址的,而這個地址可以通過 remote address 獲取
2、每個節(jié)點(不管是客戶端、代理服務(wù)器、真實服務(wù)器)都可以隨便更改 X-Forwarded-For 的值,因此這個字段只能作為參考
代理服務(wù)器實現(xiàn)
這個部分我們會介紹如何用 golang 來實現(xiàn) HTTP 代理服務(wù)器,需要讀者了解一些 HTTP 服務(wù)器端編程的知識。
正向代理
按照我們之前介紹的代理原理,我們可以編寫出這樣的代碼:
package main import ( "fmt" "io" "net" "net/http" "strings" ) type Pxy struct {} func (p *Pxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { fmt.Printf("Received request %s %s %s\n", req.Method, req.Host, req.RemoteAddr) transport := http.DefaultTransport // step 1 outReq := new(http.Request) *outReq = *req // this only does shallow copies of maps if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { if prior, ok := outReq.Header["X-Forwarded-For"]; ok { clientIP = strings.Join(prior, ", ") + ", " + clientIP } outReq.Header.Set("X-Forwarded-For", clientIP) } // step 2 res, err := transport.RoundTrip(outReq) if err != nil { rw.WriteHeader(http.StatusBadGateway) return } // step 3 for key, value := range res.Header { for _, v := range value { rw.Header().Add(key, v) } } rw.WriteHeader(res.StatusCode) io.Copy(rw, res.Body) res.Body.Close() } func main() { fmt.Println("Serve on :8080") http.Handle("/", &Pxy{}) http.ListenAndServe("0.0.0.0:8080", nil) }
這段代碼比較直觀,只包含了最核心的代碼邏輯,完全按照最上面的代理圖例進(jìn)行組織。一共分成幾個步驟:
1、代理接收到客戶端的請求,復(fù)制了原來的請求對象,并根據(jù)數(shù)據(jù)配置新請求的各種參數(shù)(添加上 X-Forward-For 頭部等)
2、把新請求發(fā)送到服務(wù)器端,并接收到服務(wù)器端返回的響應(yīng)
3、代理服務(wù)器對響應(yīng)做一些處理,然后返回給客戶端
上面的代碼運行之后,會在本地的 8080 端口啟動代理服務(wù)。修改瀏覽器的代理為 127.0.0.1::8080 再訪問網(wǎng)站,可以驗證代理正常工作,也能看到它在終端打印出所有的請求信息。
雖然這段代碼非常簡短,但是你可以添加更多的邏輯實現(xiàn)非常有用的功能。比如在請求發(fā)送之前進(jìn)行過濾,根據(jù)一定的規(guī)則直接阻止某些請求的訪問;或者對請求進(jìn)行限流,某個客戶端在一定的時間里執(zhí)行的請求有最大限額;統(tǒng)計請求的數(shù)據(jù)進(jìn)行分析等等。
這個代理目前不支持 HTTPS 協(xié)議,因為它只提供了 HTTP 請求的轉(zhuǎn)發(fā)功能,并沒有處理證書和認(rèn)證有關(guān)的內(nèi)容。如果了解 HTTPS 協(xié)議的話,你會明白這種模式下是無法完成 HTTPS 握手的,雖然代理可以和真正的服務(wù)器建立連接(知道了對方的公鑰和證書),但是代理無法代表服務(wù)器和客戶端建立連接,因為代理服務(wù)器無法知道真正服務(wù)器的私鑰。
反向代理
編寫反向代理按照上面的思路當(dāng)然沒有問題,只需要在第二步的時候,根據(jù)之前的配置修改 outReq 的 URL Host 地址可以了。不過 Golang 已經(jīng)給我們提供了編寫代理的框架: httputil.ReverseProxy 。我們可以用非常簡短的代碼來實現(xiàn)自己的代理,而且內(nèi)部的細(xì)節(jié)問題都已經(jīng)被很好地處理了。
這部分我們會實現(xiàn)一個簡單的反向代理,它能夠?qū)φ埱髮崿F(xiàn)負(fù)載均衡,隨機地把請求發(fā)送給某些配置好的后端服務(wù)器。使用 httputil.ReverseProxy 編寫反向代理最重要的就是實現(xiàn)自己的 Director 對象,這是 GoDoc 對它的介紹:
Director must be a function which modifies the request into a new request to be sent using Transport. Its response is then copied back to the original client unmodified. Director must not access the provided Request after returning.
簡單翻譯的話, Director 是一個函數(shù),它接受一個請求作為參數(shù),然后對其進(jìn)行修改。修改后的請求會實際發(fā)送給服務(wù)器端,因此我們編寫自己的 Director 函數(shù),每次把請求的 Scheme 和 Host 修改成某個后端服務(wù)器的地址,就能實現(xiàn)負(fù)載均衡的效果(其實上面的正向代理也可以通過相同的方法實現(xiàn))??创a:
package main import ( "log" "math/rand" "net/http" "net/http/httputil" "net/url" ) func NewMultipleHostsReverseProxy(targets []*url.URL) *httputil.ReverseProxy { director := func(req *http.Request) { target := targets[rand.Int()%len(targets)] req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.URL.Path = target.Path } return &httputil.ReverseProxy{Director: director} } func main() { proxy := NewMultipleHostsReverseProxy([]*url.URL{ { Scheme: "http", Host: "localhost:9091", }, { Scheme: "http", Host: "localhost:9092", }, }) log.Fatal(http.ListenAndServe(":9090", proxy)) }
我們讓代理監(jiān)聽在 9090 端口,在后端啟動兩個返回不同響應(yīng)的服務(wù)器分別監(jiān)聽在 9091 和 9092 端口,通過 curl 訪問,可以看到多次請求會返回不同的結(jié)果。
➜ curl http://127.0.0.1:9090 116064a9eb83 ➜ curl http://127.0.0.1:9090 8f7ccc11718f
同樣的,這段代碼也只是一個 demo,存在著很多問題,比如沒有錯誤處理機制,如果后端某個服務(wù)器掛了,代理會返回 502 錯誤,更好的做法是把請求轉(zhuǎn)發(fā)到另外的可用服務(wù)器。當(dāng)然也可以添加更多的特性讓它更好用,比如動態(tài)地添加后端服務(wù)器列表;根據(jù)后端服務(wù)器的負(fù)載情況進(jìn)行負(fù)載轉(zhuǎn)發(fā)等等。
補充:golang 超簡單實現(xiàn)反向代理(nginx 端口轉(zhuǎn)發(fā) Proxy)
100行你就可以做到類似nginx帶自動更新的端口轉(zhuǎn)發(fā)功能
總共就2個文件,一個main(總行數(shù)128行),一個配置文件
main:
里面的json解析和log可以忽略
package main import ( "github.com/weimingjue/json" utils2 "goProxy/utils" "goService/utils" "io/ioutil" "net" "net/http" "net/http/httputil" "net/url" "os" "strings" "sync" "time" ) var ( projectDir, _ = os.Getwd() fileName = projectDir + "/domain.config" readFileTime int64 = 0 //讀取文件的時間 fileChangedTime int64 = 0 //文件修改時間 domainData [][]string //[{***.gq,8080,http://127.0.0.1:8080/}] duPeiZhiSuo sync.Mutex //讀配置鎖 ) // 獲取反向代理域名 func getProxyUrl(reqDomain string) string { checkFile() for _, dms := range domainData { if strings.Index(reqDomain, dms[0]) >= 0 { return dms[2] } } return domainData[0][2] } //讀取配置文件 //域名:端口號,未知域名默認(rèn)用第一個 func checkFile() { nowTime := time.Now().Unix() if nowTime-readFileTime < 300 { return } //每5分鐘判斷文件是否修改 domainFile, _ := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0) info, _ := domainFile.Stat() if info.ModTime().Unix() == fileChangedTime { return } duPeiZhiSuo.Lock() defer duPeiZhiSuo.Unlock() domainFile, _ = os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0) //加鎖再來一遍,防止重入 info, _ = domainFile.Stat() changedTime := info.ModTime().Unix() if changedTime == fileChangedTime { return } //文件改變 //重置數(shù)據(jù) readFileTime = nowTime fileChangedTime = changedTime domainData = [][]string{} bytes, _ := ioutil.ReadFile(fileName) split := strings.Split(string(bytes), "\n") for _, domainInfo := range split { dLen := len(domainInfo) if dLen < 8 || dLen > 20 { //忽略錯誤信息 continue } domainItems := strings.Split(domainInfo, ":") if len(domainItems) != 2 || len(domainItems[0]) < 3 || len(domainItems[1]) < 2 { continue } if utils.EndWidth(domainItems[1], "/") { domainItems = append(domainItems, "http://127.0.0.1:"+domainItems[1]) } else { domainItems = append(domainItems, "http://127.0.0.1:"+domainItems[1]+"/") } domainData = append(domainData, domainItems) } domainSt, _ := json.Marshal(domainData) utils2.MyLogProxyI("配置已修改:" + string(domainSt)) } //獲取主機名 func getHost(req *http.Request) string { if req.Host != "" { if hostPart, _, err := net.SplitHostPort(req.Host); err == nil { return hostPart } return req.Host } return "localhost" } func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) { host := getHost(req) proxyUrl := getProxyUrl(host) url2, _ := url.Parse(proxyUrl) utils2.MyLogProxyI("請求域名:" + host + ",轉(zhuǎn)到:" + proxyUrl) // create the reverse proxy proxy := httputil.NewSingleHostReverseProxy(url2) // Update the headers to allow for SSL redirection req.URL.Host = url2.Host req.URL.Scheme = url2.Scheme req.Header.Set("X-Forwarded-Host", req.Header.Get("Host")) req.Host = url2.Host // Note that ServeHttp is non blocking and uses a go routine under the hood proxy.ServeHTTP(res, req) } func main() { http.HandleFunc("/", handleRequestAndRedirect) if err := http.ListenAndServe(":80", nil); err != nil { utils.MyLogE("Proxy監(jiān)聽80端口錯誤:" + err.Error()) panic(err) } }
domain.config:
***為自己的域名,":"后面是需要轉(zhuǎn)發(fā)的端口,不用寫http://,任何地方都不能有空格
wang.gq:8080 ***.aa:8081/
代碼寫的是相對目錄請到當(dāng)前目錄執(zhí)行"go run main.go",愉快的轉(zhuǎn)發(fā)從現(xiàn)在開始
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。如有錯誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
詳解Golang中Context的三個常見應(yīng)用場景
Golang?context主要用于定義超時取消,取消后續(xù)操作,在不同操作中傳遞值。本文通過簡單易懂的示例進(jìn)行說明,感興趣的可以了解一下2022-12-12Go語言開發(fā)kube-scheduler整體架構(gòu)深度剖析
這篇文章主要為大家介紹了Go語言開發(fā)kube-scheduler整體架構(gòu)深度剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04golang執(zhí)行命令操作 exec.Command
這篇文章主要介紹了golang執(zhí)行命令操作 exec.Command,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題及處理方法
這篇文章主要介紹了VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06