Golang實現(xiàn)反向代理的示例代碼
背景
當我們談到反向代理時,可以將其比喻為一個“中間人”。想象一下,你是一個用戶,你想要訪問某個網(wǎng)站。但是,這個網(wǎng)站并不直接向你提供服務,而是委托了一個代理來處理你的請求。這個代理就是反向代理。
你可以把反向代理想象成一個非常聰明的助手,它可以幫助你與網(wǎng)站進行交流。當你發(fā)送請求時,它會接收到你的請求,并將其轉發(fā)給網(wǎng)站。然后,網(wǎng)站會將響應發(fā)送給反向代理,反向代理再將響應發(fā)送給你。這樣,你就可以與網(wǎng)站進行交互,而不需要直接與網(wǎng)站通信。
net/http 包里面已經(jīng)幫我們內置了具有反向代理能力 ReverseProxy 對象, 但是它的能力有限, 從工程能力上面還有很多自行實現(xiàn).
本文包含了講述官方代碼內部實現(xiàn), 同時結合自身需求講述改造后對象代碼邏輯
由于筆者能力和精力有限, 因本文包含了大段代碼, 不免閱讀起來第一感覺較為繁瑣復雜, 但大部分代碼都進行了詳細的注釋標注, 可業(yè)務中用到時再回來詳讀代碼部分.
大家也可閱讀底部參考鏈接部分, 選擇的質量都很精簡, 相信大家肯定能有所收獲.
官方代碼分析
簡單使用
首先我們看下入口實現(xiàn), 只需要幾行代碼, 就將所有流量代理到了 www.domain.com 上
// 設置要轉發(fā)的地址 target, err := url.Parse("http://www.domain.com") if err != nil { panic(err) } // 實例化 ReverseProxy 包 proxy := httputil.NewSingleHostReverseProxy(target) //http.HandleFunc("/", proxy.ServeHTTP) // 啟動服務 log.Fatal(http.ListenAndServe(":8082", proxy))
本地啟動 127.0.0.1:8082 后會攜帶相關客戶端相關請求信息到 www.domain.com 域下.
但是通常上述是無法滿足我們需求的, 比如有鑒權、超時控制、鏈路傳遞、請求日志記錄等常見需求, 這樣我們怎么來實現(xiàn)呢? 在開始之前, 我們先了解下官方內置了哪些能力, 具體是怎么工作的.
底層結構
官方的 ReverseProxy 提供的結構:
type ReverseProxy struct { // 對請求內容進行修改 (對象是業(yè)務傳入req的一個副本) Director func(*http.Request) // 連接池復用連接,用于執(zhí)行請求, 默認為http.DefaultTransport Transport http.RoundTripper // 定時刷新內容到客戶端的時間間隔(流式/無內容此參數(shù)忽略) FlushInterval time.Duration // 默認為std.err,用于記錄內部錯誤日志 ErrorLog *log.Logger // 用于執(zhí)行 copyBuffer 復制響應體時,利用的bytes內存池化 BufferPool BufferPool // 如果配置后, 可修改目標代理的響應結果(響應頭和內容) // 如果此方法返回error, 將調用 ErrorHandler 方法 ModifyResponse func(*http.Response) error // 配置后代理執(zhí)行過程中, 發(fā)生錯誤均會回調此方法 // 默認邏輯不響應任務內容, 狀態(tài)碼返回502 ErrorHandler func(http.ResponseWriter, *http.Request, error) }
在開始的demo里, 我們第一步實例化了 ReverseProxy 對象, 首先我們分析下NewSingleHostReverseProxy 方法做了什么
// 實例化 ReverseProxy 包 proxy := httputil.NewSingleHostReverseProxy(target)
初始化部分
初始化對象, 設置代理請求的request結構值
// 實例化 ReverseProxy 對象 // 初始化 Director 對象, 將請求地址轉換為代理目標地址. // 對請求header頭進行處理 func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy { targetQuery := target.RawQuery director := func(req *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL) if targetQuery == "" || req.URL.RawQuery == "" { req.URL.RawQuery = targetQuery + req.URL.RawQuery } else { req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } if _, ok := req.Header["User-Agent"]; !ok { // explicitly disable User-Agent so it's not set to default value req.Header.Set("User-Agent", "") } } return &ReverseProxy{Director: director} }
小貼士:
大家可能對 User-Agent 處理比較奇怪, 為什么不存在后要設置一個空字符串呢?
這塊代碼源自于的 issues 為: https://github.com/golang/go/issues/15524目的是為了避免請求頭User-Agent被污染, 在http底層包發(fā)起請求時, 如果未設置 User-Agent 將會使用 Go-http-client/1.1 代替
發(fā)起請求部分
http.ListenAndServe(":8082", proxy) 啟動服務時, 處理請求的工作主要是 Handler 接口ServeHTTP 方法.
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ReverseProxy 中默認已實現(xiàn)此接口, 以下是處理請求的核心邏輯
我們來看下代碼是怎么處理的
// 服務請求處理方法 func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // 檢測是否設置http.Transport對象 // 如果未設置則使用默認對象 transport := p.Transport if transport == nil { transport = http.DefaultTransport } // 檢測請求是否被終止 // 終止請求或是正常結束請求等 notifyChan 都會收到請求結束通知, 之后進行cancel ctx := req.Context() if cn, ok := rw.(http.CloseNotifier); ok { var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) defer cancel() notifyChan := cn.CloseNotify() go func() { select { case <-notifyChan: cancel() case <-ctx.Done(): } }() } // 對外部傳入的http.Request對象進行克隆 // outreq 是給代理服務器傳入的請求對象 outreq := req.Clone(ctx) if req.ContentLength == 0 { // 主要修復 ReverseProxy 與 http.Transport 重試不兼容性問題 // 如果請求方法為 GET、HEAD、OPTIONS、TRACE, 同時body為nil情況下, 將會發(fā)生重試 // 避免因為復制傳入的request創(chuàng)建傳入代理的請求內容, 導致無法發(fā)生重試. // https://github.com/golang/go/issues/16036 outreq.Body = nil } if outreq.Body != nil { // 避免因panic問題導致請求未正確關閉, 其他協(xié)程繼續(xù)從中讀取 // https://github.com/golang/go/issues/46866 defer outreq.Body.Close() } if outreq.Header == nil { // Issue 33142: historical behavior was to always allocate outreq.Header = make(http.Header) } // 調用實現(xiàn)的 Director 方法修改請求代理的request對象 p.Director(outreq) if outreq.Form != nil { outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery) } outreq.Close = false // 升級http協(xié)議,HTTP Upgrade // 判斷header Connection 中是否有Upgrade reqUpType := upgradeType(outreq.Header) // 根據(jù)《網(wǎng)絡交換的 ASCII 格式》規(guī)范, 升級協(xié)議中是否包含禁止使用的字符 // https://datatracker.ietf.org/doc/html/rfc20#section-4.2 if !ascii.IsPrint(reqUpType) { // 調用 ReverseProxy 對象的 ErrorHandler 方法 p.getErrorHandler()( rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType)) return } // 請求下游移除Connetion頭 // https://datatracker.ietf.org/doc/html/rfc7230#section-6.1 removeConnectionHeaders(outreq.Header) // 請求下游根據(jù)RFC規(guī)范移除協(xié)議頭 for _, h := range hopHeaders { outreq.Header.Del(h) } // Transfer-Encoding: chunked 分塊傳輸編碼 if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") { outreq.Header.Set("Te", "trailers") } // 請求下游指定協(xié)議升級, 例如 websockeet if reqUpType != "" { outreq.Header.Set("Connection", "Upgrade") outreq.Header.Set("Upgrade", reqUpType) } // 添加 X-Forwarded-For 頭 // 最開始的是離服務端最遠的設備 IP,然后是每一級代理設備的 IP // 類似于 X-Forwarded-For: client, proxy1, proxy2 if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { prior, ok := outreq.Header["X-Forwarded-For"] // 如果header頭 X-Forwarded-For 設置為nil, 則不再 X-Forwarded-For // 這個參數(shù)下面我們將詳細說明 omit := ok && prior == nil if len(prior) > 0 { clientIP = strings.Join(prior, ", ") + ", " + clientIP } if !omit { outreq.Header.Set("X-Forwarded-For", clientIP) } } // 使用transport對象中維護的鏈接池, 向下游發(fā)起請求 res, err := transport.RoundTrip(outreq) if err != nil { p.getErrorHandler()(rw, outreq, err) return } // 處理下游響應的升級協(xié)議請求 // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc) if res.StatusCode == http.StatusSwitchingProtocols { if !p.modifyResponse(rw, res, outreq) { return } p.handleUpgradeResponse(rw, outreq, res) return } // 根據(jù)協(xié)議規(guī)范刪除響應 Connection 頭 removeConnectionHeaders(res.Header) // 下游響應根據(jù)RFC規(guī)范移除協(xié)議頭 for _, h := range hopHeaders { res.Header.Del(h) } // 如有設置 modifyResponse, 則修改響應內容 // 調用 ReverseProxy 對象 modifyResponse 方法 if !p.modifyResponse(rw, res, outreq) { return } // 拷貝響應Header到上游response對象 copyHeader(rw.Header(), res.Header) // 分塊傳輸部分協(xié)議 header 頭設置, 已跳過 // 寫入響應碼到上游response對象 rw.WriteHeader(res.StatusCode) // 拷貝結果到上游 // flushInterval將響應定時刷新到緩沖區(qū) err = p.copyResponse(rw, res.Body, p.flushInterval(res)) if err != nil { defer res.Body.Close() // ... 調用errorHandler panic(http.ErrAbortHandler) } // 關閉響應body res.Body.Close() // chunked 分塊傳輸編碼調用flush刷新到客戶端 if len(res.Trailer) > 0 { // Force chunking if we saw a response trailer. // This prevents net/http from calculating the length for short // bodies and adding a Content-Length. if fl, ok := rw.(http.Flusher); ok { fl.Flush() } } // 以下為分塊傳輸編碼相關header設置 if len(res.Trailer) == announcedTrailers { copyHeader(rw.Header(), res.Trailer) return } for k, vv := range res.Trailer { k = http.TrailerPrefix + k for _, v := range vv { rw.Header().Add(k, v) } } }
以上是代理請求的核心處理流程, 我們可以看到主要是對傳入 request 對象轉成下游代理請求對象, 請求后返回響應頭和內容, 進行處理.
內容補充
1. 為什么請求下游移除Connetion頭
Connection 通用標頭控制網(wǎng)絡連接在當前會話完成后是否仍然保持打開狀態(tài)。如果發(fā)送的值是 keep-alive,則連接是持久的,不會關閉,允許對同一服務器進行后續(xù)請求。
這個頭設置解決的是客戶端和服務端鏈接方式, 而不應該透傳給代理的下游服務.
所以再RFC中有以下明確規(guī)定:
“Connection”頭字段允許發(fā)送者指示所需的連接 當前連接的控制選項。為了避免混淆下游接收者,代理或網(wǎng)關必須刪除或在轉發(fā)之前替換任何收到的連接選項信息。
RFC: https://datatracker.ietf.org/doc/html/rfc7230#section-6.1
2. X-Forwarded-For 作用
X-Forwarded-For(XFF)請求標頭是一個事實上的用于標識通過代理服務器連接到 web 服務器的客戶端的原始 IP 地址的標頭(很容易被篡改)。
當客戶端直接連接到服務器時,其 IP 地址被發(fā)送給服務器(并且經(jīng)常被記錄在服務器的訪問日志中)。但是如果客戶端通過正向或反向代理服務器進行連接,服務器就只能看到最后一個代理服務器的 IP 地址,這個 IP 通常沒什么用。如果最后一個代理服務器是與服務器安裝在同一臺主機上的負載均衡服務器,則更是如此。X-Forwarded-For 的出現(xiàn),就是為了向服務器提供更有用的客戶端 IP 地址。
X-Forwarded-For: <client>, <proxy1>, <proxy2>
<client>
客戶端的 IP 地址。
<proxy1>, <proxy2>
如果請求經(jīng)過多個代理服務器,每個代理服務器的 IP 地址會依次出現(xiàn)在列表中。
這意味著,如果客戶端和代理服務器行為良好,最右邊的 IP 地址會是最近的代理服務器的 IP 地址,
最左邊的 IP 地址會是原始客戶端的 IP 地址。
引用: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Forwarded-For
實際應用落地
實際落地過程中, 我們不僅要考慮轉發(fā)能力, 還要有相對應的日志、超時、優(yōu)雅錯誤處理等能力,
下面將講解怎么基于官方內置的 ReverseProxy 對象的代理能力來實現(xiàn)這些功能.
設計思路: 對外實現(xiàn) Proxy ServerHttp版的接口, 在內部利用 ReverseProxy 對象代理能力基礎上設計.
1. 定義proxy ServeHTTP對象
type ServeHTTP struct { // 代理鏈接地址 targetUrl string // net/http 內置的 ReverseProxy 對象 reverseProxy *httputil.ReverseProxy // 代理錯誤處理 proxyErrorHandler ProxyErrorHandler // 日志對象 logger log.Logger }
下面我們實例化對象
// NewServeHTTP 初始化代理對象 func NewServeHTTP(targetUrl string, logger log.Logger) *ServeHTTP { target, err := url.Parse(targetUrl) if err != nil { panic(err) } // 重新設置 Director 復制請求處理 proxy := &httputil.ReverseProxy{Director: func(req *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.Host = target.Host if _, ok := req.Header["User-Agent"]; !ok { req.Header.Set("User-Agent", "") } if req.Header.Get("Content-Length") == "0" { req.Header.Del("Content-Length") } req.Header["X-Forwarded-For"] = nil for _, name := range removeRequestHeaders { req.Header.Del(name) } }} serveHttp := &ServeHTTP{ targetUrl: targetUrl, logger: logger, reverseProxy: proxy, proxyErrorHandler: DefaultProxyErrorHandler, } // 設置trasport處理對象(主要調配鏈接池大小和超時時間) serveHttp.reverseProxy.Transport = HttpTransportDefault() // 定義錯誤處理 serveHttp.reverseProxy.ErrorHandler = serveHttp.getErrorHandler(logger) // 定義響應處理 serveHttp.reverseProxy.ModifyResponse = serveHttp.getResponseHandler(logger) return serveHttp } // SetProxyErrorFunc 設置錯誤處理函數(shù) func (s *ServeHTTP) SetProxyErrorFunc(handler ProxyErrorHandler) *ServeHTTP { s.proxyErrorHandler = handler return s }
2. 我們重寫了 reverseProxy 的 Director方法
1.我們不希望轉發(fā) X-Forwarded-For 到代理層, 通過手動賦值為nil方式解決
原因是網(wǎng)絡防火墻對源IP進行了驗證, X-Forwarded-For是可選項之一, 但通常 X-Forwarded-For 不安全且容易造成本地聯(lián)通性問題, 不建議通過此參數(shù)進行驗證, 故將此移除.
2.移除指定的 removeRequestHeaders 頭
常見的鑒權類頭等
3. 覆蓋官方默認的 HttpTransportDefault
在 http.Transport 對象中, MaxIdleConnsPerHost、MaxIdleConns 參數(shù)在 http1.1 下非常影響性能, 默認 同host 建立的鏈接池內連接數(shù)只有2個, 下面我們統(tǒng)一修改為200
netHttp.Transport{ Proxy: proxyURL, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 200, MaxIdleConnsPerHost: 200, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }
4. 定義請求處理部分
考慮到在請求 reverseProxy 對象轉發(fā)邏輯時,需要攔截請求進行前置參數(shù)處理, 不能直接使用 reverseProxy 對象, 所以就由自定義 proxy 實現(xiàn) handler 接口的 ServeHTTP 方法, 對 reverseProxy 鏈接處理進行一層包裝.
邏輯如下:
// ServeHTTP 服務轉發(fā) func (s *ServeHTTP) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var ( reqBody []byte err error // 生成traceId traceId = s.getTraceId(request) ) // 前置獲取請求頭, 放入context中 // 調用結束后請求 body 將會被關閉, 后面將無法再獲取 if request.Body != nil { reqBody, err = io.ReadAll(request.Body) if err == nil { request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) } } // header 設置 traceId和超時時間傳遞 request.Header.Set(utils.TraceKey, traceId) request.Header.Set(utils.Timeoutkey, cast.ToString(s.getTimeout(request))) // 計算獲取超時時間, 發(fā)起轉發(fā)請求 ctx, cancel := context.WithTimeout( request.Context(), time.Duration(s.getTimeout(request))*time.Millisecond, ) defer cancel() // 設置請求體 ctx = context.WithValue(ctx, ctxReqBody, string(reqBody)) // 設置請求時間, 用于響應結束后計算請求耗時 ctx = context.WithValue(ctx, ctxReqTime, time.Now()) // context 設置traceId, 用于鏈路日志打印 ctx = context.WithValue(ctx, utils.TraceKey, traceId) request = request.WithContext(ctx) // 調用 reverseProxy ServeHTTP, 處理轉發(fā)邏輯 s.reverseProxy.ServeHTTP(writer, request) }
以上代碼均有詳細注釋, 下面我們看下 traceId和請求耗時函數(shù)邏輯, 比較簡單.
// getTraceId 獲取traceId // header頭中不存在則生成 func (s *ServeHTTP) getTraceId(request *http.Request) string { traceId := request.Header.Get(utils.TraceKey) if traceId != "" { return traceId } return uuid.NewV4().String() } // getTimeout 獲取超時時間 // header中不存在timeoutKey, 返回默認超時時間 // header頭存在, 則判斷是否大于默認超時時間, 大于則使用默認超時時間 // 否則返回header設置的超時時間 func (s *ServeHTTP) getTimeout(request *http.Request) uint32 { timeout := request.Header.Get(utils.Timeoutkey) if timeout == "" { return DefaultTimeoutMs } headerTimeoutMs := cast.ToUint32(timeout) if headerTimeoutMs > DefaultTimeoutMs { return DefaultTimeoutMs } return cast.ToUint32(timeout) }
5. 定義響應部分和錯誤處理部分
從一開始我們就了解 ReverseProxy 功能, 可以設置 ModifyResponse、ErrorHandler, 下面我們看下具體是怎么實現(xiàn)的.
ErrorHandler
// getErrorHandler 記錄錯誤記錄 func (s *ServeHTTP) getErrorHandler(logger log.Logger) ErrorHandler { return func(writer http.ResponseWriter, request *http.Request, e error) { var ( reqBody []byte err error ) if request.Body != nil { reqBody, err = io.ReadAll(request.Body) if err == nil { request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) } } // 初始化時確認proxyErrorHandler具體處理方法 // 調用 proxyErrorHandler,處理響應部分 s.proxyErrorHandler(writer, e) // 獲取必要信息, 記錄錯誤日志 scheme := s.getSchemeDataByRequest(request) _ = log.WithContext(request.Context(), logger).Log(log.LevelError, "x_module", "proxy/server/error", "x_component", scheme.kind, "x_error", e, "x_header", request.Header, "x_action", scheme.operation, "x_param", string(reqBody), "x_trace_id", request.Context().Value(utils.TraceKey), ) } } // 具體代理業(yè)務錯誤處理 // 包含默認錯誤響應和具體代理業(yè)務錯誤響應. // 以下為某個業(yè)務響應 func XXXProxyErrorHandler(writer http.ResponseWriter, err error) { resp := HttpXXXResponse{ ErrCode: 1, ErrMsg: err.Error(), Data: struct{}{}, } writer.Header().Set("Content-Type", "application/json; charset=utf-8") writer.Header().Set("Connection", "keep-alive") writer.Header().Set("Cache-Control", "no-cache") // 設置狀態(tài)碼為200 writer.WriteHeader(http.StatusOK) // 將響應值序列化 respByte, _ := json.Marshal(resp) // 將response數(shù)據(jù)寫入writer, 刷新到Flush // 關于Flush部分, 一般是不需要主動刷新的, 請求結束后會自動Flush _, _ = fmt.Fprintf(writer, string(respByte)) if f, ok := writer.(http.Flusher); ok { f.Flush() } }
以上有一個值的關注的地方, 設置響應頭一定要在設置響應碼之前, 否則將無效
設置響應內容一定在最后, 否則將設置失敗并返回錯誤.
ModifyResponse 處理邏輯
// getResponseHandler 獲取響應數(shù)據(jù) func (s *ServeHTTP) getResponseHandler(logger log.Logger) func(response *http.Response) error { return func(response *http.Response) error { var ( duration float64 logLevel = log.LevelInfo header http.Header ) // 獲取請求體 reqBody := response.Request.Context().Value(ctxReqBody) // 獲取開始請求時間, 計算請求耗時 startTime := response.Request.Context().Value(ctxReqBody) if startTime != nil { _, ok := startTime.(time.Time) if ok { duration = time.Since(startTime.(time.Time)).Seconds() } } // 獲取響應數(shù)據(jù) // 如果響應碼非200, 調整日志等級 scheme := s.getSchemeDataByResponse(response) if response.StatusCode != http.StatusOK { logLevel = log.LevelError header = scheme.header } // 記錄日志 _ = log.WithContext(response.Request.Context(), logger).Log(logLevel, "x_module", "proxy/server/resp", "x_component", "http", "x_code", scheme.code, "x_header", header, "x_action", scheme.operation, "x_params", reqBody, "x_response", scheme.responseData, "x_duration", duration, "x_trace_id", response.Request.Context().Value(utils.TraceKey), ) // 設置響應頭 response.Header.Set("Content-Type", "application/json; charset=utf-8") return nil } }
默認代理服務器是不設置響應頭的, 則為默認的響應頭。
響應頭必須手動設置
6. 使用自定義的 proxy 代理請求
urlStr := "https://" + targetHost proxy := utilsProxy.NewServeHTTP(urlStr, logger).SetProxyErrorFunc(utilsProxy.XXXProxyErrorHandler) log.Fatal(http.ListenAndServe(":8082", proxy))
以上就是Golang實現(xiàn)反向代理的示例代碼的詳細內容,更多關于Go反向代理的資料請關注腳本之家其它相關文章!
相關文章
Golang迭代如何在Go中循環(huán)數(shù)據(jù)結構使用詳解
這篇文章主要為大家介紹了Golang迭代之如何在Go中循環(huán)數(shù)據(jù)結構使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10