「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」。
写模拟 API 服务工具的时候,想实现这样一个功能。
如果单个 API 未开发完成,前端可选择使用模拟 API 服务;如果单个 API 已开发完成,前端也可根据需要选择使用模拟 API 服务或调用真实 API。
那在模拟 API 服务的工具里加上转发真实 API 的处理,根据设定选择使用就可以了。
网上搜了一下,如果直接转发,然后把转发后的结果直接再返回给调用端的话,还是比较简单。
只是如果中间想记录下请求响应的各种数据时, Body 读取后即关闭,想要再次读取的话,需要使用 Body 的内容重新创建 Reader。
相关代码: hellogo/proxy at main · bettersun/hellogo (github.com)
选项
// Option 选项
type Option struct {
Port string `yaml:"port"` // 端口
ApiHost string `yaml:"apiHost"` // API主机(协议 + IP + 端口)
}
选项中定义了两个成员:
- 一个是端口,即服务启动时的端口。
- 一个是API 主机,即真实API服务所在主机的 协议 + IP + 端口。
golang的HTTP服务
// Proxy 代理
// option: 选项
func Proxy(option Option) {
// 监听端口
port := ":" + option.Port
server = http.Server{
Addr: port,
}
log.Println(fmt.Sprintf("服务运行中... 端口[%v]", option.Port))
http.ListenAndServe(port, http.HandlerFunc(DoHandle))
}
响应函数
// DoHandle 响应函数
func DoHandle(w http.ResponseWriter, r *http.Request) {
// 转发请求
doProxy(w, r, VerOption)
}
这里定义了一个全局公共变量:选项。
// VerOption 全局公共变量:选项
var VerOption Option
全局变量应该控制使用,因为http.HandlerFunc()的参数是固定的,其它需要设定的选项就只能通过全局变量来控制了。
如果有什么其它好方法,不吝赐教。
响应函数里只是把参数的 http.ResponseWriter 和 *http.Request 都传递给实际的处理函数,顺便把全局变量的选项也传递给实际的处理函数。
转发请求
// 转发请求
func doProxy(w http.ResponseWriter, r *http.Request, option Option) {
// 创建一个HttpClient用于转发请求
cli := &http.Client{}
// 读取请求的Body
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println("读取请求体发生错误")
// 响应状态码
w.WriteHeader(http.StatusServiceUnavailable)
return
}
// 转发的URL
reqURL := option.ApiHost + r.URL.String()
// 创建转发用的请求
reqProxy, err := http.NewRequest(r.Method, reqURL, strings.NewReader(string(body)))
if err != nil {
log.Println("创建转发请求发生错误")
// 响应状态码
w.WriteHeader(http.StatusServiceUnavailable)
return
}
// 转发请求的 Header
for k, v := range r.Header {
reqProxy.Header.Set(k, v[0])
}
// 发起请求
responseProxy, err := cli.Do(reqProxy)
if err != nil {
log.Println("转发请求发生错误")
// 响应状态码
w.WriteHeader(http.StatusServiceUnavailable)
return
}
defer responseProxy.Body.Close()
// 转发响应的 Header
for k, v := range responseProxy.Header {
w.Header().Set(k, v[0])
}
// 转发响应的Body数据
var data []byte
// 读取转发响应的Body
data, err = ioutil.ReadAll(responseProxy.Body)
if err != nil {
log.Println("读取响应体发生错误")
// 响应状态码
w.WriteHeader(http.StatusServiceUnavailable)
return
}
// 转发响应的输出数据
var dataOutput []byte
// gzip压缩判断
isGzipped := isGzipped(responseProxy.Header)
// gzip压缩编码数据
if isGzipped {
// 读取后 r.Body 即关闭,无法再次读取
// 若需要再次读取,需要用读取到的内容再次构建Reader
resProxyGzippedBody := ioutil.NopCloser(bytes.NewBuffer(data))
defer resProxyGzippedBody.Close() // 延时关闭
// gzip Reader
gr, err := gzip.NewReader(resProxyGzippedBody)
if err != nil {
log.Println("创建gzip读取器发生错误")
// 响应状态码
w.WriteHeader(http.StatusServiceUnavailable)
return
}
defer gr.Close()
// 读取gzip对象内容
dataOutput, err = ioutil.ReadAll(gr)
if err != nil {
log.Println("读取gzip对象内容发生错误")
// 响应状态码
w.WriteHeader(http.StatusServiceUnavailable)
return
}
} else { // 非gzip压缩
dataOutput = data
}
// 打印转发响应的Body数据,查看转发响应的响应数据时需要。
log.Println(string(dataOutput))
// response的Body不能多次读取,
// 上面已经被读取过一次,需要重新生成可读取的Body数据。
resProxyBody := ioutil.NopCloser(bytes.NewBuffer(data))
defer resProxyBody.Close() // 延时关闭
// 响应状态码
w.WriteHeader(responseProxy.StatusCode)
// 复制转发的响应Body到响应Body
io.Copy(w, resProxyBody)
}
大体的处理流程:
- 接收到的请求的 URL,前面拼接上真实的 API 主机(协议 + IP + 端口)。
- 用接收到的请求的 Body 创建要转发的请求,接收到的请求的 Header 设置给转发请求(真实 API)。
- 发起转发请求(真实 API)。
- 读取转发请求(真实 API)的响应的 Header,设置到要返回给调用端的响应的 Header。
- 读取转发请求(真实 API)的响应的 Body。
因为这里加了一个输出 Body 内容的处理,所以需要读取一下转发请求(真实 API)的响应的 Body。
读取后,响应的 Body 会被关闭,需要使用 Body 的内容重新创建 Reader,再次读取后返回给调用端。
另外,读取 Body 时,这里根据是否为 gzip 加了一个对应的读取处理。 - 设置响应状态码。
- 把转发请求(真实 API)的响应的 Body 返回给调用端。
gzip 判断函数:
// HTTP Header 常量 Content-Encoding
const headerContentEncoding = "Content-Encoding"
const encodingGzip = "gzip"
// gzip压缩判断
func isGzipped(header http.Header) bool {
if header == nil {
return false
}
contentEncoding := header.Get(headerContentEncoding)
isGzipped := false
if strings.Contains(contentEncoding, encodingGzip) {
isGzipped = true
}
return isGzipped
}
现在的写法比较原始,从 Header 中读取 Content-Encoding 的值, 看是否包含 gzip。
启动转发请求的服务
main()里设置一下选项的成员的值,调用上面定义好的转发请求的服务即可。
package main
import (
"hellogo/proxy"
)
func main() {
// 转发请求
proxy.VerOption.ApiHost = "http://127.0.0.1:8012"
proxy.VerOption.Port = "9009"
// 启动服务
proxy.Proxy(proxy.VerOption)
}
用来测试的真实API
package server
import (
"fmt"
"log"
"net/http"
)
func main() {
server := http.Server{
Addr: "127.0.0.1:8012",
}
http.HandleFunc("/", home)
http.HandleFunc("/hello", hello)
log.Println("ListenAndServe 127.0.0.1:8012")
server.ListenAndServe()
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome, bettersun")
}
func hello(w http.ResponseWriter, r *http.Request) {
resp := fmt.Sprintf("[%v] Hello, world.", r.Method)
fmt.Fprintf(w, resp)
}
测试一下
再试一下
如果把真实的地址换成百度会怎么样呢?
因为这个转发请求并不支持 https,这里把真实 API 的主机设置为百度的 http 地址:
proxy.VerOption.ApiHost = "http://www.baidu.com"
再访问一下localhost:9009/,能看到返回了百度的响应内容: