Golang實(shí)現(xiàn)反向代理的示例代碼
背景
當(dāng)我們談到反向代理時(shí),可以將其比喻為一個(gè)“中間人”。想象一下,你是一個(gè)用戶,你想要訪問某個(gè)網(wǎng)站。但是,這個(gè)網(wǎng)站并不直接向你提供服務(wù),而是委托了一個(gè)代理來處理你的請(qǐng)求。這個(gè)代理就是反向代理。
你可以把反向代理想象成一個(gè)非常聰明的助手,它可以幫助你與網(wǎng)站進(jìn)行交流。當(dāng)你發(fā)送請(qǐng)求時(shí),它會(huì)接收到你的請(qǐng)求,并將其轉(zhuǎn)發(fā)給網(wǎng)站。然后,網(wǎng)站會(huì)將響應(yīng)發(fā)送給反向代理,反向代理再將響應(yīng)發(fā)送給你。這樣,你就可以與網(wǎng)站進(jìn)行交互,而不需要直接與網(wǎng)站通信。
net/http 包里面已經(jīng)幫我們內(nèi)置了具有反向代理能力 ReverseProxy 對(duì)象, 但是它的能力有限, 從工程能力上面還有很多自行實(shí)現(xiàn).
本文包含了講述官方代碼內(nèi)部實(shí)現(xiàn), 同時(shí)結(jié)合自身需求講述改造后對(duì)象代碼邏輯
由于筆者能力和精力有限, 因本文包含了大段代碼, 不免閱讀起來第一感覺較為繁瑣復(fù)雜, 但大部分代碼都進(jìn)行了詳細(xì)的注釋標(biāo)注, 可業(yè)務(wù)中用到時(shí)再回來詳讀代碼部分.
大家也可閱讀底部參考鏈接部分, 選擇的質(zhì)量都很精簡(jiǎn), 相信大家肯定能有所收獲.
官方代碼分析
簡(jiǎn)單使用
首先我們看下入口實(shí)現(xiàn), 只需要幾行代碼, 就將所有流量代理到了 www.domain.com 上
// 設(shè)置要轉(zhuǎn)發(fā)的地址
target, err := url.Parse("http://www.domain.com")
if err != nil {
panic(err)
}
// 實(shí)例化 ReverseProxy 包
proxy := httputil.NewSingleHostReverseProxy(target)
//http.HandleFunc("/", proxy.ServeHTTP)
// 啟動(dòng)服務(wù)
log.Fatal(http.ListenAndServe(":8082", proxy))本地啟動(dòng) 127.0.0.1:8082 后會(huì)攜帶相關(guān)客戶端相關(guān)請(qǐng)求信息到 www.domain.com 域下.

但是通常上述是無法滿足我們需求的, 比如有鑒權(quán)、超時(shí)控制、鏈路傳遞、請(qǐng)求日志記錄等常見需求, 這樣我們?cè)趺磥韺?shí)現(xiàn)呢? 在開始之前, 我們先了解下官方內(nèi)置了哪些能力, 具體是怎么工作的.
底層結(jié)構(gòu)
官方的 ReverseProxy 提供的結(jié)構(gòu):
type ReverseProxy struct {
// 對(duì)請(qǐng)求內(nèi)容進(jìn)行修改 (對(duì)象是業(yè)務(wù)傳入req的一個(gè)副本)
Director func(*http.Request)
// 連接池復(fù)用連接,用于執(zhí)行請(qǐng)求, 默認(rèn)為http.DefaultTransport
Transport http.RoundTripper
// 定時(shí)刷新內(nèi)容到客戶端的時(shí)間間隔(流式/無內(nèi)容此參數(shù)忽略)
FlushInterval time.Duration
// 默認(rèn)為std.err,用于記錄內(nèi)部錯(cuò)誤日志
ErrorLog *log.Logger
// 用于執(zhí)行 copyBuffer 復(fù)制響應(yīng)體時(shí),利用的bytes內(nèi)存池化
BufferPool BufferPool
// 如果配置后, 可修改目標(biāo)代理的響應(yīng)結(jié)果(響應(yīng)頭和內(nèi)容)
// 如果此方法返回error, 將調(diào)用 ErrorHandler 方法
ModifyResponse func(*http.Response) error
// 配置后代理執(zhí)行過程中, 發(fā)生錯(cuò)誤均會(huì)回調(diào)此方法
// 默認(rèn)邏輯不響應(yīng)任務(wù)內(nèi)容, 狀態(tài)碼返回502
ErrorHandler func(http.ResponseWriter, *http.Request, error)
}在開始的demo里, 我們第一步實(shí)例化了 ReverseProxy 對(duì)象, 首先我們分析下NewSingleHostReverseProxy 方法做了什么
// 實(shí)例化 ReverseProxy 包 proxy := httputil.NewSingleHostReverseProxy(target)
初始化部分
初始化對(duì)象, 設(shè)置代理請(qǐng)求的request結(jié)構(gòu)值
// 實(shí)例化 ReverseProxy 對(duì)象
// 初始化 Director 對(duì)象, 將請(qǐng)求地址轉(zhuǎn)換為代理目標(biāo)地址.
// 對(duì)請(qǐng)求header頭進(jìn)行處理
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}
}小貼士:
大家可能對(duì) User-Agent 處理比較奇怪, 為什么不存在后要設(shè)置一個(gè)空字符串呢?
這塊代碼源自于的 issues 為: https://github.com/golang/go/issues/15524目的是為了避免請(qǐng)求頭User-Agent被污染, 在http底層包發(fā)起請(qǐng)求時(shí), 如果未設(shè)置 User-Agent 將會(huì)使用 Go-http-client/1.1 代替
發(fā)起請(qǐng)求部分
http.ListenAndServe(":8082", proxy) 啟動(dòng)服務(wù)時(shí), 處理請(qǐng)求的工作主要是 Handler 接口ServeHTTP 方法.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}ReverseProxy 中默認(rèn)已實(shí)現(xiàn)此接口, 以下是處理請(qǐng)求的核心邏輯

我們來看下代碼是怎么處理的
// 服務(wù)請(qǐng)求處理方法
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// 檢測(cè)是否設(shè)置http.Transport對(duì)象
// 如果未設(shè)置則使用默認(rèn)對(duì)象
transport := p.Transport
if transport == nil {
transport = http.DefaultTransport
}
// 檢測(cè)請(qǐng)求是否被終止
// 終止請(qǐng)求或是正常結(jié)束請(qǐng)求等 notifyChan 都會(huì)收到請(qǐng)求結(jié)束通知, 之后進(jìn)行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():
}
}()
}
// 對(duì)外部傳入的http.Request對(duì)象進(jìn)行克隆
// outreq 是給代理服務(wù)器傳入的請(qǐng)求對(duì)象
outreq := req.Clone(ctx)
if req.ContentLength == 0 {
// 主要修復(fù) ReverseProxy 與 http.Transport 重試不兼容性問題
// 如果請(qǐng)求方法為 GET、HEAD、OPTIONS、TRACE, 同時(shí)body為nil情況下, 將會(huì)發(fā)生重試
// 避免因?yàn)閺?fù)制傳入的request創(chuàng)建傳入代理的請(qǐng)求內(nèi)容, 導(dǎo)致無法發(fā)生重試.
// https://github.com/golang/go/issues/16036
outreq.Body = nil
}
if outreq.Body != nil {
// 避免因panic問題導(dǎo)致請(qǐng)求未正確關(guān)閉, 其他協(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)
}
// 調(diào)用實(shí)現(xiàn)的 Director 方法修改請(qǐng)求代理的request對(duì)象
p.Director(outreq)
if outreq.Form != nil {
outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
}
outreq.Close = false
// 升級(jí)http協(xié)議,HTTP Upgrade
// 判斷header Connection 中是否有Upgrade
reqUpType := upgradeType(outreq.Header)
// 根據(jù)《網(wǎng)絡(luò)交換的 ASCII 格式》規(guī)范, 升級(jí)協(xié)議中是否包含禁止使用的字符
// https://datatracker.ietf.org/doc/html/rfc20#section-4.2
if !ascii.IsPrint(reqUpType) {
// 調(diào)用 ReverseProxy 對(duì)象的 ErrorHandler 方法
p.getErrorHandler()(
rw,
req,
fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
return
}
// 請(qǐng)求下游移除Connetion頭
// https://datatracker.ietf.org/doc/html/rfc7230#section-6.1
removeConnectionHeaders(outreq.Header)
// 請(qǐng)求下游根據(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")
}
// 請(qǐng)求下游指定協(xié)議升級(jí), 例如 websockeet
if reqUpType != "" {
outreq.Header.Set("Connection", "Upgrade")
outreq.Header.Set("Upgrade", reqUpType)
}
// 添加 X-Forwarded-For 頭
// 最開始的是離服務(wù)端最遠(yuǎn)的設(shè)備 IP,然后是每一級(jí)代理設(shè)備的 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 設(shè)置為nil, 則不再 X-Forwarded-For
// 這個(gè)參數(shù)下面我們將詳細(xì)說明
omit := ok && prior == nil
if len(prior) > 0 {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
if !omit {
outreq.Header.Set("X-Forwarded-For", clientIP)
}
}
// 使用transport對(duì)象中維護(hù)的鏈接池, 向下游發(fā)起請(qǐng)求
res, err := transport.RoundTrip(outreq)
if err != nil {
p.getErrorHandler()(rw, outreq, err)
return
}
// 處理下游響應(yīng)的升級(jí)協(xié)議請(qǐng)求
// 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ī)范刪除響應(yīng) Connection 頭
removeConnectionHeaders(res.Header)
// 下游響應(yīng)根據(jù)RFC規(guī)范移除協(xié)議頭
for _, h := range hopHeaders {
res.Header.Del(h)
}
// 如有設(shè)置 modifyResponse, 則修改響應(yīng)內(nèi)容
// 調(diào)用 ReverseProxy 對(duì)象 modifyResponse 方法
if !p.modifyResponse(rw, res, outreq) {
return
}
// 拷貝響應(yīng)Header到上游response對(duì)象
copyHeader(rw.Header(), res.Header)
// 分塊傳輸部分協(xié)議 header 頭設(shè)置, 已跳過
// 寫入響應(yīng)碼到上游response對(duì)象
rw.WriteHeader(res.StatusCode)
// 拷貝結(jié)果到上游
// flushInterval將響應(yīng)定時(shí)刷新到緩沖區(qū)
err = p.copyResponse(rw, res.Body, p.flushInterval(res))
if err != nil {
defer res.Body.Close()
// ... 調(diào)用errorHandler
panic(http.ErrAbortHandler)
}
// 關(guān)閉響應(yīng)body
res.Body.Close()
// chunked 分塊傳輸編碼調(diào)用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()
}
}
// 以下為分塊傳輸編碼相關(guān)header設(shè)置
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)
}
}
}以上是代理請(qǐng)求的核心處理流程, 我們可以看到主要是對(duì)傳入 request 對(duì)象轉(zhuǎn)成下游代理請(qǐng)求對(duì)象, 請(qǐng)求后返回響應(yīng)頭和內(nèi)容, 進(jìn)行處理.
內(nèi)容補(bǔ)充
1. 為什么請(qǐng)求下游移除Connetion頭
Connection 通用標(biāo)頭控制網(wǎng)絡(luò)連接在當(dāng)前會(huì)話完成后是否仍然保持打開狀態(tài)。如果發(fā)送的值是 keep-alive,則連接是持久的,不會(huì)關(guān)閉,允許對(duì)同一服務(wù)器進(jìn)行后續(xù)請(qǐng)求。
這個(gè)頭設(shè)置解決的是客戶端和服務(wù)端鏈接方式, 而不應(yīng)該透?jìng)鹘o代理的下游服務(wù).
所以再RFC中有以下明確規(guī)定:
“Connection”頭字段允許發(fā)送者指示所需的連接 當(dāng)前連接的控制選項(xiàng)。為了避免混淆下游接收者,代理或網(wǎng)關(guān)必須刪除或在轉(zhuǎn)發(fā)之前替換任何收到的連接選項(xiàng)信息。
RFC: https://datatracker.ietf.org/doc/html/rfc7230#section-6.1
2. X-Forwarded-For 作用
X-Forwarded-For(XFF)請(qǐng)求標(biāo)頭是一個(gè)事實(shí)上的用于標(biāo)識(shí)通過代理服務(wù)器連接到 web 服務(wù)器的客戶端的原始 IP 地址的標(biāo)頭(很容易被篡改)。
當(dāng)客戶端直接連接到服務(wù)器時(shí),其 IP 地址被發(fā)送給服務(wù)器(并且經(jīng)常被記錄在服務(wù)器的訪問日志中)。但是如果客戶端通過正向或反向代理服務(wù)器進(jìn)行連接,服務(wù)器就只能看到最后一個(gè)代理服務(wù)器的 IP 地址,這個(gè) IP 通常沒什么用。如果最后一個(gè)代理服務(wù)器是與服務(wù)器安裝在同一臺(tái)主機(jī)上的負(fù)載均衡服務(wù)器,則更是如此。X-Forwarded-For 的出現(xiàn),就是為了向服務(wù)器提供更有用的客戶端 IP 地址。
X-Forwarded-For: <client>, <proxy1>, <proxy2>
<client>
客戶端的 IP 地址。
<proxy1>, <proxy2>
如果請(qǐng)求經(jīng)過多個(gè)代理服務(wù)器,每個(gè)代理服務(wù)器的 IP 地址會(huì)依次出現(xiàn)在列表中。
這意味著,如果客戶端和代理服務(wù)器行為良好,最右邊的 IP 地址會(huì)是最近的代理服務(wù)器的 IP 地址,
最左邊的 IP 地址會(huì)是原始客戶端的 IP 地址。
引用: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Forwarded-For
實(shí)際應(yīng)用落地
實(shí)際落地過程中, 我們不僅要考慮轉(zhuǎn)發(fā)能力, 還要有相對(duì)應(yīng)的日志、超時(shí)、優(yōu)雅錯(cuò)誤處理等能力,
下面將講解怎么基于官方內(nèi)置的 ReverseProxy 對(duì)象的代理能力來實(shí)現(xiàn)這些功能.

設(shè)計(jì)思路: 對(duì)外實(shí)現(xiàn) Proxy ServerHttp版的接口, 在內(nèi)部利用 ReverseProxy 對(duì)象代理能力基礎(chǔ)上設(shè)計(jì).
1. 定義proxy ServeHTTP對(duì)象
type ServeHTTP struct {
// 代理鏈接地址
targetUrl string
// net/http 內(nèi)置的 ReverseProxy 對(duì)象
reverseProxy *httputil.ReverseProxy
// 代理錯(cuò)誤處理
proxyErrorHandler ProxyErrorHandler
// 日志對(duì)象
logger log.Logger
}下面我們實(shí)例化對(duì)象
// NewServeHTTP 初始化代理對(duì)象
func NewServeHTTP(targetUrl string, logger log.Logger) *ServeHTTP {
target, err := url.Parse(targetUrl)
if err != nil {
panic(err)
}
// 重新設(shè)置 Director 復(fù)制請(qǐng)求處理
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,
}
// 設(shè)置trasport處理對(duì)象(主要調(diào)配鏈接池大小和超時(shí)時(shí)間)
serveHttp.reverseProxy.Transport = HttpTransportDefault()
// 定義錯(cuò)誤處理
serveHttp.reverseProxy.ErrorHandler = serveHttp.getErrorHandler(logger)
// 定義響應(yīng)處理
serveHttp.reverseProxy.ModifyResponse = serveHttp.getResponseHandler(logger)
return serveHttp
}
// SetProxyErrorFunc 設(shè)置錯(cuò)誤處理函數(shù)
func (s *ServeHTTP) SetProxyErrorFunc(handler ProxyErrorHandler) *ServeHTTP {
s.proxyErrorHandler = handler
return s
}2. 我們重寫了 reverseProxy 的 Director方法
1.我們不希望轉(zhuǎn)發(fā) X-Forwarded-For 到代理層, 通過手動(dòng)賦值為nil方式解決
原因是網(wǎng)絡(luò)防火墻對(duì)源IP進(jìn)行了驗(yàn)證, X-Forwarded-For是可選項(xiàng)之一, 但通常 X-Forwarded-For 不安全且容易造成本地聯(lián)通性問題, 不建議通過此參數(shù)進(jìn)行驗(yàn)證, 故將此移除.
2.移除指定的 removeRequestHeaders 頭
常見的鑒權(quán)類頭等
3. 覆蓋官方默認(rèn)的 HttpTransportDefault
在 http.Transport 對(duì)象中, MaxIdleConnsPerHost、MaxIdleConns 參數(shù)在 http1.1 下非常影響性能, 默認(rèn) 同host 建立的鏈接池內(nèi)連接數(shù)只有2個(gè), 下面我們統(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. 定義請(qǐng)求處理部分
考慮到在請(qǐng)求 reverseProxy 對(duì)象轉(zhuǎn)發(fā)邏輯時(shí),需要攔截請(qǐng)求進(jìn)行前置參數(shù)處理, 不能直接使用 reverseProxy 對(duì)象, 所以就由自定義 proxy 實(shí)現(xiàn) handler 接口的 ServeHTTP 方法, 對(duì) reverseProxy 鏈接處理進(jìn)行一層包裝.
邏輯如下:
// ServeHTTP 服務(wù)轉(zhuǎn)發(fā)
func (s *ServeHTTP) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var (
reqBody []byte
err error
// 生成traceId
traceId = s.getTraceId(request)
)
// 前置獲取請(qǐng)求頭, 放入context中
// 調(diào)用結(jié)束后請(qǐng)求 body 將會(huì)被關(guān)閉, 后面將無法再獲取
if request.Body != nil {
reqBody, err = io.ReadAll(request.Body)
if err == nil {
request.Body = io.NopCloser(bytes.NewBuffer(reqBody))
}
}
// header 設(shè)置 traceId和超時(shí)時(shí)間傳遞
request.Header.Set(utils.TraceKey, traceId)
request.Header.Set(utils.Timeoutkey, cast.ToString(s.getTimeout(request)))
// 計(jì)算獲取超時(shí)時(shí)間, 發(fā)起轉(zhuǎn)發(fā)請(qǐng)求
ctx, cancel := context.WithTimeout(
request.Context(),
time.Duration(s.getTimeout(request))*time.Millisecond,
)
defer cancel()
// 設(shè)置請(qǐng)求體
ctx = context.WithValue(ctx, ctxReqBody, string(reqBody))
// 設(shè)置請(qǐng)求時(shí)間, 用于響應(yīng)結(jié)束后計(jì)算請(qǐng)求耗時(shí)
ctx = context.WithValue(ctx, ctxReqTime, time.Now())
// context 設(shè)置traceId, 用于鏈路日志打印
ctx = context.WithValue(ctx, utils.TraceKey, traceId)
request = request.WithContext(ctx)
// 調(diào)用 reverseProxy ServeHTTP, 處理轉(zhuǎn)發(fā)邏輯
s.reverseProxy.ServeHTTP(writer, request)
}以上代碼均有詳細(xì)注釋, 下面我們看下 traceId和請(qǐng)求耗時(shí)函數(shù)邏輯, 比較簡(jiǎn)單.
// 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 獲取超時(shí)時(shí)間
// header中不存在timeoutKey, 返回默認(rèn)超時(shí)時(shí)間
// header頭存在, 則判斷是否大于默認(rèn)超時(shí)時(shí)間, 大于則使用默認(rèn)超時(shí)時(shí)間
// 否則返回header設(shè)置的超時(shí)時(shí)間
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. 定義響應(yīng)部分和錯(cuò)誤處理部分
從一開始我們就了解 ReverseProxy 功能, 可以設(shè)置 ModifyResponse、ErrorHandler, 下面我們看下具體是怎么實(shí)現(xiàn)的.
ErrorHandler
// getErrorHandler 記錄錯(cuò)誤記錄
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))
}
}
// 初始化時(shí)確認(rèn)proxyErrorHandler具體處理方法
// 調(diào)用 proxyErrorHandler,處理響應(yīng)部分
s.proxyErrorHandler(writer, e)
// 獲取必要信息, 記錄錯(cuò)誤日志
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è)務(wù)錯(cuò)誤處理
// 包含默認(rèn)錯(cuò)誤響應(yīng)和具體代理業(yè)務(wù)錯(cuò)誤響應(yīng).
// 以下為某個(gè)業(yè)務(wù)響應(yīng)
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")
// 設(shè)置狀態(tài)碼為200
writer.WriteHeader(http.StatusOK)
// 將響應(yīng)值序列化
respByte, _ := json.Marshal(resp)
// 將response數(shù)據(jù)寫入writer, 刷新到Flush
// 關(guān)于Flush部分, 一般是不需要主動(dòng)刷新的, 請(qǐng)求結(jié)束后會(huì)自動(dòng)Flush
_, _ = fmt.Fprintf(writer, string(respByte))
if f, ok := writer.(http.Flusher); ok {
f.Flush()
}
}以上有一個(gè)值的關(guān)注的地方, 設(shè)置響應(yīng)頭一定要在設(shè)置響應(yīng)碼之前, 否則將無效
設(shè)置響應(yīng)內(nèi)容一定在最后, 否則將設(shè)置失敗并返回錯(cuò)誤.
ModifyResponse 處理邏輯
// getResponseHandler 獲取響應(yīng)數(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
)
// 獲取請(qǐng)求體
reqBody := response.Request.Context().Value(ctxReqBody)
// 獲取開始請(qǐng)求時(shí)間, 計(jì)算請(qǐng)求耗時(shí)
startTime := response.Request.Context().Value(ctxReqBody)
if startTime != nil {
_, ok := startTime.(time.Time)
if ok {
duration = time.Since(startTime.(time.Time)).Seconds()
}
}
// 獲取響應(yīng)數(shù)據(jù)
// 如果響應(yīng)碼非200, 調(diào)整日志等級(jí)
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),
)
// 設(shè)置響應(yīng)頭
response.Header.Set("Content-Type", "application/json; charset=utf-8")
return nil
}
}默認(rèn)代理服務(wù)器是不設(shè)置響應(yīng)頭的, 則為默認(rèn)的響應(yīng)頭。
響應(yīng)頭必須手動(dòng)設(shè)置
6. 使用自定義的 proxy 代理請(qǐng)求
urlStr := "https://" + targetHost
proxy := utilsProxy.NewServeHTTP(urlStr, logger).SetProxyErrorFunc(utilsProxy.XXXProxyErrorHandler)
log.Fatal(http.ListenAndServe(":8082", proxy))以上就是Golang實(shí)現(xiàn)反向代理的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Go反向代理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
細(xì)細(xì)探究Go 泛型generic設(shè)計(jì)
這篇文章主要帶大家細(xì)細(xì)探究了Go 泛型generic設(shè)計(jì)及示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Golang迭代如何在Go中循環(huán)數(shù)據(jù)結(jié)構(gòu)使用詳解
這篇文章主要為大家介紹了Golang迭代之如何在Go中循環(huán)數(shù)據(jù)結(jié)構(gòu)使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
GoLang中socket心跳檢測(cè)的實(shí)現(xiàn)
本文主要介紹了GoLang中socket心跳檢測(cè)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
Go實(shí)現(xiàn)MD5加密的三種方法小結(jié)
本文主要介紹了Go實(shí)現(xiàn)MD5加密的三種方法小結(jié),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03

