Proxy Server Overview

The function of a Proxy Server is to obtain network information on behalf of network users. Figuratively speaking, it is the network information transfer station, is the intermediary agency between individual network and Internet service provider, responsible for forwarding legitimate network information, control and registration of forwarding.

As a bridge between Internet and Intranet, proxy server plays an extremely important role in practical applications. It can be used for multiple purposes. The most basic function is connection, and it also includes security, cache, content filtering, access control management and other functions. More importantly, proxy server is an important security function provided by Internet link-level gateway, and its work is mainly in the open System Interconnection (OSI) dialogue layer

This use of Go language to achieve a simple HTTP proxy server, mainly divided into the following parts to complete:

  • Implement a simple Web server

  • Implement a simple proxy server

    • Manual implementation
      • Configure the proxy WEB object through the INI file
      • Implement a basic proxy server based on access paths
      • Implement Basic authentication of proxy server
      • Implemented using the GO built-in proxy function
    • Implement proxy server load balancing
      • Simple random load
      • IP_HASH load
      • Load weighted randomness
      • Polling load
        • Polling weighted
        • Smooth polling weighting
    • Load balancing HTTPSERVER health check
      • Simple health check
      • Implementing a simple FailOver

Implement a simple Web server

Complete two Web servers using Go’s Http and listen on ports 9001 and 9002, respectively

type web1Handler struct{}

func (h web1Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	_, _ = writer.Write([]byte("WEB1"))}type web2Handler struct{}

func (h web2Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	_, _ = writer.Write([]byte("WEB2"))}func main(a) {

	c := make(chan os.Signal)

	go func(a) {
		_ = http.ListenAndServe(": 9001", web1Handler{})
	}()

	go func(a) {
		_ = http.ListenAndServe(": 9002", web2Handler{})
	}()

	signal.Notify(c, os.Interrupt)

	s := <-c

	log.Println(s)
}

Copy the code

Implement a simple proxy server

Manual implementation

  • Configure the proxy WEB object through the INI file

    Create env.ini file to store the list of WEB servers that need to be propped

    [proxy]
    
    [proxy.a]
    path=/a
    pass=http://localhost:9001
    
    [proxy.b]
    path=/b
    pass=http://localhost:9002
    Copy the code

    Read configuration files. Read ini files using third-party dependencies

    go get github.com/go-ini/ini
    Copy the code
    var ProxyConfigs map[string]string
    
    type EnvConfig *os.File
    
    func init(a) {
    	ProxyConfigs = make(map[string]string)
    	EnvConfig, err := ini.Load("env.ini")
    	iferr ! =nil {
    		fmt.Println(err)
    	}
    	section, _ := EnvConfig.GetSection("proxy")
    	ifsection ! =nil {
    		sections := section.ChildSections()
    		for _, s := range sections {
    			path, _ := s.GetKey("path")
    			pass, _ := s.GetKey("pass")
    			ifpath ! =nil&& pass ! =nil {
    				ProxyConfigs[path.Value()] = pass.Value()
    			}
    		}
    	}
    }
    Copy the code

    Implement basic proxy functions based on access paths

    Obtain the PrxoyConfigs configuration item list, and obtain the corresponding path and Web server access path

    for k, v := range ProxyConfigs {
        fmt.Println(k,v)
        if matched, _ := regexp.MatchString(k, request.URL.Path); matched == true {
            // Proxy processing
            RequestUrl(request, writer, v)
            return
        }
    }
    _, _ = writer.Write([]byte("defaut"))
    Copy the code

    The proxy server implements Basic authentication by returning the original HTTP Request header and HTTP Response Header to the browser

    Basic is a simple HTTP authentication method. The client transmits the user name and password to the server in plain text (Base64 encoding format) for authentication. Usually, HTTPS is required to ensure the security of information transmission

    Basic authentication adds the www-Authenticate Header to the Response Header. After the browser identifies Basic, the dialog box Realm is displayed indicating the security domain of the protected document on the Web server

    Enable Basic authentication for WEB1 servers

    func (h web1Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
    	auth := request.Header.Get("Authorization")
    	if auth == "" {
    		writer.Header().Set("WWW-Authenticate".'Basic realm=" You must enter a username and password "')
    		writer.WriteHeader(http.StatusUnauthorized)
    		return
    	}
    
    	authList := strings.Split(auth, "")
    	if len(authList) == 2 && authList[0] = ="Basic" {
    		res, err := base64.StdEncoding.DecodeString(authList[1])
    		if err == nil && string(res) == "tom:123" {
    			_, _ = writer.Write([]byte(fmt.Sprintf("web1,form ip:%s", GetIp(request))))
    			return
    		}
    	}
    	_, _ = writer.Write([]byte("Wrong username or password"))}Copy the code

    The effect is shown below:

    What the proxy server needs to do is copy the input headers and output headers.

    func CloneHead(src http.Header, dest *http.Header) {
    	for k, v := range src {
    		dest.Set(k, v[0])}}Copy the code

    Proxy server Proxy logic

    func RequestUrl(request *http.Request, writer http.ResponseWriter, url string) {
    	fmt.Println(request.RemoteAddr)
    	newReq, _ := http.NewRequest(request.Method, url, request.Body)
    
    	CloneHead(request.Header, &newReq.Header)
    
    	if ip := request.Header.Get(XForwardedFor); ip == "" {
    		newReq.Header.Add(XForwardedFor, request.RemoteAddr)
    	}
    
    	response, _ := http.DefaultClient.Do(newReq)
    
    	getHeader := writer.Header()
    	CloneHead(response.Header, &getHeader)
    	writer.WriteHeader(response.StatusCode)
    
    	defer response.Body.Close()
    	c, _ := ioutil.ReadAll(response.Body)
    	_, _ = writer.Write(c)
    }
    Copy the code

    , by using the method of manual implementation above agent already know about agent in logic, how about go whether existing proxy function, the answer is yes, direct use httpUtil. NewSingleHostReverseProxy direct implementation

    for k, v := range ProxyConfigs {
        fmt.Println(k,v)
        if matched, _ := regexp.MatchString(k, request.URL.Path); matched == true {
    
            target, _ := url.Parse(v)
            proxy := httputil.NewSingleHostReverseProxy(target)
            proxy.ServeHTTP(writer, request)
            // RequestUrl(request, writer, v)
            return}}Copy the code

Implement proxy server load balancing

Load Balance refers to balancing loads (work tasks) and allocating them to multiple operation units, such as the Web server, FTP server, enterprise core application server, and other main task servers, so as to cooperatively complete work tasks.

Random load selects a random server from the server list for access through a random algorithm. According to probability theory, as the number of times the client calls the server increases, the actual effect tends to be equal distribution of each server requested to the server, that is, to achieve the effect of polling.

To facilitate the viewing, adjust the web server code and write the Web server access address to the proxy.

The web server

type web1Handler struct{}

func (h web1Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	_, _ = writer.Write([]byte("web1"))}type web2Handler struct{}

func (h web2Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	_, _ = writer.Write([]byte("web2"))}Copy the code

Add LoadBalance

package util

import (
	"math/rand"
	"time"
)

type HttpServer struct {
	Host string
}

type LoadBalance struct {
	Servers []*HttpServer
}

func NewHttpServer(host string) *HttpServer {
	return &HttpServer{
		Host: host,
	}
}

func NewLoadBalance(a) *LoadBalance {
	return &LoadBalance{
		Servers: make([]*HttpServer, 0),}}func (b *LoadBalance) AddServer(server *HttpServer) {
	b.Servers = append(b.Servers, server)
}

func (b *LoadBalance) SelectByRand(a) *HttpServer {
	rand.Seed(time.Now().UnixNano())
	index:=rand.Intn(len(b.Servers))
	return b.Servers[index]
}
Copy the code

Use the GO RAND function to randomly select the HTTPSERVER

Adjust the PROXY

func (*ProxyHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	defer func(a) {
		if err := recover(a); err ! =nil {
			writer.WriteHeader(500)
			_, _ = writer.Write([]byte("server error"))
			log.Println(err)
		}
	}()

	bl := NewLoadBalance()
	bl.AddServer(NewHttpServer("http://localhost:9001"))
	bl.AddServer(NewHttpServer("http://localhost:9002"))

	hostUrl, _ := url.Parse(bl.SelectByRand().Host)

	proxy := httputil.NewSingleHostReverseProxy(hostUrl)
	proxy.ServeHTTP(writer, request)
}
Copy the code

The end result: visit local http://localhost:8080 and the page will display either web1 or Web2 content randomly

The IP_HASH load is calculated based on the IP address of the client to which the request belongs and then sent to the corresponding backend.

Therefore, requests from the same client are sent to the same backend, unless the backend is unavailable, so IP_HASH can maintain the session effect.

This can be implemented in GO using the CRC algorithm (cyclic redundancy check) and the terminology algorithm.

ip:="127.0.0.1"
fmt.Println(crc32.ChecksumIEEE([]byte(ip)))
// Compute output by IP :3619153832
Copy the code

Added the method of getting HTTPSERVER by IP

func (b *LoadBalance) SelectByIpHash(ip string) *HttpServer {
	index := int(crc32.ChecksumIEEE([]byte(ip))) % len(b.Servers)
	return b.Servers[index]
}
Copy the code

Set the PROXY to the IP_HASH PROXY

ip := request.RemoteAddr
//hostUrl, _ := url.Parse(bl.SelectByRand().Host)
hostUrl, _ := url.Parse(bl.SelectByIpHash(ip).Host)

proxy := httputil.NewSingleHostReverseProxy(hostUrl)
proxy.ServeHTTP(writer, request)
Copy the code

In the end, the same server is accessed when accessing http://localhost:8080.

Load weighted randomization is a random algorithm that adds weights to httpServers. The httpServers are randomly selected based on the weights.

HTTPSERVER WERIGHT calculate the weight of the number of arrays, the weight will account for the number of arrays, random selection, the probability is greater.

Adjust the LoadBalance

/ / add WEIGHT
type HttpServer struct {
	Host   string
	Weight int
}


// Initialize LOADBALANCE and SERVERINDICES
var BL *LoadBalance
var ServerIndices []int

func init(a) {
	BL = NewLoadBalance()
	BL.AddServer(NewHttpServer("http://localhost:9001".5))
	BL.AddServer(NewHttpServer("http://localhost:9002".15))

	for index, server := range BL.Servers {
		if server.Weight > 0 {
			for i := 0; i < server.Weight; i++ {
				ServerIndices = append(ServerIndices, index)
			}
		}
	}

	fmt.Println(ServerIndices)
}

Copy the code

Adjust the PROXY to use random weighting

hostUrl, _ := url.Parse(BL.SelectByWeightRand().Host)

proxy := httputil.NewSingleHostReverseProxy(hostUrl)
proxy.ServeHTTP(writer, request)
Copy the code

The end result, when you visit http://localhost:8080, is 1 to 3, because the weights are now set to 5 and 15

Disadvantages: You need to generate an array slice for the HTTPSERVER list. If the weight value is set too high, it can cause memory problems.

The improved algorithm calculates the value range according to the weight

If the permission is set to 5:2:1,

Through 5, 7 (5 + 2), 8 (5 + 2 + 1)

[0,5] [5,7] [7,8]

Then according to [0,8) within a random number, random number in which interval, that is, which HTTPSERVER

Adjust the WEIGHT RAND method

func (b *LoadBalance) SelectByWeightRand2(a) *HttpServer {
	rand.Seed(time.Now().UnixNano())
	sumList := make([]int.len(b.Servers))
	sum := 0
	for i := 0; i < len(b.Servers); i++ {
		sum += b.Servers[i].Weight
		sumList[i] = sum
	}
	rad := rand.Intn(sum) / / /)
	for index, value := range sumList {
		if rad < value {
			return b.Servers[index]
		}
	}
	return b.Servers[0]}Copy the code

Adjust the PROXY to use modified methods

hostUrl, _ := url.Parse(BL.SelectByWeightRand2().Host)
proxy := httputil.NewSingleHostReverseProxy(hostUrl)
proxy.ServeHTTP(writer, request)
Copy the code

The polling load is the rotation of requests from users to internal servers: starting at server 1, going all the way to server N, and then starting the cycle again

Adjust the LoadBalance Server list and add the curIndex value to calculate the current HTTPSERVER

type LoadBalance struct {
	Servers  []*HttpServer
	CurIndex int // Points to the current server, 0 by default
}
Copy the code

Add a polling algorithm

func (b *LoadBalance) RoundRobin(a) *HttpServer {
	server := b.Servers[b.CurIndex]
	b.CurIndex = (b.CurIndex + 1) % len(b.Servers)
	return server
}
Copy the code

Use a polling algorithm

hostUrl, _ := url.Parse(BL.RoundRobin().Host)
proxy := httputil.NewSingleHostReverseProxy(hostUrl)
proxy.ServeHTTP(writer, request)
Copy the code

The end result is sequential access to HTTPSERVER

If the result is not as expected during implementation, check for a browser default request such as “/favicon.icon”.

Polling weighting adds weights on the basis of polling, which is basically consistent with the idea of load weighting random

Add polling weighted algorithm (using weighted array slices to calculate HTTPSERVER)

func (b *LoadBalance) RoundRobinByWeight(a) *HttpServer {
	server := b.Servers[ServerIndices[b.CurIndex]]
	b.CurIndex = (b.CurIndex + 1) % len(ServerIndices)
	return server
}
Copy the code

Use polling weights

hostUrl, _ := url.Parse(BL.RoundRobinByWeight().Host)
proxy := httputil.NewSingleHostReverseProxy(hostUrl)
proxy.ServeHTTP(writer, request)
Copy the code

Interval algorithm is used for polling weighting

func (b *LoadBalance) RoundRobinByWeight2(a) *HttpServer {
	server := b.Servers[0]
	sum := 0
	for i := 0; i < len(b.Servers); i++ {
		sum += b.Servers[i].Weight
		if b.CurIndex < sum {
			server = b.Servers[i]
			if b.CurIndex == sum- 1&& i ! =len(b.Servers)- 1 {
				b.CurIndex++
			} else {
				b.CurIndex = (b.CurIndex + 1) % sum
			}
			break}}return server
}
Copy the code

Interval weighting algorithm is used

hostUrl, _ := url.Parse(BL.RoundRobinByWeight2().Host)
proxy := httputil.NewSingleHostReverseProxy(hostUrl)
proxy.ServeHTTP(writer, request)
Copy the code

Smooth polling weighting is used to solve the original polling weighting existence must use the high weight of HTTPSERVER pressure is too big shortcomings, smooth polling weighting as long as ensure in the total weight number, HTTPSERVER as long as can appear its weight can, without the order of the execution of the high weight of HTTPSERVER, Then execute the low-weight HTTPSERVER.

Add the original WEIGHT to the HTTPSERVER by adding the CURWERIGHT value to the HTTPSERVER, and subtract the total WEIGHT from the HTTPSERVER. Up to HTTPSERVER WEIGHT is 0.

Examples are as follows:

The weight hit Weight after hit
{s1:3, S2 :1,s3:1} (initialization weight) S1 (maximum) {s1:-2,s2:1:s3:1} s1 minus 5
{s1:-2,s2:2,s3:2} add 3 to s1 and 1 to others s2 {s1:1,:s2:-3,s3:2} s2 minus 5
{s1:4, s2: – 2, s3:3} s1 {s1:-1,:s2:-2,s3:3} s1 minus 5
{s1:2, s2:1, s3:4} s3 {s1:2,:s2:-1,s3:-1} s3 minus 5
{s1:5,s2:0,s3:0} s1 {s1:0,s2:0,s3:0} s1 minus 5

Adjust HTTPSERVER and add CURWEIGHT

type HttpServers []*HttpServer
type HttpServer struct {
	Host      string
	Weight    int
	CurWeight int // Default is 0
}
Copy the code

Add a smooth polling method

func (b *LoadBalance) RoundRobinByWeight3(a) *HttpServer {
	for _, s := range b.Servers {
		s.CurWeight = s.CurWeight + s.Weight
	}
	sort.Sort(b.Servers)
	fmt.Println(b.Servers)
	max := b.Servers[0]
	max.CurWeight = max.CurWeight - SumWeight
    
	test := ""
	for _, s := range b.Servers {
		test += fmt.Sprint(s.Host,s.CurWeight, ",")
	}
	fmt.Println(test)
    
	return max
}
Copy the code

Load balancing HTTPSERVER health check

Simple health check

  • The STATUS of the HTTP service is changed periodically

    The HEAD request in HTTP returns only the HTTP header, not the HTTP BODY, avoiding too much BODY content and small transmission.

Add the STATUS attribute to HTTPSERVER

type HttpServer struct {
	Host      string
	Weight    int
	CurWeight int    // Default is 0
	Status    string // Status, default UP, DOWN DOWN
}
Copy the code

Add a periodic check object

package util

import (
	"net/http"
	"time"
)

type HttpChecker struct {
	Servers HttpServers
}

func NewHttpChecker(servers HttpServers) *HttpChecker {
	return &HttpChecker{
		Servers: servers,
	}
}

func (h *HttpChecker) Check(timeout time.Duration) {
	client := http.Client{
		Timeout: timeout,
	}
	for _, s := range h.Servers {
		res, err := client.Head(s.Host)
		ifres ! =nil {
			res.Body.Close()
		}
		iferr ! =nil {
			s.Status = "DOWN"
			continue
		}
		if res.StatusCode >= 200 && res.StatusCode < 400 {
			s.Status = "UP"
		} else {
			s.Status = "DOWN"}}}Copy the code

The check object is called when the server is initialized

func checkServers(servers HttpServers) {
	t := time.NewTicker(time.Second * 3)
	check := NewHttpChecker(servers)
	for {
		select {
		case <-t.C:
			check.Check(time.Second * 2)
			for _, s := range servers {
				fmt.Println(s.Host, s.Status)
			}
			fmt.Println("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -")}}}go func(a) {
    checkServers(BL.Servers)
}()
Copy the code

The HTTPSERVER STATUS is marked DOWN when the server is shut DOWN and UP when the server is started

---------------------
http://localhost:9001 UP
http://localhost:9002 UP
http://localhost:9003 UP
---------------------
http://localhost:9001 UP
http://localhost:9002 DOWN
http://localhost:9003 DOWN
---------------------
http://localhost:9001 UP
http://localhost:9002 DOWN
http://localhost:9003 DOWN
---------------------
http://localhost:9001 DOWN
http://localhost:9002 DOWN
http://localhost:9003 DOWN
---------------------
http://localhost:9001 UP
http://localhost:9002 UP
http://localhost:9003 UP
---------------------

Copy the code

Implement a simple FailOver combined with health check to handle faulty HttpServers

  • Counter algorithm

Add FAILCOUNT and SUCCESSCOUNT properties to HTTPSERVER,

Add FAILMAX and RECOVERCOUNT properties to HTTPCHECKER

type HttpServer struct {
	Host      string
	Weight    int
	CurWeight int    // Default is 0
	Status    string // Status, default UP, DOWN DOWN
	FailCount int    // Number of errors 0
	SuccessCount int
}

type HttpChecker struct {
	Servers      HttpServers
	FailMax      int
	RecoverCount int
}

func NewHttpChecker(servers HttpServers) *HttpChecker {
	return &HttpChecker{
		Servers:      servers,
		FailMax:      6,
		RecoverCount: 3.// Continuously successful, this value is marked UP}}Copy the code

HTTPCHECKER adds failed and successful method handling methods

func (h *HttpChecker) Fail(server *HttpServer) {
	if server.FailCount >= h.FailMax {
		server.Status = "DOWN"
	} else {
		server.FailCount++
	}
	server.SuccessCount = 0
}

func (h *HttpChecker) Success(server *HttpServer) {
	if server.FailCount > 0 {
		server.FailCount--
		server.SuccessCount++
		if server.SuccessCount == h.RecoverCount {
			server.FailCount = 0
			server.Status = "UP"
			server.SuccessCount = 0}}else {
		server.Status = "UP"}}Copy the code

Add the FAILOVER mechanism for common polling

Notice that all servers are DOWN

// Check whether all servers are DOWN
func (b *LoadBalance) IsAllDown(a) bool {
	downCount := 0
	for _, s := range b.Servers {
		if s.Status == "DOWN" {
			downCount++
		}
	}
	if downCount == len(b.Servers) {
		return true
	}
	return false
}
// Common polling
func (b *LoadBalance) RoundRobin(a) *HttpServer {
	server := b.Servers[b.CurIndex]
	b.CurIndex = (b.CurIndex + 1) % len(b.Servers)
	// recursive query
	if server.Status == "DOWN" && !b.IsAllDown() {
		return b.RoundRobin()
	}
	return server
}
Copy the code

Add FAILWEIGHT to the HTTPSERVER. The FAILWEIGHT is specified by the current FAILWEEIGHT+=WEIGHT*(1/FailFactor). If the value is 0, the server is DOWN. If the HTTPSERVER health check succeeds, the FAILWEIGHT is set to 0

Added weighted polling FAILOVER

type HttpServer struct {
	Host         string
	Weight       int
	CurWeight    int    // Default is 0
	FailWeight   int    // Reduce the weight
	Status       string // Status, default UP, DOWN DOWN
	FailCount    int    // Number of errors 0
	SuccessCount int
}

type HttpChecker struct {
	Servers      HttpServers
	FailMax      int
	RecoverCount int
	FailFactor   float64 // The weight reduction factor is 5.0 by default
}


func (h *HttpChecker) Fail(server *HttpServer) {
	if server.FailCount >= h.FailMax {
		server.Status = "DOWN"
	} else {
		server.FailCount++
	}
	server.SuccessCount = 0

	fw := int(math.Floor(float64(server.Weight)) * (1 / h.FailFactor))
	if fw == 0 {
		fw = 1
	}
	server.FailWeight += fw
	if server.FailWeight > server.Weight {
		server.FailWeight = server.Weight
	}
}

func (h *HttpChecker) Success(server *HttpServer) {
	if server.FailCount > 0 {
		server.FailCount--
		server.SuccessCount++
		if server.SuccessCount == h.RecoverCount {
			server.FailCount = 0
			server.Status = "UP"
			server.SuccessCount = 0}}else {
		server.Status = "UP"
	}

	server.FailWeight = 0
}

// Interval algorithm
func (b *LoadBalance) RoundRobinByWeight2(a) *HttpServer {
	server := b.Servers[0]
	sum := 0
	for i := 0; i < len(b.Servers); i++ {

		// Check whether the weight is 0, which indicates that the server is unavailable
		realWeight := b.Servers[i].Weight - b.Servers[i].FailWeight
		if realWeight == 0 {
			continue
		}
		//sum += b.Servers[i].Weight
		sum += realWeight
		if b.CurIndex < sum {
			server = b.Servers[i]
			if b.CurIndex == sum- 1&& i ! =len(b.Servers)- 1 {
				b.CurIndex++
			} else {
				b.CurIndex = (b.CurIndex + 1) % sum
			}
			break
		} else {
			b.CurIndex = 0}}return server
}
Copy the code

Smooth weighting FAILOVER is basically the same as normal polling weighting, as long as the true weight is obtained in the smooth weighting method

func (b *LoadBalance) getSumWeight(a) int {
	sum := 0
	for _, s := range b.Servers {
		realWeight := s.Weight - s.FailWeight
		if realWeight > 0 {
			sum += realWeight
		}
	}
	return sum
}


func (b *LoadBalance) RoundRobinByWeight3(a) *HttpServer {

	for _, s := range b.Servers {
		s.CurWeight = s.CurWeight + s.Weight - s.FailWeight // Get the real weight
	}

	sort.Sort(b.Servers)

	fmt.Println(b.Servers)

	max := b.Servers[0]
	// max.CurWeight = max.CurWeight - SumWeight
	max.CurWeight = max.CurWeight - b.getSumWeight()

	test := ""
	for _, s := range b.Servers {
		test += fmt.Sprint(s.Host, s.CurWeight, ",")
	}
	fmt.Println(test)

	return max
}

Copy the code