目标:理解HTTP代理的本质——读取客户端请求,建立到后端连接,发送请求,读取响应,写回客户端。
httputil.ReverseProxy 实现了什么?
proxy := httputil.NewSingleHostReverseProxy(target)
这段代码返回一个 ReverseProxy 对象,该对象本质上是一个 http.Handler 。
当调用 proxy.ServeHTTP(w, r) 时,实现了以下几个核心流程:
- 复制并修改原始请求
- 发送请求到后端
- 把后端的下响应返回给客户端
- 处理部分细节
实现一个简单反向代理
我们尝试自己实现一个 http.HandlerFunc ,在其中:
- 创建一个新请求,该请求要发往后端
- 把原始请求的内容复制到新请求里(方法、body、header等),但把 URL 改成后端的地址
- 用 http.DefaultClient 发送新请求,拿到响应
- 把后端响应的内容(状态码、header、body)写回给客户端
package main
import (
"io"
"log"
"net/http"
"net/url"
)
func ReverseProxy(target *url.URL) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 目标 URL 是 target
// 原始请求的路径是 r.URL.Path, 查询是 r.URL.RawQuery
// 拼接成完整 URL: target.Scheme://target.Host/r.URL.Path?r.URL.RawQuery
proxyURL := target.ResolveReference(r.URL)
// 该方法将一个相对 URL 解析成一个绝对 URL
req, err := http.NewRequest(r.Method, proxyURL.String(), r.Body)
// 注意 r.Body 只能读一次,如果之前被其他中间件读过了或者后续还需要读,需要额外处理
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
req.Header = r.Header.Clone()
req.Host = target.Host
// 默认 http.NewRequest 会用 URL 中的 Host 填充 req.Host,但 header 中的 Host 会被忽略
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 避免资源泄露
// 状态码
w.WriteHeader(resp.StatusCode)
// header
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
// body
_, err = io.Copy(w, resp.Body)
// io.Copy(dst, src) 的作用是把 src 的所有数据复制到 dst 里
// dst 必须实现 io.Writer 接口(有 Write 方法)
// src 必须实现 io.Reader 接口(有 Read 方法)
if err != nil {
log.Println("Error copying body:", err)
}
}
}
func main() {
target, err := url.Parse("http://127.0.0.1:8000")
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", ReverseProxy(target))
log.Println("Starting proxy server on :8080")
err = http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}