Go | Go 結合 Consul 實現動態反向代理

  • 2021 年 2 月 27 日
  • 筆記

Go 結合 Consul 實現動態反向代理

代理的核心功能可以用一句話概括:接受客戶端的請求,轉發到後端服務器,獲得應答之後返回給客戶端。


Table of Contents


反向代理

反向代理(Reverse Proxy)實際運行方式是指以代理服務器來接受 internet 上的連接請求,然後將請求轉發給內部網絡上的服務器,並將從服務器上得到的結果返回給 internet 上請求連接的客戶端,此時代理服務器對外就表現為一個服務器。

實現邏輯

根據代理的描述一共分成幾個步驟:

  1. 代理接收到客戶端的請求,複製了原來的請求對象
  2. 根據一些規則,修改新請求的請求指向
  3. 把新請求發送到根據服務器端,並接收到服務器端返回的響應
  4. 將上一步的響應根據需求處理一下,然後返回給客戶端

Go 語言實現

原生代碼

因為要接收並轉發 http 請求,所以要實現 http.Handler

type OriginReverseProxy struct {
	servers []*url.URL
}

func NewOriginReverseProxy(targets []*url.URL) *OriginReverseProxy {
	return &OriginReverseProxy{
		servers: targets,
	}
}

// 實現 http.Handler, 用於接收所有的請求
func (proxy *OriginReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	// 1. 複製了原來的請求對象
	r2 := clone(req)

	// 2. 修改請求的地址,換為對應的服務器地址
	target := proxy.servers[rand.Int()%len(proxy.servers)]
	r2.URL.Scheme = target.Scheme
	r2.URL.Host = target.Host

	// 3. 發送複製的新請求
	transport := http.DefaultTransport

	res, err := transport.RoundTrip(r2)
	
	// 4。處理響應
	if err != nil {
		rw.WriteHeader(http.StatusBadGateway)
		return
	}

	for key, value := range res.Header {
		for _, v := range value {
			rw.Header().Add(key, v)
		}
	}

	rw.WriteHeader(res.StatusCode)
	io.Copy(rw, res.Body)
	res.Body.Close()
}

// 複製原請求,生成新的請求
func clone(req *http.Request) *http.Request {
	r2 := new(http.Request)
	*r2 = *req
	r2.URL = cloneURL(req.URL)
	if req.Header != nil {
		r2.Header = req.Header.Clone()
	}
	if req.Trailer != nil {
		r2.Trailer = req.Trailer.Clone()
	}
	if s := req.TransferEncoding; s != nil {
		s2 := make([]string, len(s))
		copy(s2, s)
		r2.TransferEncoding = s2
	}
	r2.Form = cloneURLValues(req.Form)
	r2.PostForm = cloneURLValues(req.PostForm)
	return r2
}

func cloneURLValues(v url.Values) url.Values {
	if v == nil {
		return nil
	}
	return url.Values(http.Header(v).Clone())
}

func cloneURL(u *url.URL) *url.URL {
	if u == nil {
		return nil
	}
	u2 := new(url.URL)
	*u2 = *u
	if u.User != nil {
		u2.User = new(url.Userinfo)
		*u2.User = *u.User
	}
	return u2
}

測試


// 先用 gin 起一個 web 項目,方便轉發
func TestGin(t *testing.T)  {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run(":9091") // listen and serve on 0.0.0.0:9091
}

func main() {
	proxy := proxy.NewOriginReverseProxy([]*url.URL{
		{
			Scheme: "http",
			Host:   "localhost:9091",
		},
	})
	http.ListenAndServe(":19090", proxy)
}

請求 //127.0.0.1:19090/ping 返回 {"message":"pong"}

httputil.ReverseProxy 工具實現

在上面的例子中,我們自己實現了請求的接收、複製、轉發和處理。自己寫的代碼還算湊合吧。

從上面的示例中,其實我們主要關心的是第二步: 修改請求的地址,換為對應的服務器地址, 其餘的邏輯都是通用的,還好官方已經幫我們處理了重複邏輯,那我們看看官方是怎麼實現的

httputil.ReverseProxy 源碼中,可以看出,通過自定義 Director 方法就可以在原請求複製後,新請求轉發出之前對複製出的新請求進行修改,這裡就是我們真正改動的地方,當然如果有其他定製需求,可以通過自定義 ModifyResponse 實現對響應的修改,自定義 ErrorHandler 來處理異常

type ReverseProxy struct {
	// Director must be a function which modifies
	// the request into a new request to be sent
	// using Transport. Its response is then copied
	// back to the original client unmodified.
	// Director must not access the provided Request
	// after returning.
	Director func(*http.Request)

	Transport http.RoundTripper

	FlushInterval time.Duration

	ErrorLog *log.Logger

	BufferPool BufferPool

	ModifyResponse func(*http.Response) error

	ErrorHandler func(http.ResponseWriter, *http.Request, error)
}

這裡我們通過自定義 Director 來修改請求地址


func NewMultipleHostsReverseProxy(targets []*url.URL) *httputil.ReverseProxy {
	director := func(req *http.Request) {
		target := targets[rand.Int()%len(targets)]
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		req.URL.Path = target.Path
	}
	return &httputil.ReverseProxy{Director: director}
}


測試

gin 的項目還是要啟動的,這裡不在贅敘

func TestMultipleHostsReverseProxy(t *testing.T) {
	proxy := proxy.NewMultipleHostsReverseProxy([]*url.URL{
		{
			Scheme: "http",
			Host:   "localhost:9091",
		},
	})
	http.ListenAndServe(":9090", proxy)
}

接入 consul 實現動態代理

在前面的一篇文章中講了如何用 Go 實現 Consul 的服務發現, 如果要結合 consul 實現動態代理,需要考慮如何將請求的地址和對應的服務對應上。這裡需要在原理的基礎上加上一下功能:

  1. 根據請求地址找到對應的服務
  2. 根據服務找到對應的示例

針對第一步先實現最簡單的,就以 請求地址開頭 為規則

type LoadBalanceRoute interface {
	ObtainInstance(path string) *url.URL
}

type Route struct {
	Path string
	ServiceName string
}

type DiscoveryLoadBalanceRoute struct {

	DiscoveryClient DiscoveryClient

	Routes []Route

}

func (d DiscoveryLoadBalanceRoute) ObtainInstance(path string) *url.URL {
	for _, route := range d.Routes {
		if strings.Index(path, route.Path) == 0 {
			instances, _ := d.DiscoveryClient.GetInstances(route.ServiceName)
			instance := instances[rand.Int()%len(instances)]
			scheme := "http"
			return &url.URL{
				Scheme: scheme,
				Host: instance.GetHost(),
			}
		}
	}
	return nil
}

func NewLoadBalanceReverseProxy(lb LoadBalanceRoute) *httputil.ReverseProxy {
	director := func(req *http.Request) {
		target := lb.ObtainInstance(req.URL.Path)
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
	}
	return &httputil.ReverseProxy{Director: director}
}

測試

func main() {
	registry, _ := proxy.NewConsulServiceRegistry("127.0.0.1", 8500, "")
	reverseProxy := proxy.NewLoadBalanceReverseProxy(&proxy.DiscoveryLoadBalanceRoute{
		DiscoveryClient: registry,
		Routes: []proxy.Route{
			{
				Path: "abc",
				ServiceName: "abc",
			},
		},
	})
	http.ListenAndServe(":19090", reverseProxy)
}

參考

白色兔子公眾號圖片