基于Golang实现反向代理服务器(1)手动实现转发逻辑

目标:理解HTTP代理的本质——读取客户端请求,建立到后端连接,发送请求,读取响应,写回客户端。

httputil.ReverseProxy 实现了什么?

proxy := httputil.NewSingleHostReverseProxy(target)

这段代码返回一个 ReverseProxy 对象,该对象本质上是一个 http.Handler 。

当调用 proxy.ServeHTTP(w, r) 时,实现了以下几个核心流程:

  1. 复制并修改原始请求
  2. 发送请求到后端
  3. 把后端的下响应返回给客户端
  4. 处理部分细节

实现一个简单反向代理

我们尝试自己实现一个 http.HandlerFunc ,在其中:

  1. 创建一个新请求,该请求要发往后端
  2. 把原始请求的内容复制到新请求里(方法、body、header等),但把 URL 改成后端的地址
  3. 用 http.DefaultClient 发送新请求,拿到响应
  4. 把后端响应的内容(状态码、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)
	}
}

发表评论