首页 > 其他分享 >go是怎么实现正反向代理的

go是怎么实现正反向代理的

时间:2024-04-24 22:22:42浏览次数:26  
标签:http 请求 正反 req 代理 代理服务器 go 客户端

Go语言是 如何实现 HTTP代理 和 反向代理

磊丰 Go语言圈 2024-04-22 08:30 广东 65人听过 Go语言圈 Go语言开发者的学习好助手,分享Go语言知识,技术技巧,学习与交流Go语言开发经验,互动才有助于技术的提升,每天5分钟,助你GO语言技术快乐成长 158篇原创内容 公众号
学习与交流:Go语言技术微信群

商务合作加微信:LetsFeng

 

现在就开始你的Go语言学习之旅吧!人生苦短,let’s Go.


图片

图片

点击获取:Goland 全家桶激活码正式版激活码

代理的核心功能可以用一句话概括:接受客户端的请求,转发到后端服务器,获得应答之后返回给客户端。

代理的功能有很多,事实上整个互联网到处都充斥着代理服务器。如果所有的 HTTP 访问都是客户端和服务器端直接进行的话,我们的网络不仅会变得缓慢,而且性能会大打折扣。

代理服务器根据不同的配置和使用,可能会有不同的功能,这些功能主要包括:

内容过滤:代理可以根据一定的规则限制某些请求的连接。比如有些公司会设置内部网络无法访问某些购物、游戏网站,或者学校的网络不让学生访问色情暴力的网站等

节省成本:代理服务器可以作为缓存使用,对于某些资源只需要第一次访问的时候去下载,以后代理直接把缓存的结果返回给客户端,节约网络带宽的开销。

提高性能:通过代理服务器的缓存(比如 CDN)和负载均衡(比如 nginx lb)功能,服务器端可以加速请求的访问,在更快的时间内返回结果)

增加安全性:公司可以在内网和外网之间通过代理进行转发,这样不仅对外隐藏了实现的细节,而且可以在代理层对爬虫、病毒性请求进行过滤,保护内部服务

所有的这些功能的实现都依赖于代理的特性,它可以在客户端和服务器端做一些事情,根据代理做的事情不同,它的角色和功能也就不同。

那么,代理具体可以做哪些事情呢?比如:

修改 HTTP 请求:url、header、body

过滤请求:根据一定的规则丢弃、过滤请求

决定转发到哪个后端(可以是静态定义的,也可以是动态决定)

保存服务器的应答,后续的请求可以直接使用保存的应答

修改应答:对应答做一些格式的转换,修改数据,甚至返回完全不一样的应答数据

重试机制,如果后端服务器暂时无法响应,隔一段时间重试

》》》》程序员福利《《《《

图片

正向代理和反向代理

代理可以分为正向代理和反向代理两种。

正向代理需要客户端来配置,一般来说我们会通过浏览器或者操作系统提供的工具或者界面来配置。

这个时候,代理对客户端不是透明的,客户端需要知道代理的地址并且手动配置。配置了代理,浏览器在发送请求的时候会对报文做特殊的修改。

反向代理对客户端是透明的,也就是说客户端一般不知道代理的存在,认为自己是直接和服务器通信。

我们大部分访问的网站就是反向代理服务器,反向代理服务器会转发到真正的服务器,一般在反向代理这一层实现负载均衡和高可用的功能。而且这里也可以看到,客户端是不会知道真正服务器端的 ip 地址和端口的,这在一定程度上起到了安全保护的作用。

代理服务器怎么知道目的服务器的地址?
在反向代理中,代理服务器要转发的服务器地址都是事先知道的(包括静态配置和动态配置)。比如 使用 nginx 来配置负载均衡 。

而对于正向代理来说,客户端可能访问的服务器地址是无法事先知道的。因为HTTP 协议活动在应用层,它无法获取网络层(IP层)信息,那么该协议要有一个地方可以拿到这个信息。

HTTP 中可能保存这个信息的地方有两个:URL 和 header。默认情况下,HTTP 请求的 status line 有三部分组成:方法、uri 和协议版本,比如:

GET /index.html HTTP/1.0
User-Agent: gohttp 1.0

如果客户端(比如浏览器)知道自己在通过正向代理进行报文传输,那么它会在 status line 加上要访问服务器的真实地址。这个时候发送的报文是:

GET http://www.marys-antiques.com/index.html HTTP/1.0
User-Agent: gohttp 1.0

代理路径
客户端不管是通过代理服务器,还是直接访问后端服务器对于最终的结果是没有区别的,也就是说大多数情况下客户端根本不关心它访问的到底是什么,只需要(准确快速地)拿到想要的信息就够了。

但是有时候,我们还是希望知道请求到底在中间经历了哪些代理,比如用来调试网络异常,或者做数据统计,而 HTTP 协议也提供了响应的功能。

虽然 RFC 2616 定义了 Via 头部字段来跟踪 HTTP 请求经过的代理路径,但在实际中用的更多的还是 X-Forwarded-For 字段, X-Forwarded-For 是 Squid 缓存代理服务软件引入的,目前已经在规范化在 RFC 7239 文档。

X-Forwarded-For 头部格式也比较简单,比如某个服务器接受到请求的对应头部可能是:

X-Forwarded-Forclientproxy1proxy2

对应的值有多个字段,每个字段代表中间的一个节点,它们之间由逗号和空格隔开,从左到右距离当前节点越来越近。

每个代理服务器会在 X-Forwarded-For 头部填上前一个节点的 ip 地址,这个地址可以通过 TCP 请求的 remote address 获取。为什么每个代理服务器不填写自己的 ip 地址呢?

有两个原因,如果由代理服务器填写自己的 ip 地址,那么代理可以很简单地伪造这个地址,而上一个节点的 remote address 是根据 TCP 连接获取的(如果不建立正确的 TCP 连接是无法进行 HTTP 通信的);另外一个原因是如果由当前节点填写 X-Forwarded-For ,那么很多情况客户端无法判断自己是否会通过代理的。

NOTE:
1、最终客户端或者服务器端接受的请求,X-Forwarded-For 是没有最邻近节点的 ip 地址的,而这个地址可以通过 remote address 获取

2、每个节点(不管是客户端、代理服务器、真实服务器)都可以随便更改 X-Forwarded-For 的值,因此这个字段只能作为参考

代理服务器实现
这个部分我们会介绍如何用 golang 来实现 HTTP 代理服务器,需要读者了解一些 HTTP 服务器端编程的知识。

正向代理
按照我们之前介绍的代理原理,我们可以编写出这样的代码:

package main
import (
    "fmt"
    "io"
    "net"
    "net/http"
    "strings"
)
type Pxy struct {}
func (p *Pxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    fmt.Printf("Received request %s %s %s\n", req.Method, req.Host, req.RemoteAddr)
    transport :=  http.DefaultTransport
    // step 1
    outReq := new(http.Request)
    *outReq = *req // this only does shallow copies of maps
    if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
        if prior, ok := outReq.Header["X-Forwarded-For"]; ok {
            clientIP = strings.Join(prior, ", ") + ", " + clientIP
        }
        outReq.Header.Set("X-Forwarded-For", clientIP)
    }
    // step 2
    res, err := transport.RoundTrip(outReq)
    if err != nil {
        rw.WriteHeader(http.StatusBadGateway)
        return
    }
    // step 3
    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 main() {
    fmt.Println("Serve on :8080")
    http.Handle("/", &Pxy{})
    http.ListenAndServe("0.0.0.0:8080", nil)
}

这段代码比较直观,只包含了最核心的代码逻辑,完全按照最上面的代理图例进行组织。一共分成几个步骤:

1、代理接收到客户端的请求,复制了原来的请求对象,并根据数据配置新请求的各种参数(添加上 X-Forward-For 头部等)

2、把新请求发送到服务器端,并接收到服务器端返回的响应

3、代理服务器对响应做一些处理,然后返回给客户端

上面的代码运行之后,会在本地的 8080 端口启动代理服务。修改浏览器的代理为 127.0.0.1::8080 再访问网站,可以验证代理正常工作,也能看到它在终端打印出所有的请求信息。

虽然这段代码非常简短,但是你可以添加更多的逻辑实现非常有用的功能。比如在请求发送之前进行过滤,根据一定的规则直接阻止某些请求的访问;或者对请求进行限流,某个客户端在一定的时间里执行的请求有最大限额;统计请求的数据进行分析等等。

这个代理目前不支持 HTTPS 协议,因为它只提供了 HTTP 请求的转发功能,并没有处理证书和认证有关的内容。

如果了解 HTTPS 协议的话,你会明白这种模式下是无法完成 HTTPS 握手的,虽然代理可以和真正的服务器建立连接(知道了对方的公钥和证书),但是代理无法代表服务器和客户端建立连接,因为代理服务器无法知道真正服务器的私钥。

反向代理
编写反向代理按照上面的思路当然没有问题,只需要在第二步的时候,根据之前的配置修改 outReq 的 URL Host 地址可以了。

不过 Golang 已经给我们提供了编写代理的框架:httputil.ReverseProxy 。我们可以用非常简短的代码来实现自己的代理,而且内部的细节问题都已经被很好地处理了。

这部分我们会实现一个简单的反向代理,它能够对请求实现负载均衡,随机地把请求发送给某些配置好的后端服务器。使用 httputil.ReverseProxy 编写反向代理最重要的就是实现自己的 Director 对象,这是 GoDoc 对它的介绍:

看代码:

package main
import (
        "log"
        "math/rand"
        "net/http"
        "net/http/httputil"
        "net/url"
)
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}
}
func main() {
        proxy := NewMultipleHostsReverseProxy([]*url.URL{
                {
                        Scheme: "http",
                        Host:   "localhost:9091",
                },
                {
                        Scheme: "http",
                        Host:   "localhost:9092",
                },
        })
        log.Fatal(http.ListenAndServe(":9090", proxy))
}

我们让代理监听在 9090 端口,在后端启动两个返回不同响应的服务器分别监听在 9091 和 9092 端口,通过 curl 访问,可以看到多次请求会返回不同的结果。

➜  curl http://127.0.0.1:9090
116064a9eb83
➜  curl http://127.0.0.1:9090
8f7ccc11718f

同样的,这段代码也只是一个 demo,存在着很多问题,比如没有错误处理机制,如果后端某个服务器挂了,代理会返回 502 错误,更好的做法是把请求转发到另外的可用服务器。当然也可以添加更多的特性让它更好用,比如动态地添加后端服务器列表;根据后端服务器的负载情况进行负载转发等等。

补充:golang 超简单实现反向代理(nginx 端口转发 Proxy)

100行你就可以做到类似nginx带自动更新的端口转发功能

总共就2个文件,一个main(总行数128行),一个配置文件

图片

main:
里面的json解析和log可以忽略

package main 
import (
    "github.com/weimingjue/json"
    utils2 "goProxy/utils"
    "goService/utils"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
    "strings"
    "sync"
    "time"
)

var (
    projectDir, _         = os.Getwd()
    fileName              = projectDir + "/domain.config"
    readFileTime    int64 = 0  //读取文件的时间
    fileChangedTime int64 = 0  //文件修改时间
    domainData      [][]string //[{***.gq,8080,http://127.0.0.1:8080/}]
    duPeiZhiSuo     sync.Mutex //读配置锁
)

// 获取反向代理域名
func getProxyUrl(reqDomain string) string {
    checkFile()

    for _, dms := range domainData {
        if strings.Index(reqDomain, dms[0]) >= 0 {
            return dms[2]
        }
    }
    return domainData[0][2]
}

//读取配置文件
//域名:端口号,未知域名默认用第一个
func checkFile() {
    nowTime := time.Now().Unix()
    if nowTime-readFileTime < 300 {
        return
    }
    //每5分钟判断文件是否修改
    domainFile, _ := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0)
    info, _ := domainFile.Stat()
    if info.ModTime().Unix() == fileChangedTime {
        return
    }
    duPeiZhiSuo.Lock()
    defer duPeiZhiSuo.Unlock()
    domainFile, _ = os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0) //加锁再来一遍,防止重入
    info, _ = domainFile.Stat()
    changedTime := info.ModTime().Unix()
    if changedTime == fileChangedTime {
        return
    }

    //文件改变

    //重置数据
    readFileTime = nowTime
    fileChangedTime = changedTime
    domainData = [][]string{}

    bytes, _ := ioutil.ReadFile(fileName)
    split := strings.Split(string(bytes), "\n")

    for _, domainInfo := range split {
        dLen := len(domainInfo)
        if dLen < 8 || dLen > 20 { //忽略错误信息
            continue
        }
        domainItems := strings.Split(domainInfo, ":")
        if len(domainItems) != 2 || len(domainItems[0]) < 3 || len(domainItems[1]) < 2 {
            continue
        }
        if utils.EndWidth(domainItems[1], "/") {
            domainItems = append(domainItems, "http://127.0.0.1:"+domainItems[1])
        } else {
            domainItems = append(domainItems, "http://127.0.0.1:"+domainItems[1]+"/")
        }
        domainData = append(domainData, domainItems)
    }

    domainSt, _ := json.Marshal(domainData)
    utils2.MyLogProxyI("配置已修改:" + string(domainSt))
}

//获取主机名
func getHost(req *http.Request) string {
    if req.Host != "" {
        if hostPart, _, err := net.SplitHostPort(req.Host); err == nil {
            return hostPart
        }
        return req.Host
    }
    return "localhost"
}

func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
    host := getHost(req)
    proxyUrl := getProxyUrl(host)
    url2, _ := url.Parse(proxyUrl)
    utils2.MyLogProxyI("请求域名:" + host + ",转到:" + proxyUrl)

    // create the reverse proxy
    proxy := httputil.NewSingleHostReverseProxy(url2)

    // Update the headers to allow for SSL redirection
    req.URL.Host = url2.Host
    req.URL.Scheme = url2.Scheme
    req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
    req.Host = url2.Host

    // Note that ServeHttp is non blocking and uses a go routine under the hood
    proxy.ServeHTTP(res, req)
}

func main() {
    http.HandleFunc("/", handleRequestAndRedirect)
    if err := http.ListenAndServe(":80", nil); err != nil {
        utils.MyLogE("Proxy监听80端口错误:" + err.Error())
        panic(err)
    }
}

domain.config:
***为自己的域名,":"后面是需要转发的端口,不用写http://,任何地方都不能有空格

wang.gq:8080
***.aa:8081/

代码写的是相对目录请到当前目录执行"go run main.go"

 

文章首发:https://www.jb51.net/article/211491.htm

 

 

 

 

 

 

 

 

 

更多相关Go语言的技术文章或视频教程,请关注本公众号获取并查看,感谢你的支持与信任!

 

学Go语言哪些事儿279 学Go语言哪些事儿 · 目录 上一篇Go语言中 既然不存在三元运算符,是何道理啊?下一篇Go语言 实现dll恶意劫持转发 阅读 887 ​ 喜欢此内容的人还喜欢   分享 Golang 健壮高效的一种重试模式     我看过的号 Go Official Blog 不看的原因   为什么 VMware 会给我多创建了两个网络呢?     Raymond运维 不看的原因   go: x509.Certificate中的IPAddresses对服务器证书有效吗?什么原理?     我常看的号 运维开发王义杰 不看的原因   写留言       Go语言圈            

人划线

标签:http,请求,正反,req,代理,代理服务器,go,客户端
From: https://www.cnblogs.com/cheyunhua/p/18156509

相关文章

  • Go汇编
    Go语言编译后的汇编指令Go1.20以及之后的版本为了减小Go发行版的大小,默认不再安装标准库的预编译包,需要执行以下命令自行安装。GODEBUG=installgoroot=allgoinstallstd否则若依赖到标准库,会抛出类似以下异常:couldnotimportfmt(openfmt.a:nosuchfileordirector......
  • godot shader 升级
    WORLD_MATRIXrenametoMODEL_MATRIXWORLD_MATRIX重命名为MODEL_MATRIXWORLD_NORMAL_MATRIXrenametoMODEL_NORMAL_MATRIXWORLD_NORMAL_MATRIX重命名为MODEL_NORMAL_MATRIXCAMERA_MATRIXrenametoINV_VIEW_MATRIXCAMERA_MATRIX重命名为INV_VIEW_MATRIXINV_CAMERA_MATRIXrena......
  • Golang - 并发同步更新全局切片失败的原因以及解决方案
    当多个协程同时访问和修改同一个共享资源(如切片)时,如果没有适当的同步机制,可能会导致数据竞争和不一致的结果。packagemainimport("fmt""sync")funcprocessChunk(chunk[]int64,wg*sync.WaitGroup,failedList[]int64){deferwg.Done()fmt.Print......
  • golang 实现文件下载
    golang实现文件下载packagemainimport("fmt""html/template""io""io/fs""mime""net/http""os""path/filepath""regexp"&qu......
  • 6.prometheus监控--监控redis/rabbitmq/mongodb
    1.监控redis1.1安装方式1.1.1二进制源码安装方式参考nginx二进制安装方法redis_exporter下载地址:https://github.com/oliver006/redis_exporter/releases系统服务:cat>/etc/systemd/system/redis_exporter.service<<"EOF"[Unit]Description=PrometheusRedisExport......
  • godot4.2 vscode配置
    launch.json文件{//使用IntelliSense了解相关属性。//悬停以查看现有属性的描述。//欲了解更多信息,请访问:https://go.microsoft.com/fwlink/?linkid=830387"version":"0.2.0","configurations":[{"name":......
  • 如何在 VSCode 中配置和编写 LINGO
    目录如何在VSCode中配置和编写LINGO安装VSCode扩展LINGO脚本文件与runlingo命令LINGO命令行交互和脚本文件配置VisualStdioCode的设置配置LTF文件的代码高亮关于文件兼容的一些小问题如何在VSCode中配置和编写LINGOLINGO是用来解决优化问题的一个特别好用的......
  • MIGO BAPI BAPI_GOODSMVT_CREATE 各种类型使用汇总
    ***********GOODSMVT_CODE取值含义********01MB01*02MB31*03MB1A"发*04MB1B"转储*05MB1C"其它收货*06MB11*07MB04经常会遇到一些自定义的移动类型,但是并不知道对应的goodsmvt_code是多少。可以用如下方法进行查找首先去T158B中根据移动类......
  • 【介绍一个组件】go: Copy-On-Write map,对读极多和写极少的场景做优化
    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!cnblogs博客zhihuGithub公众号:一本正经的瞎扯代码请看:https://github.com/ahfuzhang/cowmap有这样一种场景:数据量不多的map,在使用中读极多写极少。为了在这种场景下做极致的优化,我实现了copy-on-write的map:其......
  • golang通过sock进行通信
    只是demo,生产环境要防止粘包。可以作为多进程之间通讯。。。。serverpackagemainimport( "fmt" "net" "os" "os/signal" "sync" "syscall")//客户端连接结构typeClientstruct{ Conn*net.UnixConn}varclients=make(map......