什么是网络代理
- 用户通过代理请求信息
- 请求通过网络代理完成转发达到目标服务器
- 目标服务器响应后在通过网络代理回传给用户
网络代理和网络转发区别:
网络代理:用户不直接连接服务器,网络代理去连接,获取数据后返回给用户
网络转发:是路由器对报文的转发操作,中间可能对数据包修改
代理类型
正向代理
原理: 正向代理是从客户端的角度出发,服务于特定用户(比如说一个局域网内的客户)无法访问的服务资源,可以隐藏用户真实IP,比如:浏览器web代理,VPN;
实现一个web浏览器代理:
- 代理接收客户端请求,复制原请求对象,并根据数据配置新请求各种参数
- 把新请求发送到真实的服务端,并接收到服务端返回
- 代理服务器对相应做一些处理,然后返回给客户端
正向代理的代码:
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)
//panic("implement me")
transport := http.DefaultTransport //定义一个默认的链接池
// 1.浅拷贝对象,然后就在新增属性数据
outReq := new(http.Request)
*outReq = *req //拷贝一份新的,下文发生改变
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)
}
//2.请求下游
resp, err := transport.RoundTrip(outReq)
// resp 里面的内容进行输出
if err != nil {
rw.WriteHeader(http.StatusBadGateway) //状态不良网关
return
}
//3.把下游的请求返回给上游
for key, value := range resp.Header { //头的写入
for _, v := range value {
rw.Header().Add(key, v)
}
}
rw.WriteHeader(resp.StatusCode)
io.Copy(rw, resp.Body) // 拷贝body的内容
resp.Body.Close()
}
//web 的正向代理
func main() {
fmt.Println("server on :8080")
http.Handle("/", &Pxy{})
http.ListenAndServe("127.0.0.1:800", nil)
}
反向代理
原理: 是一种服务端的代理技术,帮助服务器做负载均衡,缓存,提供安全校验等,可以隐藏服务器真实IP,比如:LVS技术,nginx proxy_pass等
如何实现一个反向代理:
- 代理接收客户端请求,更改请求结构体信息
- 通过一定的均衡负载算法获取下游的服务地址
- 把请求都送到下游服务器,并获取返回的内容
- 对返回的内容做一些处理,然后返回给客户端
反向代理:
- 先构建好下游服务器
//模拟反向代理的真实服务器
type RealServer struct {
Addr string
}
func (r *RealServer) Run() {
log.Println("start httpserver at " + r.Addr)
mux := http.NewServeMux() //多路服用
mux.HandleFunc("/", r.HelloHandler) //启动路由将方法设置好
mux.HandleFunc("/base/error", r.ErrorHandler)
server := &http.Server{
Addr: r.Addr,
WriteTimeout: 3 * time.Second,
Handler: mux,
}
go func() {
log.Fatal(server.ListenAndServe())
}()
}
// HelloHandler 方法
func (r *RealServer) HelloHandler(w http.ResponseWriter, req *http.Request) {
//127.0.0.1:8080/abc?sdsdsa=11
//r.Addr = 127.0.0.1:8080
// req.URL.Path= abc
upath := fmt.Sprintf("http://%s%s\n", r.Addr, req.URL.Path)
fmt.Println("upPath:", upath)
io.WriteString(w, upath)
}
func (r *RealServer) ErrorHandler(w http.ResponseWriter, req *http.Request) {
upath := "error handler"
w.WriteHeader(500)
io.WriteString(w, upath)
}
// 模拟反向代理的真实服务器
func main() {
//创建两个真实的realServer
rs1 := &RealServer{Addr: "127.0.0.1:2003"}
rs1.Run()
rs2 := &RealServer{Addr: "127.0.0.1:2004"}
rs2.Run()
// 监听关闭信号
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
}
- 再看真是代理服务器的设置
//reverse_proxy 反向代理的实现
func main() {
http.HandleFunc("/", handler)
log.Println("start serving on port" + port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
log.Fatal(err)
}
}
var (
proxy_addr = "http://127.0.0.1:2003"
port = "2002"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 1. 解析代理地址,并更改请求体的协议和主机
proxy, err := url.Parse(proxy_addr)
r.URL.Scheme = proxy.Scheme //http
r.URL.Host = proxy.Host // 127.0.0.1:2003
//2. 请求下游
transport := http.DefaultTransport
resp, err := transport.RoundTrip(r)
if err != nil {
log.Fatal(err)
return
}
//3. 把下游的请求的内容返回给上游
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
defer resp.Body.Close()
bufio.NewReader(resp.Body).WriteTo(w) // 将resp.Body的内容输出到http.ResponseWriter,
}
上面的简版http代理,不具备以下功能:
- 错误回调及错误日志处理
- 更改代理返回内容
- 负载均衡
- url重写
- 限流,熔断,降级
- 数据统计
- 权限认证
http代理,重点详解
用官方的ReverseProxy 实现一个代理http代理
- ReverseProxy 功能点
- 更改内容支持
- 错误信息回调
- 支持自定义的负载均衡
- url重写功能
- 连接池功能
- 支持websocket服务
- 支持https代理
- ReverseProxy 示例
- ReverseProxy 源码分析
拓展 ReverseProxy 功能
- 4种负载轮询类型实现及接口封装
- 拓展中间件的支持:限流,熔断实现,权限,数据统计
用ReverseProxy 实现一个HTTP代理
官方自带的 ReverseProxy 结构体
地址:https://golang.google.cn/pkg/net/http/httputil/
type ReverseProxy struct {
// 控制器必须是一个函数,函数内部可以对请求进行修改
Director func(*http.Request)
// 连接池,如果为nil,则使用 http.DefaultTransport。
Transport http.RoundTripper
ErrorLog *log.Logger
//返回内容 修改response的函数
ModifyResponse func(*http.Response) error
// 错误处理回调函数,如果为零,则默认记录提供的错误并返回 502
ErrorHandler func(http.ResponseWriter, *http.Request, error)
}
先实例化一个*ReverseProxy 然后实现ServerHTTP方法就行
var addr = "127.0.0.1:2002" // 请求的转发的地址
//用官方的reverseProxy来代理http
func main() {
rs1 := "http://127.0.0.1:2003/base" //转发到rs1
url1, err := url.Parse(rs1)
fmt.Println("url1:", url1)
if err != nil {
log.Println(err)
}
proxy := httputil.NewSingleHostReverseProxy(url1)
log.Println("Starting httpserver at " + addr)
log.Fatal(http.ListenAndServe(addr, proxy))
}
先启动模拟反向代理的真实服务器
在启动reverseProxy反向代理**
reverseProxy更改内容实现(modifyFunc):
代码:
//reverseProxy 更改内容的支持 主要实在reverseProxy结构体中的modifyFunc模块里面修改内容
var addr = "127.0.0.1:2002" // 请求的转发的地址
func main() {
//127.0.0.1:2002/xxx
//127.0.0.1:2003/base/xxx
rs1 := "http://127.0.0.1:2003/base"
url1, err1 := url.Parse(rs1)
if err1 != nil {
log.Println(err1)
}
proxy := NewSingleHostReverseProxy(url1)
log.Println("Starting httpserver at " + addr)
log.Fatal(http.ListenAndServe(addr, proxy))
}
func NewSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy {
//http://127.0.0.1:2002/dir?name=123
//RayQuery: name=123
//Scheme: http
//Host: 127.0.0.1:2002
targetQuery := target.RawQuery
director := func(req *http.Request) {
//url_rewrite
//127.0.0.1:2002/dir/abc ==> 127.0.0.1:2003/base/abc ??
//127.0.0.1:2002/dir/abc ==> 127.0.0.1:2002/abc
//127.0.0.1:2002/abc ==> 127.0.0.1:2003/base/abc
re, _ := regexp.Compile("^/dir(.*)")
req.URL.Path = re.ReplaceAllString(req.URL.Path, "$1")
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
//target.Path : /base
//req.URL.Path : /dir
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
}
//更改内容
modifyFunc := func(res *http.Response) error {
if res.StatusCode != 200 {
//return errors.New("error statusCode")
// 获取内容
oldPayload, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Println(err)
return nil
}
// 追加内容
newPayLoad := []byte("hello" + string(oldPayload)) //在原来的内容基础的前面添加“hello”
res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayLoad)) //将新的内容(newPayLoad)回写到resp.Body里面
res.ContentLength = int64(len(newPayLoad)) // 将新的内容长度重新赋值
res.Header.Set("Content_Length", strconv.FormatInt(int64(len(newPayLoad)), 10)) //提示客户端去读这个内容
// 上面或者写成: resp.Header.Set("Content_Length",fmt.Sprint(newPayLoad))
}
return nil
}
errorHandler := func(res http.ResponseWriter, req *http.Request, err error) {
res.Write([]byte(err.Error()))
}
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc, ErrorHandler: errorHandler}
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
Connection
- 标记请求发起方与第一代理的状态
HTTP请求发送之前,需要先建立TCP连接,只有TCP连接建立,才可以发送HTTP请求。
当HTTP请求发送并响应完成,有两种情况,
在浏览器中,ConnectionID 表示的就是TCP建立连接的ID
Connection 头(header) 决定当前的事务完成后,是否会关闭网络连接。如果该值是“keep-alive”,网络连接就是持久的,不会关闭,使得对同一个服务器的请求可以继续在该连接上完成。
特定于连接的标头字段(例如Connection)不得与HTTP / 2一起使用
除去标准的逐段传输(hop-by-hop)头(Keep-Alive
, Transfer-Encoding
, TE
, Connection
, Trailer
, Upgrade (en-US), Proxy-Authorization
and Proxy-Authenticate
),任何逐段传输头都需要在 Connection 头中列出,这样才能让第一个代理知道必须处理它们且不转发这些头。标准的逐段传输头也可以列出(常见的例子是 Keep-Alive
,但这不是必须的)。
-
决定当前事务完成后,是否会关闭网络
Conection:keep-alive 不关闭网络,长连接 Conection:close 关闭网络 Conection:Upgrade 协议升级
“TE” “Trailer”
- TE 是request_header,表示希望传输的编码类型
如:TE:trailers,deflate;q= 0.5 表示,期望在采用分块传输编码响应中接收挂载字段,zlib编码,0.5优先级排序
- Trailer 是response header ,允许发送方在消息后面添加额外的元信息,超时时间等。
如: Trailer :Expires (超时时间的标记),表示,EXpires 将出现在分块信息的结尾
第一代理除去标准的逐段传输头(hop-by-hop)
- 逐段传输头都需要在 Connection 头中列出
- 第一个代理知道必须处理(如协议升级)它们且不转发它
- 逐段传输头:Keep_alive ,Transfer_encoding,TE,Connection,Trailer,Upgrade,Proxy-Authorization,proxy-Authenticate(验证)
ReverseProxy 补充知识-
-特殊的StatusCode
- 100 表示目前一切正常,客户端可以继续请求
- 101 服务端发送客户端升级协议的请求
100-continue 状态:
- 客户端要post 的数据大于1024字节的时候
- 客户端不会直接就发起post请求,二十分两步
- 发送一个请求,包含一个(期望)Expect:100-continue,询问Server是否愿意接收数据
- 接收到server返回的100-continue应答以后,返回100状态,才把数据post给server
特殊的Header头
- “X-Forwarded-For” :标记客户端的ip,每一个反向代理的服务器的ip做个列表的呈现
- “X-Real-IP”:实际请求的ip标记
- “Connection” :请求的时候,这个连接的状态,(关闭,长连接,升级的协议)连接
- “TE” : 希望传输的类型是什么,请求的header
- “Trailer”: 返回的header头,允许发送方在后面添加的一些元信息,超时时间等等
X-Forwarded-For
记录代理服务器的地址,每经过一个代理,该字段会加上一个记录,由于是记录来源地址,所以该字段不会保存最后一个真实服务器的地址
- 存储客户端 ip 和反向代理 IP 列表,以逗号+空格分隔
- 记录最后直连实际服务器之前的整个 代理过程
- 可能会被伪造 ip,但是直连实际服务器这段不会被伪造
图示:
可以看到,第一层代理,其存储了客户端的 IP,第二层 代理 追加了 第一层的代理的IP(proxy_ip1),但并没有保存自己的proxy_ip2。
X-Real-IP
也是用来记录服务器的地址,但是和上面的不同,它不把记录添加到结尾,而是直接替换。
-
请求实际服务器的 IP,不会被伪造,
-
每过一层 代理都会被覆盖掉,只需第一层代理设置转发
-
IP可以被伪造,但如果存在一级以上的 代理,它就不会收到影响,因为每经过一次代理,它就会被覆盖
可以通过:request.RemoteAddr 拿到IP地址
第一层 代理已经拿到了真实的 ip,并在 header 中设置了 X-Real-IP,因此第二层 代理不需要再去设置 X-Real-IP,只需要做转发就可以
remote_addr
表示上一个客户端连接的地址,不存在 dai li 就表示客户端的地址,存在 dai li 就表示最后一个 dai li 服务器的地址
remote_addr
无法伪造,remote_addr
字段不是通过请求头来决定的,而是服务端在建立tcp连接时获取的的客户端地址
可以这样理解,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求
Go 中可以通过 req.RemoteAddr
代码获取请求地址的 ip,
End-to-end 和 Hop-by-hop
End-to-end "端到端"头部
- 此类头部字段会转发给 请求/响应 的最终接收目标。
- 必须保存在由缓存生成的响应头部。
- 必须被转发。
Hop-by-hop 逐跳首部
- 此类头部字段只对单次转发有效。会因为转发给缓存dai li 服务器而失效。
- HTTP 1.1 版本之后,如果要使用 Hop-by-hop 头部字段则需要提供 Connection 字段。
除了一下 8 个字段为逐跳字段,其余均为端到端字段。
- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authenrization
- Trailer
- TE
- Tranfer-Encoding
- Upgrade
其他 header 头
- Connection——标记请求连接是长连接还是短连接,或者说已关闭;http1.1 后默认是从长连接,该字段的值为:keep-alive
- TE——HTTP 请求头字段,传输编码的优先级,固定值:TE: trailers,deflate
- Trailer——HTTP 响应头字段,用户说明传输中分块编码的编码信息,固定值 Trailer: Max-Forwards
https代理
websocket代理
tcp代理
到结尾,而是直接替换。
-
请求实际服务器的 IP,不会被伪造,
-
每过一层 代理都会被覆盖掉,只需第一层代理设置转发
-
IP可以被伪造,但如果存在一级以上的 代理,它就不会收到影响,因为每经过一次代理,它就会被覆盖
[外链图片转存中…(img-bMwhEVog-1648220235100)]
可以通过:request.RemoteAddr 拿到IP地址
第一层 代理已经拿到了真实的 ip,并在 header 中设置了 X-Real-IP,因此第二层 代理不需要再去设置 X-Real-IP,只需要做转发就可以
remote_addr
表示上一个客户端连接的地址,不存在 dai li 就表示客户端的地址,存在 dai li 就表示最后一个 dai li 服务器的地址
remote_addr
无法伪造,remote_addr
字段不是通过请求头来决定的,而是服务端在建立tcp连接时获取的的客户端地址
可以这样理解,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求
Go 中可以通过 req.RemoteAddr
代码获取请求地址的 ip,
End-to-end 和 Hop-by-hop
End-to-end "端到端"头部
- 此类头部字段会转发给 请求/响应 的最终接收目标。
- 必须保存在由缓存生成的响应头部。
- 必须被转发。
Hop-by-hop 逐跳首部
- 此类头部字段只对单次转发有效。会因为转发给缓存dai li 服务器而失效。
- HTTP 1.1 版本之后,如果要使用 Hop-by-hop 头部字段则需要提供 Connection 字段。
除了一下 8 个字段为逐跳字段,其余均为端到端字段。
- Connection
- Keep-Alive
- Proxy-Authenticate
- Proxy-Authenrization
- Trailer
- TE
- Tranfer-Encoding
- Upgrade
其他 header 头
- Connection——标记请求连接是长连接还是短连接,或者说已关闭;http1.1 后默认是从长连接,该字段的值为:keep-alive
- TE——HTTP 请求头字段,传输编码的优先级,固定值:TE: trailers,deflate
- Trailer——HTTP 响应头字段,用户说明传输中分块编码的编码信息,固定值 Trailer: Max-Forwards