代理服务器概述
代理服务器(Proxy Server)的功能是代理网络用户去取得网络信息。形象地说,它是网络信息的中转站,是个人网络和Internet服务商之间的中间代理机构,负责转发合法的网络信息,对转发进行控制和登记。
代理服务器作为连接Internet与Intranet的桥梁,在实际应用中发挥着极其重要的作用,它可用于多个目的,最基本的功能是连接,此外还包括安全性、缓存、内容过滤、访问控制管理等功能。更重要的是,代理服务器是Internet链路级网关所提供的一种重要的安全功能,它的工作主要在开放系统互联(OSI)模型的对话层
本次利用Go语言实现一个简单的HTTP代理服务器,主要分为以下几个部分完成:
实现简单的Web服务器
实现简单的代理服务器
- 手动实现
- 通过ini文件配置代理WEB对象
- 根据访问路径实现基本的代理服务器
- 实现代理服务器基本的Basic认证
- 使用go内置代理函数实现
- 实现代理服务器负载均衡
- 简单的随机负载
- IP_HASH负载
- 负载加权随机
- 轮询负载
- 轮询加权
- 平滑轮询加权
- 负载均衡HTTPSERVER健康检查
- 简易健康检查
- 实现简单FailOver
- 手动实现
实现简单的Web服务器
使用Go的Http完成两个Web服务器并分别监听在9001和9002端口
1 | type web1Handler struct{} |
实现简单的代理服务器
手动实现
通过ini文件配置代理WEB对象
创建env.ini文件用于存储所需要代理的WEB服务器列表
1
2
3
4
5
6
7
8
9[proxy]
[proxy.a]
path=/a
pass=http://localhost:9001
[proxy.b]
path=/b
pass=http://localhost:9002读取配置文件,使用第三方依赖读取ini文件
1
go get github.com/go-ini/ini
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var ProxyConfigs map[string]string
type EnvConfig *os.File
func init() {
ProxyConfigs = make(map[string]string)
EnvConfig, err := ini.Load("env.ini")
if err != nil {
fmt.Println(err)
}
section, _ := EnvConfig.GetSection("proxy")
if section != nil {
sections := section.ChildSections()
for _, s := range sections {
path, _ := s.GetKey("path")
pass, _ := s.GetKey("pass")
if path != nil && pass != nil {
ProxyConfigs[path.Value()] = pass.Value()
}
}
}
}根据访问路径实现基本的代理功能
获取PrxoyConfigs配置项列表,循环获取对应的path及Web服务器访问路径
1
2
3
4
5
6
7
8
9for k, v := range ProxyConfigs {
fmt.Println(k,v)
if matched, _ := regexp.MatchString(k, request.URL.Path); matched == true {
// 代理处理
RequestUrl(request, writer, v)
return
}
}
_, _ = writer.Write([]byte("defaut"))实现代理服务器基本的Basic认证,主要是通过将原始的http request header头和http response header头原样返回给浏览器
Basic认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全
Basic认证会在Response Header中添加
WWW-Authenticate
标头,浏览器识别到Basic后弹出对话框Realm表示Web服务器中受保护文档的安全域为WEB1服务器启用Basic认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (h web1Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
auth := request.Header.Get("Authorization")
if auth == "" {
writer.Header().Set("WWW-Authenticate", `Basic realm="您必须输入用户名和密码"`)
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("用户名或密码错误"))
}效果如下图:
代理服务器需要做的是输入头及输出头的复制。
1
2
3
4
5func CloneHead(src http.Header, dest *http.Header) {
for k, v := range src {
dest.Set(k, v[0])
}
}代理服务器代理逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20func 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)
}通过上面手动实现代理的方法,已对代理大体逻辑了解,那go是否已存在代理函数呢,答案是有的,直接利用httpUtil.NewSingleHostReverseProxy直接实现
1
2
3
4
5
6
7
8
9
10
11for 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
}
}
实现代理服务器负载均衡
负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如上面建立的Web服务器、FTP服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。
随机负载是通过随机算法从服务器列表中随机选取一台服务器进行访问。由概率论可以得知,随着客户端调用服务端的次数增多,其实际效果趋近于平均分配请求到服务端的每一台服务器,也就是达到轮询的效果。
为了查看效果方便,此处调整web服务器代码和写死web服务器访问地址到proxy中。
web服务器
1 |
|
添加LoadBalance
1 | package util |
使用GO RAND函数进行随机选择HTTPSERVER
调整PROXY
1 | func (*ProxyHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { |
IP_HASH负载是根据请求所属的客户端IP计算得到一个数值,然后把请求发往该数值对应的后端。
所以同一个客户端的请求,都会发往同一台后端,除非该后端不可用了,所以IP_HASH能够达到保持会话的效果。
在GO中可以利用CRC算法(循环冗余校验)和术语算法实现。
1 | ip:="127.0.0.1" |
添加根据IP获取HTTPSERVER方法
1 | func (b *LoadBalance) SelectByIpHash(ip string) *HttpServer { |
调整PROXY为IP_HASH代理
1 | ip := request.RemoteAddr |
最终效果,访问http://localhost:8080时,均会访问同一台服务器。
负载加权随机是在随机算法上,为HTTPSERVER添加权重,选择HTTPSERVER时,根据权重进行随机选择。
根据HTTPSERVER WERIGHT计算出权重所占数组的个数,权重较大的会占数组的个数均多,随机选择时,选中的概率较大。
调整LoadBalance
1 | // 添加WEIGHT |
调整PROXY使用随机加权
1 | hostUrl, _ := url.Parse(BL.SelectByWeightRand().Host) |
最终效果,访问http://localhost:8080时,会有1比3的效果,因为现在设置权重是5和15
缺点:需要生一个数组切片用于对应HTTPSERVER列表,如果设置权重数值过大,会引起内存问题。
改良算法 根据权重计算取值区间
如权限设置为 5:2:1,
通过5,7(5+2),8(5+2+1)
得出HTTPSERVER选择区间值应为[0,5) [5,7) [7,8)
然后根据[0,8)之内取一个随机数,随机数落在哪个区间内,就是哪台HTTPSERVER
调整WEIGHT RAND方法
1 | func (b *LoadBalance) SelectByWeightRand2() *HttpServer { |
调整PROXY使用改良方法
1 | hostUrl, _ := url.Parse(BL.SelectByWeightRand2().Host) |
轮询负载是把来自用户的请求轮流分配给内部的服务器:从服务器1开始,直到服务器N,然后重新开始循环
调整LoadBalance Server列表,添加curIndex值用于计算当前是哪个HTTPSERVER
1 | type LoadBalance struct { |
添加轮询算法
1 | func (b *LoadBalance) RoundRobin() *HttpServer { |
使用轮询算法
1 | hostUrl, _ := url.Parse(BL.RoundRobin().Host) |
最终效果,按顺序访问HTTPSERVER
如在做实现时发现,结果和预想不一致,查看是否有浏览器默认请求,如”/favicon.icon”
轮询加权 在轮询的基础上加上权重,与负载加权随机思路基本一致
添加轮询加权算法(用加权数组切片计算HTTPSERVER)
1 | func (b *LoadBalance) RoundRobinByWeight() *HttpServer { |
使用轮询加权
1 | hostUrl, _ := url.Parse(BL.RoundRobinByWeight().Host) |
使用区间算法进行轮询加权
1 | func (b *LoadBalance) RoundRobinByWeight2() *HttpServer { |
使用区间加权算法
1 | hostUrl, _ := url.Parse(BL.RoundRobinByWeight2().Host) |
平滑轮询加权 是用于解决原先轮询加权存在的必须使用完权重较高的HTTPSERVER压力过大的缺点,平滑轮询加权只要保证在总权重次数内,HTTPSERVER只要能够出现它的权重即可,无需顺序执行权重较高的HTTPSERVER,再执行权重低的HTTPSERVER。
算法是通过给HTTPSERVER添加CURWERIGHT值,初始值为HTTPSERVER WEIGHT,然后通过命中权重的HTTPSERVER减去总权重,第二次请求将CURWEIGHT加上原始权重,依次执行,直到HTTPSERVER WEIGHT均为0。
示例如下表:
权重 | 命中 | 命中后的权重 |
---|---|---|
{s1:3,s2:1,s3:1}(初始化权重) | s1(最大) | {s1:-2,s2:1:s3:1} s1减去5 |
{s1:-2,s2:2,s3:2} s1要加3,其他加1 | s2 | {s1:1,:s2:-3,s3:2} s2减去5 |
{s1:4,s2:-2,s3:3} 同上 | s1 | {s1:-1,:s2:-2,s3:3} s1减去5 |
{s1:2,s2:-1,s3:4} 同上 | s3 | {s1:2,:s2:-1,s3:-1} s3减去5 |
{s1:5,s2:0,s3:0} | s1 | {s1:0,s2:0,s3:0} s1减去5 |
调整HTTPSERVER,添加CURWEIGHT
1 | type HttpServers []*HttpServer |
添加平滑轮询方法
1 | func (b *LoadBalance) RoundRobinByWeight3() *HttpServer { |
负载均衡HTTPSERVER健康检查
简易健康检查
http服务定时检查,修改状态
使用HTTP中的HEAD请求方式进行检查,优点仅返回HTTP头,不返回HTTP BODY,避免BODY内容过多,传输量较小。
为HTTPSERVER添加STATUS属性
1 | type HttpServer struct { |
添加定时检查对象
1 | package util |
初始化服务器时调用检查对象
1 |
|
最终检查效果,当关闭服务器时,标注HTTPSERVER STATUS为DOWN,启动后标注为UP
1 | --------------------- |
实现简单FailOver结合健康检查,处理有问题的HTTPSERVER
- 计数器算法
为HTTPSERVER添加FAILCOUNT和SUCCESSCOUNT属性,
为HTTPCHECKER添加FAILMAX和RECOVERCOUNT属性
1 | type HttpServer struct { |
HTTPCHECKER添加失败和成功方法处理方法
1 | func (h *HttpChecker) Fail(server *HttpServer) { |
普通轮询添加FAILOVER机制
注意全部服务器都DOWN了
1 | // 检查所有服务器状态是否DOWN |
普通加权轮询出错降权处理,为HTTPSERVER增加降权权重值FAILWEIGHT,FAILWEIGHT是由当前FAILWEEIGHT+=WEIGHT*(1/FailFactor降权因子)得到,然后在获取服务器时真正的权重为WEIGHT-FAILWEIGHT,如果为0则代理此服务器为DOWN,在健康检查HTTPSERVER成功,则FAILWEIGHT直接设置为0
添加加权轮询FAILOVER
1 | type HttpServer struct { |
平滑加权轮询FAILOVER与普通轮询加权基本一致,只需在平滑加权方法里面得到真正的权重
1 | func (b *LoadBalance) getSumWeight() int { |