Go combines Consul to implement dynamic reverse proxy

The core function of a proxy can be summed up in one sentence: accept a request from a client, forward it to a back-end server, get a response and return it to the client.

The reverse proxy

The actual operation mode of Reverse Proxy refers to that the Proxy server receives Internet connection requests, forwards the requests to the server on the internal network, and returns the results obtained from the server to the client requesting Internet connection. In this case, the proxy server behaves as a server.

Implementation logic

According to the description of the agent, there are several steps:

  1. The proxy receives the request from the client and copies the original request object
  2. Modify the request direction of a new request according to some rules
  3. Sends the new request to the base server and receives the response back from the server
  4. The response from the previous step is processed as required and returned to the client

Go language implementation

Native code

Because HTTP requests are received and forwarded, http.handler is implemented

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

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

// Implement http.handler to receive all requests
func (proxy *OriginReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	// 1. The original request object is copied
	r2 := clone(req)

	// 2. Change the requested IP address to the corresponding server address
	target := proxy.servers[rand.Int()%len(proxy.servers)]
	r2.URL.Scheme = target.Scheme
	r2.URL.Host = target.Host

	// 3. Send a new request for replication
	transport := http.DefaultTransport

	res, err := transport.RoundTrip(r2)
	
	/ / 4. Process the response
	iferr ! =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()
}

// Copy the original request to generate a new request
func clone(req *http.Request) *http.Request {
	r2 := new(http.Request)
	*r2 = *req
	r2.URL = cloneURL(req.URL)
	ifreq.Header ! =nil {
		r2.Header = req.Header.Clone()
	}
	ifreq.Trailer ! =nil {
		r2.Trailer = req.Trailer.Clone()
	}
	ifs := 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
	ifu.User ! =nil {
		u2.User = new(url.Userinfo)
		*u2.User = *u.User
	}
	return u2
}
Copy the code

test


// Start a Web project with gin for easy forwarding
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(a) {
	proxy := proxy.NewOriginReverseProxy([]*url.URL{
		{
			Scheme: "http",
			Host:   "localhost:9091",
		},
	})
	http.ListenAndServe(": 19090", proxy)
}

Copy the code

Request http://127.0.0.1:19090/ping returns {” message “:” pong “}

Implementation of Httputil. ReverseProxy

In the above example, we implemented the receiving, copying, forwarding, and processing of the request ourselves. The code I wrote is ok.

From the above example, we are mainly concerned with the second step: change the address of the request to the corresponding server address, the rest of the logic is common, but the official has already handled the logic for us, so let’s look at the official how to implement

In the Httputil. ReverseProxy source code, you can use the custom Director method to modify the original request after the original request is copied, but before the new request is sent. This is where we really change the request. You can modify the response by customizing ModifyResponse and customizing ErrorHandler to handle exceptions

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)
}
Copy the code

Here we modify the request address by customizing the 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}
}


Copy the code

test

Gin’s project still needs to start. There’s no ambiguity here

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

Access Consul to implement dynamic proxy

In the previous article, we discussed how to use Go to implement Consul service discovery. If you want to implement dynamic proxy in Consul, you need to consider how to map the requested address to the corresponding service. Here we need to add some functions on the basis of principle:

  1. Find the corresponding service based on the requested address
  2. Find the corresponding example based on the service

The simplest implementation for the first step is to start the rule with the request address

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}
}
Copy the code

test

func main(a) {
	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)
}
Copy the code

reference

  • Juejin. Cn/post / 688309…