布景

当咱们谈到反向署理时,能够将其比喻为一个“中间人”。幻想一下,你是一个用户,你想要拜访某个网站。可是,这个网站并不直接向你供给服务,而是委托了一个署理来处理你的恳求。这个署理就是反向署理。

你能够把反向署理幻想成一个十分聪明的帮手,它能够帮助你与网站进行沟通。当你发送恳求时,它会接纳到你的恳求,并将其转发给网站。然后,网站会将呼应发送给反向署理,反向署理再将呼应发送给你。这样,你就能够与网站进行交互,而不需要直接与网站通讯。

net/http 包里面已经帮咱们内置了具有反向署理才能 ReverseProxy 方针, 可是它的才能有限, 从工程才能上面还有很多自行完成.

本文包括了叙述官方代码内部完成, 一起结合本身需求叙述改造后代码逻辑

因为笔者才能和精力有限, 因本文包括了大段代码, 难免阅览起来榜首感觉较为繁琐杂乱, 但大部分代码都进行了详细的注释标注, 可事务顶用到时再回来详读代码部分.

咱们也可阅览底部参阅链接部分, 挑选的质量都很精简, 相信咱们肯定能有所收获.

官方代码剖析

简略运用

首先咱们看下进口完成, 只需要几行代码, 就将所有流量署理到了 www.domain.com 上

// 设置要转发的地址
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 域下.

golang 实现反向代理

可是一般上述是无法满足咱们需求的, 比方有鉴权、超时操控、链路传递、恳求日志记载等常见需求, 这样咱们怎样来完成呢? 在开端之前, 咱们先了解下官方内置了哪些才能, 详细是怎样工作的.

底层结构

官方的 ReverseProxy 供给的结构:

type ReverseProxy struct {
	// 对恳求内容进行修正 (方针是事务传入req的一个副本)
	Director func(*http.Request)
	// 衔接池复用衔接,用于履行恳求, 默以为http.DefaultTransport 
	Transport http.RoundTripper
	// 守时改写内容到客户端的时刻距离(流式/无内容此参数疏忽)
	FlushInterval time.Duration
	// 默以为std.err,用于记载内部过错日志
	ErrorLog *log.Logger
	// 用于履行 copyBuffer 复制呼应体时,利用的bytes内存池化
	BufferPool BufferPool
	// 假如配置后, 可修正方针署理的呼应成果(呼应头和内容)
    // 假如此办法回来error, 将调用 ErrorHandler 办法
	ModifyResponse func(*http.Response) error
    // 配置后署理履行过程中, 产生过错均会回调此办法
    // 默许逻辑不呼应任务内容, 状况码回来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 为: github.com/golang/go/i…
目的是为了防止恳求头User-Agent被污染, 在http底层包主张恳求时, 假如未设置 User-Agent 将会运用 Go-http-client/1.1 替代

详细代码地址: github.com/golang/go/b…

主张恳求部分

http.ListenAndServe(“:8082”, proxy) 发动服务时, 处理恳求的工作主要是 Handler 接口ServeHTTP 办法.

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

ReverseProxy 中默许已完成此接口, 以下是处理恳求的中心逻辑

golang 实现反向代理

咱们来看下代码是怎样处理的

// 服务恳求处理办法
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情况下, 将会产生重试
        // 防止因为复制传入的request创立传入署理的恳求内容, 导致无法产生重试.
        // https://github.com/golang/go/issues/16036
		outreq.Body = nil
	}
	if outreq.Body != nil {
		// 防止因panic问题导致恳求未正确关闭, 其他协程持续从中读取
        // 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)
	}
    // 调用完成的 Director 办法修正恳求署理的request方针
	p.Director(outreq)
	if outreq.Form != nil {
		outreq.URL.RawQuery = cleanQueryParams(outreq.URL.RawQuery)
	}
	outreq.Close = false
    // 晋级http协议,HTTP Upgrade
    // 判断header Connection 中是否有Upgrade
	reqUpType := upgradeType(outreq.Header)
    // 依据《网络交换的 ASCII 格式》标准, 晋级协议中是否包括制止运用的字符
    // 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)
	// 恳求下流依据RFC标准移除协议头
	for _, h := range hopHeaders {
		outreq.Header.Del(h)
	}
	// Transfer-Encoding: chunked 分块传输编码
	if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
		outreq.Header.Set("Te", "trailers")
	}
	// 恳求下流指定协议晋级, 例如 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
        // 这个参数下面咱们将详细说明
		omit := ok && prior == nil
		if len(prior) > 0 {
			clientIP = strings.Join(prior, ", ") + ", " + clientIP
		}
		if !omit {
			outreq.Header.Set("X-Forwarded-For", clientIP)
		}
	}
    // 运用transport方针中维护的链接池, 向下流主张恳求
	res, err := transport.RoundTrip(outreq)
	if err != nil {
		p.getErrorHandler()(rw, outreq, err)
		return
	}
    // 处理下流呼应的晋级协议恳求
	// 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
	}
    // 依据协议标准删除呼应 Connection 头
	removeConnectionHeaders(res.Header)
    // 下流呼应依据RFC标准移除协议头
	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)
	// 分块传输部分协议 header 头设置, 已跳过
    // 写入呼应码到上游response方针
	rw.WriteHeader(res.StatusCode)
    // 拷贝成果到上游
    // flushInterval将呼应守时改写到缓冲区
	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 通用标头操控网络衔接在当时会话完成后是否仍然保持翻开状况。假如发送的值是 keep-alive,则衔接是持久的,不会关闭,允许对同一服务器进行后续恳求。

这个头设置处理的是客户端和服务端链接方法, 而不应该透传给署理的下流服务.

所以再RFC中有以下明确规定:

“Connection”头字段允许发送者指示所需的衔接 当时衔接的操控选项。为了防止混杂下流接纳者,署理或网关有必要删除或在转发之前替换任何收到的衔接选项信息。

RFC: datatracker.ietf.org/doc/html/rf…

2. X-Forwarded-For 效果

X-Forwarded-For(XFF)恳求标头是一个事实上的用于标识经过署理服务器衔接到 web 服务器的客户端的原始 IP 地址的标头(很简略被篡改)。

当客户端直接衔接到服务器时,其 IP 地址被发送给服务器(并且经常被记载在服务器的拜访日志中)。可是假如客户端经过正向或反向署理服务器进行衔接,服务器就只能看到最后一个署理服务器的 IP 地址,这个 IP 一般没什么用。假如最后一个署理服务器是与服务器安装在同一台主机上的负载均衡服务器,则更是如此。X-Forwarded-For 的呈现,就是为了向服务器供给更有用的客户端 IP 地址。

X-Forwarded-For: <client>, <proxy1>, <proxy2>
<client>
客户端的 IP 地址。
<proxy1>, <proxy2>
假如恳求经过多个署理服务器,每个署理服务器的 IP 地址会依次呈现在列表中。
这意味着,假如客户端和署理服务器行为良好,最右边的 IP 地址会是最近的署理服务器的 IP 地址,
最左边的 IP 地址会是原始客户端的 IP 地址。

引证: developer.mozilla.org/zh-CN/docs/…

实践应用落地

实践落地过程中, 咱们不只要考虑转发才能, 还要有相对应的日志、超时、高雅过错处理等才能,

下面将讲解怎样基于官方内置的 ReverseProxy 方针的署理才能来完成这些功用.

golang 实现反向代理

设计思路: 对外完成 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 设置过错处理函数
func (s *ServeHTTP) SetProxyErrorFunc(handler ProxyErrorHandler) *ServeHTTP {
	s.proxyErrorHandler = handler
	return s
}

2. 咱们重写了 reverseProxy 的 Director办法

  1. 咱们不希望转发 X-Forwarded-For 到署理层, 经过手动赋值为nil方法处理
    原因是网络防火墙对源IP进行了验证, X-Forwarded-For是可选项之一, 但一般 X-Forwarded-For 不安全且简略形成本地联通性问题, 不主张经过此参数进行验证, 故将此移除.

  2. 移除指定的 removeRequestHeaders 头
    常见的鉴权类头号

3. 掩盖官方默许的 HttpTransportDefault

在 http.Transport 方针中, MaxIdleConnsPerHost、MaxIdleConns 参数在 http1.1 下十分影响性能, 默许 同host 树立的链接池内衔接数只要2个, 下面咱们统一修正为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 方针转发逻辑时,需要阻拦恳求进行前置参数处理, 不能直接运用 reverseProxy 方针, 所以就由自界说 proxy 完成 handler 接口的 ServeHTTP 办法, 对 reverseProxy 链接处理进行一层包装.

逻辑如下:

// ServeHTTP 服务转发
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)))
	// 核算获取超时时刻, 主张转发恳求
	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, 处理转发逻辑
	s.reverseProxy.ServeHTTP(writer, request)
}

以上代码均有详细注释, 下面咱们看下 traceId和恳求耗时函数逻辑, 比较简略.

// 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 headerTimeoutMs
}

5. 界说呼应部分和过错处理部分

从一开端咱们就了解 ReverseProxy 功用, 能够设置 ModifyResponse、ErrorHandler, 下面咱们看下详细是怎样完成的.

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),
		)
	}
}
// 详细署理事务过错处理
// 包括默许过错呼应和详细署理事务过错呼应. 
// 以下为某个事务呼应
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")
    // 设置状况码为200
	writer.WriteHeader(http.StatusOK)
    // 将呼应值序列化
	respByte, _ := json.Marshal(resp)
    // 将response数据写入writer, 改写到Flush
    // 关于Flush部分, 一般是不需要主动改写的, 恳求完毕后会自动Flush
	_, _ = fmt.Fprintf(writer, string(respByte))
	if f, ok := writer.(http.Flusher); ok {
		f.Flush()
	}
}

以上有一个值的关注的当地, 设置呼应头必定要在设置呼应码之前, 不然将无效

设置呼应内容必定在最后, 不然将设置失败并回来过错.

ModifyResponse 处理逻辑

// getResponseHandler 获取呼应数据
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()
			}
		}
        // 获取呼应数据
        // 假如呼应码非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简略而强壮的反向署理】 h1z3y3.me/posts/simpl…

【Golang ReverseProxy 怎么完成反向署理?】/post/697330…

【golang反向署理源码解析】www.cnblogs.com/FengZeng666…

【golang x-forwared-for issues】github.com/golang/go/i…

【golang User-Agent issues】github.com/golang/go/i…

【golang req body issuses】github.com/golang/go/i…

【golang header头设置的坑】blog.alovn.cn/2020/01/20/…

【http协议】developer.mozilla.org/zh-CN/docs/…

【rfc Connection协议头标准】datatracker.ietf.org/doc/html/rf…

【rfc 协议网络字符标准】datatracker.ietf.org/doc/html/rf…