最近对 Go 语言的反向代理使用得偏多,其实在大概两年前就写过 TCP 层面的代理,而且那时也是用的 Go 语言,不同之处在于之前只是偶尔尝试一下使用,最近是因为工作需要使用的。相比较于 TCP 层面的代理,HTTP 的代理实现起来麻烦事比较多,如果我们仅仅是简单的反向代理,OK,那还好,做个 Host 替换就差不多了。但是,很多时候我们作反向代理,那么需求就比较多样了,例如我们可能希望对代理的响应内容做些改变,也可能希望是 HA 的反向代理。

Go 语言在自身的内置库中就提供了一个很方便的反向代理组件,使用内置的 httputil 的组件我们可以非常快的开发出一个简单的反向代理,稍后我们会看到。但是,一旦与我们前面提到的多样化的需求的时候,就不能简单得写代码了,我们得了解一些组件,然后根据需求对组件进行个性化,从而满足我们的需求。下面,我就从简入繁来看下 Go 语言内置的反向代理库。

内置反向代理

根据 Go 语言官方文档:Package httputil 中描述,我们可以写这么一段简单的代码,这段代码是从 Go 语言的官方文档上抄下来的:

这里我对这段代码解释一下:

执行一下这段代码,应该看到的结果是:

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 2Proxy:ProxyFromEnviroment 会使用系统默认的代理,如果你设置了环境变量 HTTP_PROXY 的话,那么默认是会使用这个代理的,切记切记,这是个坑!

小结

DefaultTransport 这里就不再往下深挖了,因为到这里已经可以满足平时的大部分需求了,再往下其实就涉及到 Go 语言中 HTTP 模块的核心功能了,这些会在后面的文章中讲。在这篇文章中,虽然我只是讲了三个组件,但是,这三个小组件已经很够了,而且,在本文中,我也着重描述了可能出现坑的几个地方,这些地方都值得我们去关注,因为在使用 ReverseProxy 的时候,一般遇到的问题都离不开这几个。

Reference

  1. Package httputil