go 进阶 请求代理相关: 二. ReverseProxy 基础讲解

2023-11-17

一. ReverseProxy 基础

  1. 在前面我们基于原生HTTP实现正反向代理,可能存在以下问题
  1. 没有错误回调及错误日志等处理
  2. 无法更改代理后返回的内容
  3. 没有负载均衡
  4. 没有url重写
  5. 没有熔断限流,降级,数据统计等功能
  1. 解决以上问题,golang提供了ReverseProxy,在"net/http/httputil/"包下

ReverseProxy 中提供了哪些功能

  1. 提供了4种负载均衡的实现及接口封装,并且支持自定义负载均衡

随机
轮询
权重轮询
hash

  1. 通过中间件提供了: 限流, 熔断,降级, 权限,数据统计等功能
  2. 允许更改启动内容
  3. 可以设置错误信息回调
  4. 支持url重写
  5. 支持连接池功能
  6. 支持webSocket
  7. 支持https代理

ReverseProxy 结构详解

type ReverseProxy struct {
   //控制器必须是一个函数,通过该函数内部可以对请求进行修改,比如请求的路径,请求的参数
   Director func(*http.Request)

   //连接池,如果为nil,则使用http.DefaultTransport
   Transport http.RoundTripper

   //刷新到客户端的刷新间隔
   FlushInterval time.Duration

   //错误记录器
   ErrorLog *log.Logger

   //定义一个缓冲池,在复制http响应的时候使用,用以提高请求效率
   BufferPool BufferPool

   //修改response返回内容的函数
   //将函数格式定义为以下格式,就能对返回内容进行修改
   ModifyResponse func(*http.Response) error

   //错误回调函数,如果为nil,则默认为记录提供的错误并返回502状态错误网关响应,
   //当发生异常时(包括整个流程上某一部分发生异常)可以通过该函数进行处理
   ErrorHandler func(http.ResponseWriter, *http.Request, error)
}

ReverseProxy实现代理的简单示例

  1. 对下方示例进行解释:
  1. ReverseProxy在"net/http/httputil/"包下
  2. 通过httputil下的NewSingleHostReverseProxy()方法可以直接创建一个ReverseProxy
  3. ReverseProxy实现了Handler接口,所以可以直接当成路由处理器来使用
  4. 下方代码中,访问当前服务8080端口时,会通过ReverseProxy代理到rs1 指向的地址
import (
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
)

func main() {
	//1.真实需要访问的地址
	rs1 := "http://127.0.0.1:9999/base"
	//通过url.Parse()解析地址
	url1, err1 := url.Parse(rs1)
	if err1 != nil {
		log.Println(err1)
	}
	//2.获取到ReverseProxy
	proxy := httputil.NewSingleHostReverseProxy(url1)
	//3.ReverseProxy实现了Handler,可以直接当成处理器路由来使用
	//通过ReverseProxy实现http代理,当访问当前服务8080端口时,
	//会被ReverseProxy代理到rs1 
	log.Fatal(http.ListenAndServe(":8080", proxy))
}
  1. 上方示例的注意点: 假设访问当前服务"127.0.0.1:8080/xxx"在经过ReverseProxy代理后,实际会访问到"127.0.0.1:2003/base/xxx", 内部提供了一定的重写规则

1. NewSingleHostReverseProxy()函数源码解释

  1. 用来创建ReverseProxy的NewSingleHostReverseProxy()函数源码解释
//target url.URL:代理的目标服务,假设为"http://127.0.0.1:2002/base?name=123"
func NewSingleHostReverseProxy(target *url.URL) *ReverseProxy {
   //1.获取路径参数,根据上面假设的路径,当前targetQuery 就是"name=123"
   targetQuery := target.RawQuery
   
   //2.创建ReverseProxy需要的Director方法
   //Director:用来改写请求路径,请求参数的函数
   director := func(req *http.Request) {
   	  //2.1设置协议Scheme: http
      req.URL.Scheme = target.Scheme 
      //2.2设置主机Host: 127.0.0.1:2002
      req.URL.Host = target.Host 
       //2.3设置path
       //设置规则:比如当前服务到此处的路径为"http://ip:端口号/dir"
       //上面要代理到target指向的path为"/base"
       //拼接后位"/base/dir" 也就是target.path后要拼接当前服务的path
       //joinURLPath()方法中会有一些合并校验等逻辑
      req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
      //2.4 url参数的设置
      if targetQuery == "" || req.URL.RawQuery == "" {
         req.URL.RawQuery = targetQuery + req.URL.RawQuery
      } else {
         req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
      }
      //2.4设置请求头
      if _, ok := req.Header["User-Agent"]; !ok {
         // explicitly disable User-Agent so it's not set to default value
         req.Header.Set("User-Agent", "")
      }
   }
   //3.创建ReverseProxy设置Director并返回
   return &ReverseProxy{Director: director}
}

2. 模拟NewSingleHostReverseProxy()自定义创建ReverseProxy

  1. 在NewSingleHostReverseProxy源码中我们发现主要就是创建了用来改写请求路径,请求参数的Director(), 我们可以模拟NewSingleHostReverseProxy()实现一个自己的ReverseProxy创建方法,
  2. 并且在创建ReverseProxy也可以同时将用来修改response返回内容的ModifyResponse 函数与异常回调函数errorHandler定义出来
  3. 模拟NewSingleHostReverseProxy实现自定义创建ReverseProxy
import (
	"bytes"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"regexp"
	"strings"
)

//1.模拟NewSingleHostReverseProxy创建ReverseProxy
//target: 目标服务
func NewSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy {
	//1.获取path上的请求参数
	targetQuery := target.RawQuery
	
	//2.封装用来修改请求路径,请求参数的Director函数
	director := func(req *http.Request) {
		//2.1请求参数
		re, _ := regexp.Compile("^/dir(.*)");
		req.URL.Path = re.ReplaceAllString(req.URL.Path, "$1")
		//2.2设置协议
		req.URL.Scheme = target.Scheme
		//2.3设置主机地址
		req.URL.Host = target.Host
		
		//2.4 设置path
		req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
		//2.5 设置path参数
		if targetQuery == "" || req.URL.RawQuery == "" {
			req.URL.RawQuery = targetQuery + req.URL.RawQuery
		} else {
			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
		}
		//2.5设置请求头
		if _, ok := req.Header["User-Agent"]; !ok {
			req.Header.Set("User-Agent", "")
		}
		//读取body
		body, err := ioutil.ReadAll(req.Body)
		if err != nil {
			log.Println("Failed to read request body:", err)
			return
		}
		fmt.Println(string(body))
		defer req.Body.Close()
		data := map[string]int{
			"apple":  2,
			"banana": 3,
			"cherry": 4,
		}
		jsonBytes, err := json.Marshal(data)
		if err != nil {
			fmt.Println("Failed to serialize map to JSON:", err)
			return
		}
		//req.Body = ioutil.NopCloser(bytes.NewReader(body))
		req.Body = ioutil.NopCloser(bytes.NewReader(jsonBytes))
		//注意如果修改Body内容,要同步修改req.ContentLength长度,否则会报错
		req.ContentLength = int64(len(jsonBytes))
		
		//添加请求头
		req.Header.Set("token", "ssss")
	}
	
	//3.封装可用用来改写响应的modifyFunc 函数
	modifyFunc := func(res *http.Response) error {
		if res.StatusCode != 200 {
			//3.1此处判断如果响应的http状态码为异常时,封装异常返回
			return errors.New("error statusCode")
		}
		//3.2读取下游服务响应的body
		oldPayload, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return err
		}
		//3.3封装新的响应
		newPayLoad := []byte("hello " + string(oldPayload))
		//3.4将数据再次填充到resp中(ioutil.NopCloser()该函数直接将byte数据转换为Body中的read)
		res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayLoad))
		//3.5重置响应数据长度
		res.ContentLength = int64(len(newPayLoad))
		res.Header.Set("Content-Length", fmt.Sprint(len(newPayLoad)))
		return nil
	}
	
	//4.设置异常回调,在上面几个步骤如果发送异常,返回的err不为nin,
	//会执行该函数,执行指定业务逻辑
	errorHandler := func(res http.ResponseWriter, req *http.Request, err error) {
		res.Write([]byte(err.Error()))
	}
	
	//5.创建ReverseProxy返回
	return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc, ErrorHandler: errorHandler}
}

//2.启动服务
func main() {
	//2.1代理的目标服务地址,转换为url类型
	rs1 := "http://127.0.0.1:2003/base"
	url1, err1 := url.Parse(rs1)
	if err1 != nil {
		log.Println(err1)
	}
	//2.2通过自定义的NewSingleHostReverseProxy创建ReverseProxy
	proxy := NewSingleHostReverseProxy(url1)
	//2.3ReverseProxy实现了ServeHTTP()可以作为Handle使用
	log.Fatal(http.ListenAndServe(":8080", proxy))
}

//复制的源码中的
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
}

3. 去除标准逐段传输头

  1. 在代理时,代理服务器向下发起代理调用时,有几个请求头(如协议升级等)不需要向下转发,此时就需要去除这几个请求头
    在这里插入图片描述

4. ReverseProxy中一些特殊的StatusCode状态

  1. 100: 表示正常,客户端可以继续请求
  2. 100-continue状态:
  1. 客户端发送post请求数据大于1024字节时,不会直接发送请求,而是分两步
  2. 先发送询问请求,包含一个"Expect:100-continue",获取到Server端是否愿意接受数据
  3. 接受到Server端返回的100-continue应答后,返回100状态,才会把数据post给Server
  1. 101: 服务端发送给客户端升级协议的请求

5. 特殊Header头解释

  1. “X-Forwarded-For”: 反向代理时,经过的每一个反向代理服务器的ip列表(可被伪造)
  2. “X-Real-IP”: 实际请求的ip(不会被伪造)
  3. “Connection”:
  4. “TE”: 希望的传输类型
  5. “Trailer”: 响应头

二. ReverseProxy 源码相关

1. 了解ReverseProxy 下实现的 ServeHTTP()

func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	//验证结构体里面有没有设置过ReverseProxy的连接池,没有则使用默认连接池
    transport := p.Transport
	if transport == nil {
		transport = http.DefaultTransport 
	}

    //1、验证是否请求终止
     //上下文取得信息,向下转型为CloseNotifier
     //(http.CloseNotifier是一个接口,只有一个方法CloseNotify() <-chan bool,作用是检测连接是否断开)
     //取出里面通知的一个channel,即cn.CloseNotify(),紧接着开启一个协程,一直监听这个channel是否有请求终止的消息,如果有,便执行cancel()方法
	ctx := req.Context()
	if cn, ok := rw.(http.CloseNotifier); ok {
		var cancel context.CancelFunc
		ctx, cancel = context.WithCancel(ctx)
		defer cancel()
		notifyChan := cn.CloseNotify()
		go func() {
			select {
			case <-notifyChan:
				cancel()
			case <-ctx.Done():
			}
		}()
	}

    //2、设置context信息
    //通过上游发送过来的req,重新拷贝新建一个outreq对外请求的request,可以理解为往下文请求的一个request
	outreq := req.Clone(ctx)
     //对outreq的信息做特殊处理
	if req.ContentLength == 0 {
		outreq.Body = nil // Issue 16036: nil Body for http.Transport retries
	}
	if outreq.Body != nil {
		defer outreq.Body.Close()
	}
    
    //3、深拷贝Header
	if outreq.Header == nil {
		outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
	}
	
    //4、修改request,也就是之前控制器Director那里,地址和请求信息的修改拼接
	p.Director(outreq)
    //outreq.Close = false的意思是表示outreq请求到下游的链接是可以被复用的
	outreq.Close = false

    //5、Upgrade头的特殊处理
     //upgradeType(outreq.Header)取出upgrade的类型并判断是否存在
	reqUpType := upgradeType(outreq.Header)
	if !ascii.IsPrint(reqUpType) {
		p.getErrorHandler()(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
		return
	}
     //删除connection的head头信息
	removeConnectionHeaders(outreq.Header)

    //逐段消息头:客户端和第一代理之间的消息头,与是否往下传递head消息头是没有关联的,往下传递的信息中不应该包含这些逐段消息头
	//删除后端的逐段消息头
	for _, h := range hopHeaders {
		outreq.Header.Del(h)
	}

	//这两个特殊消息头跳过,不进行删除
	if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
		outreq.Header.Set("Te", "trailers")
	}

	if reqUpType != "" {
		outreq.Header.Set("Connection", "Upgrade")
		outreq.Header.Set("Upgrade", reqUpType)
	}

    //6、X-Forwarded-For追加ClientIP信息
     //设置 X-Forwarded-For,以逗号+空格分隔
	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
		prior, ok := outreq.Header["X-Forwarded-For"]
		omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
		if len(prior) > 0 {
			clientIP = strings.Join(prior, ", ") + ", " + clientIP
		}
		if !omit {
			outreq.Header.Set("X-Forwarded-For", clientIP)
		}
	}
	//7、向下游请求数据,拿到响应response
	res, err := transport.RoundTrip(outreq)
	if err != nil {
		p.getErrorHandler()(rw, outreq, err)
		return
	}

	//8、处理升级协议请求
     //验证响应状态码是否为101,是才考虑升级
    // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
	if res.StatusCode == http.StatusSwitchingProtocols {
		if !p.modifyResponse(rw, res, outreq) {
			return
		}
        //请求升级方法(具体源码步骤见补充)
		p.handleUpgradeResponse(rw, outreq, res)
		return
	}

    //9、移除逐段消息头,删除从下游返回的无用的数据
	removeConnectionHeaders(res.Header)

	for _, h := range hopHeaders {
		res.Header.Del(h)
	}
    
    //10、修改response返回内容
	if !p.modifyResponse(rw, res, outreq) {
		return
	}

    //11、拷贝头部数据
	copyHeader(rw.Header(), res.Header)

	 //处理Trailer头部
	announcedTrailers := len(res.Trailer)
	if announcedTrailers > 0 {
		trailerKeys := make([]string, 0, len(res.Trailer))
		for k := range res.Trailer {
			trailerKeys = append(trailerKeys, k)
		}
		rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
	}

    //12、写入状态码
	rw.WriteHeader(res.StatusCode)

    //13、按周期刷新内容到response
	err = p.copyResponse(rw, res.Body, p.flushInterval(res))
	if err != nil {
		defer res.Body.Close()
		if !shouldPanicOnCopyError(req) {
			p.logf("suppressing panic for copyResponse error in test; copy error: %v", err)
			return
		}
		panic(http.ErrAbortHandler)
	}
    //读取完body内容后,对body进行关闭
	res.Body.Close()

    //对Trailer逻辑处理
	if len(res.Trailer) > 0 {
		if fl, ok := rw.(http.Flusher); ok {
			fl.Flush()
		}
	}

	if len(res.Trailer) == announcedTrailers {
		copyHeader(rw.Header(), res.Trailer)
		return   
	}

	for k, vv := range res.Trailer {
		k = http.TrailerPrefix + k
		for _, v := range vv {
			rw.Header().Add(k, v)   
		}
	}
}
  1. 通过ReverseProxy 实现代理的中断就是ServeHTTP()方法,在该方法中重点实现了一下逻辑
  1. 验证是否请求终止: 若请求终止,我们就不会把这个服务请求下游,例如关闭浏览器、网络断开等等,那么就会终止请求
  2. 设置请求context信息,如果上游传了部分context信息,那么我就会将这一部分的context信息做设置
  3. 深拷贝header
  4. 修改req: 这里的修改request信息就包含了请求到下游的特殊的head头信息的变更,比如X-Forwarded-For,X-Real-IP
  5. Upgrade头的特殊处理
  6. 追加ClientIP信息: 这里就是X-Forwarded-For,X-Real-IP这一块的设置
  7. 向下游请求数据: transport、roundtrip?方法
  8. 处理升级协议请求
  9. 移除逐段头部
  10. 修改返回数据
  11. 拷贝头部的数据
  12. 写入状态码
  13. 周期刷新内容到response
  1. 请求升级handleUpgradeResponse
func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {
    //比对上游和下游的协议,判断是否都升级成功
   reqUpType := upgradeType(req.Header)
   resUpType := upgradeType(res.Header)
   if !ascii.IsPrint(resUpType) { 
      p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType))
   }
   if !ascii.EqualFold(reqUpType, resUpType) {
      p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType))
      return
   }
	//劫持当前http,通过向下转型的方式获得connection
   hj, ok := rw.(http.Hijacker)
   if !ok {
      p.getErrorHandler()(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw))
      return
   }
   backConn, ok := res.Body.(io.ReadWriteCloser)
   if !ok {
      p.getErrorHandler()(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"))
      return
   }

   backConnCloseCh := make(chan bool)
   go func() {
      // Ensure that the cancellation of a request closes the backend.
      // See issue https://golang.org/issue/35559.
      select {
      case <-req.Context().Done():
      case <-backConnCloseCh:
      }
      backConn.Close()
   }()

   defer close(backConnCloseCh)

   conn, brw, err := hj.Hijack()
   if err != nil {
      p.getErrorHandler()(rw, req, fmt.Errorf("Hijack failed on protocol switch: %v", err))
      return
   }
   defer conn.Close()

   copyHeader(rw.Header(), res.Header)

   res.Header = rw.Header()
    //将response的body赋值为空,只写入头部信息
   res.Body = nil
    //将下游的数据写入response里面
   if err := res.Write(brw); err != nil {
      p.getErrorHandler()(rw, req, fmt.Errorf("response write: %v", err))
      return
   }
    //刷新写入状态
   if err := brw.Flush(); err != nil {
      p.getErrorHandler()(rw, req, fmt.Errorf("response flush: %v", err))
      return
   }
   errc := make(chan error, 1)
   //升级成功,但是还有保持一直维持的状态
    //交换协议,一直维持互相拷贝,直到一方报错,返回
   spc := switchProtocolCopier{user: conn, backend: backConn}
   go spc.copyToBackend(errc)
   go spc.copyFromBackend(errc)
   <-errc
   return
}

三. iris中httputil.ReverseProxy使用示例

  1. 可能不太符合规范,只是一个示例
  2. 编写中间件,在中间件中创建httputil.ReverseProxy保存到iris的context中,创建httputil.ReverseProxy时:
  1. 在httputil.ReverseProxy的director函数中获取目标地址,兼容请求参数,添加请求头
  2. 在httputil.ReverseProxy的modifyFunc函数中校验并重写响应
  3. 在httputil.ReverseProxy的errorHandler函数中处理异常
package middleware

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/kataras/iris"
	"io/ioutil"
	"lmcd_adapter_police/config"
	baseResp "lmcd_adapter_police/entity/response"
	"lmcd_adapter_police/lmerror"
	"lmcd_adapter_police/log"
	encrypt "lmcd_adapter_police/util/aes"
	"net/http"
	"net/http/httputil"
	"net/url"
	"regexp"
	"strconv"
	"strings"
)

var UrlMap = map[string]string{
	"/gw/police/site/info/update":  "/site/info/update",  //上报经营者信息
	"/gw/police/site/room/add":     "/site/room/add",     //增加子经营信息 (民宿 添加房号)
	"/gw/police/site/room/update":  "/site/room/update",  //修改子经营信息 (民宿 修改房号)
	"/gw/police/site/room/delete":  "/site/room/delete",  //删除子经营信息 (民宿 删除房号)
	"/gw/police/site/enter/report": "/site/enter/report", //流动人口进入上报
	"/gw/police/site/exit/report":  "/site/exit/report",  //流动人口离开上报
}

//var policeReportDomain, _ = config.Conf.GetValue("police", "domain")
var policeReportDomain = "http://localHost:8080"

func GetPoliceProxyMiddleware(ctx iris.Context) {
	msgid := ctx.GetHeader("msgid")
	if policeReportDomain == "" {
		baseResp.ResponseCode(ctx, msgid, lmerror.HTTP_DECODE_ERROR)
		return
	}
	targetURL, _ := url.Parse(policeReportDomain)
	reverseProxy := NewPoliceReverseProxy(targetURL)
	if nil == reverseProxy {
		baseResp.ResponseCode(ctx, msgid, lmerror.HTTP_DECODE_ERROR)
		return
	}
	ctx.Values().Set("reverseProxy", reverseProxy)
	ctx.Next()
	ctx.Header("Access-Control-Allow-Headers", "origin, x-requested-with, content-type")
	ctx.Header("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS")
	ctx.Header("Access-Control-Allow-Origin", "*")
	ctx.Header("Access-Control-Allow-Credentials", "true")
	ctx.Header("Content-Type", "application/json")
	ctx.Header("Sec-Fetch-Mode", "cors")
	ctx.Header("Sec-Fetch-Site", "cross-site")
}

func NewPoliceReverseProxy(target *url.URL) *httputil.ReverseProxy {
	//1.封装用来修改请求路径,请求参数的Director函数
	director := func(req *http.Request) {
		msgid := req.Header.Get("msgid")
		path := req.URL.Path

		// 1. 获取path上的请求参数
		targetQuery := target.RawQuery

		// 2. 封装用来修改请求路径、请求参数的Director函数
		re := regexp.MustCompile("^/dir(.*)")
		req.URL.Path = re.ReplaceAllString(path, "$1")

		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host

		// 2.4 设置请求路径
		targetPath, ok := UrlMap[req.URL.Path]
		if !ok {
			baseResp.APIResourceCode(msgid, lmerror.HTTP_DECODE_ERROR)
			return
		}
		req.URL.Path = targetPath

		// 2.5 设置请求参数
		if targetQuery == "" || req.URL.RawQuery == "" {
			req.URL.RawQuery = targetQuery + req.URL.RawQuery
		} else {
			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
		}

		// 读取并处理请求 Body
		if req.Body == nil {
			return
		}
		body, err := getRequestByBody(req)
		defer req.Body.Close()
		if err != nil {
			baseResp.APIResourceCode(msgid, lmerror.HTTP_DECODE_ERROR)
			return
		}

		reqMap := make(map[string]interface{})
		// 将字节切片转换为 Map
		if err := json.Unmarshal(body, &reqMap); err != nil {
			baseResp.APIResourceCode(msgid, lmerror.HTTP_DECODE_ERROR)
			return
		}
		reqMap = requestCompatibility(reqMap)
		reqByte, _ := json.Marshal(reqMap)

		// 更新请求上下文中的 Body 数据
		req.Body = ioutil.NopCloser(bytes.NewReader(reqByte))
		req.ContentLength = int64(len(reqByte))

		// 设置请求头
		// req.Header.Set("token", "ssss")
	}


	//2.封装可用用来改写响应的modifyFunc 函数
	modifyFunc := func(res *http.Response) error {
		msgid := res.Header.Get("msgid")
		if res.StatusCode != 200 {
			// 2.1 此处判断如果响应的 HTTP 状态码为异常时,封装异常返回
			return errors.New("error statusCode")
		}

		// 2.2 读取下游服务响应的 body
		oldPayload, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return err
		}

		policResult := &baseResp.PolicResult{}
		resultData := &baseResp.APIJsonReturn{}

		if err := json.Unmarshal(oldPayload, policResult); err != nil {
			log.ErrorLog(msgid, "请求公安接口解析响应异常 result:= %v", policResult)
			resultData = baseResp.APIResourceCode(msgid, lmerror.CONF_UNKNOW_PMSTYPE_ERROR)
		} else {
			statusCode, err := strconv.Atoi(policResult.ErrCode)
			if err != nil {
				log.ErrorLog(msgid, "请求公安接口获取响应状态码异常 result:= %v", policResult)
				resultData = baseResp.APIResourceCode(msgid, lmerror.POLICE_RETURN_RESULT_ERROR)
			} else if statusCode != 0 {
				resultData = baseResp.APIResource(msgid, lmerror.POLICE_RETURN_RESULT_ERROR, policResult.ErrMsg, nil)
			} else if len(policResult.TraceId) <= 0 {
				resultData = baseResp.APIResourceOk(msgid, policResult.Result)
			} else {
				traceMap := map[string]string{"traceId": policResult.TraceId}
				resultData = baseResp.APIResourceOk(msgid, traceMap)
			}
		}

		// 2.3 封装新的响应
		newPayload, _ := json.Marshal(resultData)

		// 2.4 将数据再次填充到 resp 中 (ioutil.NopCloser() 函数直接将 byte 数据转换为 Body 中的 read)
		res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayload))

		// 2.5 重置响应数据长度和 Content-Length
		res.ContentLength = int64(len(newPayload))
		res.Header.Set("Content-Length", fmt.Sprint(len(newPayload)))

		return nil
	}

	//3.设置异常回调,在上面几个步骤如果发送异常,返回的err不为nin,
	//会执行该函数,执行指定业务逻辑
	errorHandler := func(res http.ResponseWriter, req *http.Request, err error) {
		if nil == err {
			return
		}
		msgid := res.Header().Get("msgid")
		resultData := baseResp.APIResourceCode(msgid, lmerror.CONF_UNKNOW_PMSTYPE_ERROR)
		newPayLoad, _ := json.Marshal(resultData)
		res.Write(newPayLoad)
	}

	//5.创建ReverseProxy返回
	return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc, ErrorHandler: errorHandler}
}

func getRequestByBody(req *http.Request) ([]byte, *lmerror.LmError) {
	data, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return []byte(""), lmerror.NewLmError(lmerror.HTTP_NO_ENCODE, "Failed to read request body")
	}

	msgid := req.Header.Get("msgid")
	lmts := req.Header.Get("lmts")
	path := req.URL.Path
	method := req.Method
	remote := req.RemoteAddr
	lmsignature := strings.ToLower(req.Header.Get("lmsignature"))
	log.TraceLog(msgid, "RequestRaw %s:[%s]:%s:%s:%s:RequestRaw:%s", method, remote, path, lmsignature, lmts, string(data))

	lmencode := req.Header.Get("lmencode")
	if len(lmencode) <= 0 {
		return data, nil
	}

	lmencodevalue, err := strconv.Atoi(lmencode)
	if err != nil {
		return []byte(""), lmerror.NewLmError(lmerror.HTTP_NO_ENCODE, "Invalid lmencode value")
	}

	support, _ := config.Conf.Int("AES", "support")
	if lmencodevalue < support {
		return []byte(""), lmerror.NewLmError(lmerror.HTTP_ENCODE_NOTSUPPORT, "Encoding not supported")
	}

	if lmencodevalue == 1 {
		aeskey, _ := config.Conf.GetValue("AES", "aeskey")
		aesiv, _ := config.Conf.GetValue("AES", "aesiv")
		c := encrypt.NewAES()
		c.Key = []byte(aeskey)
		c.Iv = []byte(aesiv)
		data, err = c.AesCBCDncrypt(data)
		if err != nil {
			return []byte(""), lmerror.NewLmError(lmerror.HTTP_DECODE_ERROR, "Failed to decode request body")
		}
		log.TraceLog(msgid, "RequestDncryptData:%s", string(data))
	}

	return data, nil
}

var imageKeyList = []string{"ownerPic", "licensePic", "cardPic", "pic"}

func requestCompatibility(request map[string]interface{}) map[string]interface{} {
	for _, item := range imageKeyList {
		if _, ok := request[item]; ok {
			request[item] = joinImageBase64Prefix(request[item])
		}
	}
	return request
}
func joinImageBase64Prefix(originalBase64 interface{}) string {
	if nil == originalBase64 {
		return ""
	}
	originalStr := originalBase64.(string)
	if len(originalStr) <= 0 {
		return ""
	}
	base64Prefix := "data:text/plain;base64,"
	return base64Prefix + originalStr
}
  1. 在需要代理的router接口中实现中间件获取到ReverseProxy,在处理器中执行这个Proxy
package main

import (
	"github.com/kataras/iris"
	baseResp "lmcd_adapter_police/entity/response"
	"lmcd_adapter_police/middleware"
	"net/http/httputil"
)

func main() {
	app := iris.New()

	// 使用自定义中间件
	gateway := app.Party("/gw")
	{
		//1.使用GetPoliceProxyMiddleware中间件获取到ReverseProxy
		gateway.Post("/police/*", middleware.GetPoliceProxyMiddleware, func(ctx iris.Context) {
			//处理器中通过ReverseProxy的执行调用到目标服务
			reverseProxy := ctx.Values().Get("reverseProxy")
			reverseProxy.(*httputil.ReverseProxy).ServeHTTP(ctx.ResponseWriter(), ctx.Request())
		})
	}
	app.Post("/site/info/update", routeHandler)

	app.Run(iris.Addr(":8080"))
}

func routeHandler(ctx iris.Context) {
	// 获取解密后的参数
	var params map[string]interface{}
	err := ctx.ReadJSON(&params)
	if err != nil {
		ctx.StatusCode(iris.StatusBadRequest)
		ctx.WriteString("Invalid JSON")
		return
	}
	resp := &baseResp.PolicResult{
		ErrCode: "0",
		ErrMsg:  "成功",
	}
	ctx.JSON(*resp)
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go 进阶 请求代理相关: 二. ReverseProxy 基础讲解 的相关文章

  • nacos简易实现负载均衡

    目录 一 什么是Nacos 二 Nacos下载和安装 1 使用Windows启动 2 验证nacos是否成功启动 三 Nacos Discovery服务注册 发现 四 简易实现负载均衡 1 注册者配置 2 注册者启动类 3 注册者业务层 4
  • golang sleep

    golang的休眠可以使用time包中的sleep 函数原型为 func Sleep d Duration 其中的Duration定义为 type Duration int64 Duration的单位为 nanosecond 为了便于使用
  • Golang协程与通道整理

    协程goroutine 不由OS调度 而是用户层自行释放CPU 从而在执行体之间切换 Go在底层进行协助实现 涉及系统调用的地方由Go标准库协助释放CPU 总之 不通过OS进行切换 自行切换 系统运行开支大大降低 通道channel 并发编
  • goland环境配置

    goland modules环境配置 下载和安装goland 环境配置 配置环境变量GOPATH 配置go modules GOPROXY代理的系统变量 工程目录中新建三个工作目录 goland中启用go modules 新建一个go程序
  • springboot中用undertow踩坑记

    场景 准备基于springboot的静态资源实现mp4资源的播放 不同版本的springboot下效果不一样 可能导致正常的资源不可用 本文测试了几个版本 也针对这种情况提出了解决建议 希望对你的工作有所帮助 众所周知 springboot
  • Go 语言输出文本函数详解

    Go语言拥有三个用于输出文本的函数 Print Println Printf Print 函数以其默认格式打印其参数 示例 打印 i 和 j 的值 package main import fmt func main var i j stri
  • Go 语言注释教程

    注释是在执行时被忽略的文本 注释可用于解释代码 使其更易读 注释还可用于在测试替代代码时防止代码执行 Go支持单行或多行注释 Go单行注释 单行注释以两个正斜杠 开头 在 和行尾之间的任何文本都将被编译器忽略 不会被执行 示例 This i
  • 为什么最近听说 Go 岗位很少很难?

    大家好 我是煎鱼 其实这个话题已经躺在我的 TODO 里很久了 近来很多社区的小伙伴都私下来交流 也有在朋友圈看到朋友吐槽 Go 上海的大会没什么人 还不如 Rust 大会 比较尴尬 今天主要是看看为什么 Go 岗位看起来近来很难的样子 也
  • go-zero开发入门-API网关开发示例

    开发一个 API 网关 代理 https blog csdn net Aquester article details 134856271 中的 RPC 服务 网关完整源代码 file main go package main import
  • go-zero开发入门-API服务开发示例

    接口定义 定义 API 接口文件 接口文件 add api 的内容如下 syntax v1 info title API 接口文件示例 desc 演示如何编写 API 接口文件 author 一见 date 2023年12月07日 vers
  • “go mod tidy”之错误“not a valid zip file”

    执行 go mod tidy 时 遇到如下错误 rpc imports github com zeromicro go zero zrpc imports github com zeromicro go zero zrpc resolver
  • GoLong的学习之路,进阶,微服务之序列化协议,Protocol Buffers V3

    这章是接上一章 使用 RPC包 序列化中没有详细去讲 因为这一块需要看的和学习的地方很多 并且这一块是RPC中可以说是最重要的一块 也是性能的重要影响因子 今天这篇主要会讲其使用方式 文章目录 Protocol Buffers V3 背景以
  • 【golang】go执行shell命令行的方法( exec.Command )

    所需包 import os exec cmd 的用法 cmd exec Command ls lah ls是命令 后面是参数 e cmd Run 多个参数的要分开传入 如 ip link show bond0 cmd
  • go语言实现文件夹上传前后端代码案例

    go语言实现文件夹上传前后端代码案例 前端用于上传的测试界面 如果上传的文件夹有子文件要遍历子文件夹创建出子文件夹再进行拷贝 需要获取文件名和对应的路径 将文件的相对路径和文件对象添加到FormData中 这几行代码很关键 for let
  • 用一个简单的例子教你如何 自定义ASP.NET Core 中间件(二)

    上一章已经说过了 中间件是一种装配到应用管道以处理请求和响应的软件 每个组件 选择是否将请求传递到管道中的下一个组件 可在管道中的下一个组件前后执行工作 请求委托用于生成请求管道 请求委托处理每个 HTTP 请求 一句话总结 中间件是比筛选
  • 消息队列选型:Kafka 如何实现高性能?

    在分布式消息模块中 我将对消息队列中应用最广泛的 Kafka 和 RocketMQ 进行梳理 以便于你在应用中可以更好地进行消息队列选型 另外 这两款消息队列也是面试的高频考点 所以 本文我们就一起来看一下 Kafka 是如何实现高性能的
  • ASP.NET Core路由中间件[1]: 终结点与URL的映射

    一 路由注册 我们演示的这个ASP NET Core应用是一个简易版的天气预报站点 如果用户希望获取某个城市在未来 N 天之内的天气信息 他可以直接利用浏览器发送一个GET请求并将对应城市 采用电话区号表示 和天数设置在URL中 如下图所示
  • 如何将支持标准可观测性协议的中间件快速接入观测

    前言 作为一名云原生工程师 如何将支持标准可观测性协议的中间件快速接入观测云呢 答案是只需要三步 首先 需要确定您要观测的中间件类型 支持标准可观测性协议中间件可通过观测云的 DataKit 采集到中间件的关键指标 有些中间件自带可观测 会
  • go cannot find package “github.com/gorilla/websocket“解读

    Go无法找到包 github com gorilla websocket 的解决方案 在Go开发过程中 我们经常会依赖第三方库来简化开发工作 而使用 go get 命令安装这些库时 有时候我们可能会遇到类似于以下错误的情况 plaintex
  • 【go语言】读取toml文件

    一 简介 TOML 全称为Tom s Obvious Minimal Language 是一种易读的配置文件格式 旨在成为一个极简的数据序列化语言 TOML的设计原则之一是保持简洁性 易读性 同时提供足够的灵活性以满足各种应用场景 TOML

随机推荐

  • 字符串应用-实现KMP匹配算法

    题目描述 给定一个主串S和子串P 使用KMP算法查找子串P在主串S中存在的位置 若子串P在主串S中存在 则输出与子串P中第一字符相等的字符在主串S中的序号 若不存在则输出 no 程序输入格式 主串S 子串P 程序输出格式 输出与子串P中第一
  • Linux三剑客之awk命令详解

    目录 一 awk常见用法 二 案例 2 1 awk中 F的使用 2 2 awk中几个特殊的内部变量 用法 三 实战案例 一 awk常见用法 通常情况下awk所使用的命令格式如下 其中 单引号家伙是那个大括号 用于设置对于数据进行的处理动作
  • HDFS DataNode高密度存储机型的探索尝试

    前言 随着公司业务的发展 我们需要存储越来越庞大的数据来支撑公司业务的发展 这里就涉及到了数据存储能力的问题 需要存储的数据越多 其实意味着我们需要更多的机器来扩增HDFS集群存储的总capacity 但是机器数量的变多另外一方面带来的则是
  • Android Studio获取系统级签名方式

    android sharedUserId android uid system 系统签名 通过sharedUserId 拥有同一个User id的多个APK可以配置成运行在同一个进程中 那么把程序的UID配成android uid syst
  • CAS与ABA问题

    在JDK 5之前Java语言是靠synchronized关键字保证同步的 这会导致有锁机制存在以下问题 1 在多线程竞争下 加锁 释放锁会导致比较多的上下文切换和调度延时 引起性能问题 2 一个线程持有锁会导致其它所有需要此锁的线程挂起 3
  • Python爬虫到入门只需要三个月

    如何入门Python 为了能够帮助大家更轻松的学好Python开发 Python爬数据 Python数据分析等相关理论知识 给大家共同分享自己一套Python学习生活资料 文章最后面的有附属的相关资料 无论你是大牛还是小白 是想转行还是想入
  • C语言-扫雷游戏程序设计

    文章目录 一 问题要求 1 问题描述 2 程序的功能 二 基本要求 1 要求分析 2 需求分析 四 设计概要 1 程序的设计概要 2 程序的主要流程 1 设置棋盘 2 布地雷 五 用户说明 六 测试结果 1 运行结果说明 2 测试结论 七
  • 区块链基本加密概念

    什么是区块链 目前狭义就任务就是一个超级账本 区块链可以用来做什么 可以用来无障碍的置换 既然是用来交易的 那么我们就要有一个地址存放我的资产 地址 举例比特资产地址 一个比特币地址由两部分组成 一部分是公钥哈希值经过Base58check
  • 面试:Spring&SpringMVC&Mybatis 面试必备面试题

    Spring SpringMVC Mybatis常见面试题 历史文章 多线程史上最全面试题 持续更新中 dubbo zookeeper55道高频面试题 附加答案 SpringCloud SpringBoot经典面试题 附加答案 Spring
  • linux进程snprintf函数功能,linux 之 snprintf函数用法

    int snprintf char restrict buf size t n const char restrict format 函数说明 最多从源串中拷贝n 1个字符到目标串中 然后再在后面加一个0 所以如果目标串的大小为n 的话 将
  • MapReduce实现TopN的效果

    1 背景 最近在学习Hadoop的MapReduce 此处记录一下如何实现 TopN 的效果 以及在MapReduce中如何实现 自定义分组 2 需求 我们有一份数据 数据中存在如下3个字段 订单编号 订单项和订单项价格 输出的数据 需求如
  • 最近邻检索(NN)和近似最近邻(ANN)检索

    文章目录 1 最近邻检索 Nearest Neighbor Search 1 1 概述 1 2 应用领域 2 最近邻检索的发展 2 1 精确检索 2 2 近似检索 参考文献 1 最近邻检索 Nearest Neighbor Search 1
  • 解决导入aliyun-sdk-vod-upload后仍报错的问题

    在手动下载阿里云的aliyun sdk vod upload jar包并执行mvn install install file DgroupId com aliyun DartifactId aliyun sdk vod upload Dve
  • STM32 Freertos 添加 外部sram heap_5.c

    1 添加外部SRAM 初始化 2 添加heap 5 c 3 初始化heap 5 c 外部堆栈 Define the start address and size of the two RAM regions not used by the
  • MYSQL基础命令及添加用户及权限操作

    用root管理打开mysql 1 连接数据库 mysql u root p或者 mysql h192 168 222 132 uroot p通过ip远程连接数据库 mysql h host u user p password P port
  • 啊哈C语言——安装啊哈C编译器

    安装啊哈C编译器 首先访问啊哈C语言编译器的官网www ahacpp com 下载啊哈 C语言编译器安装包 Windows 单击下载之后会跳转到腾讯微云 选中文件复选框 然后单击保存微云 这个时候会弹出登录框 选择自己喜欢的登录方式 然后再
  • 华为OD机试 - 全量和已占用字符集(Java)

    题目描述 给定两个字符集合 一个是全量字符集 一个是已占用字符集 已占用字符集中的字符不能再使用 要求输出剩余可用字符集 输入描述 输入一个字符串 一定包含 前为全量字符集 后的为已占用字符集 已占用字符集中的字符一定是全量字符集中的字符
  • drop mysql,MySQL删除大表更快的DROP TABLE办法

    本文内容遵从CC版权协议 可以随意转载 但必须以超链接形式标明文章原始出处和作者信息及版权声明网址 http www penglixun com tech database mysql fast drop table use hard li
  • 【Python】植物大战僵尸-基于pygame模块-part2

    文章目录 版本介绍 一 种子栏类 SeedBank py 1 成员变量 2 方法 展示种子栏 二 主游戏类 MainGame py 1 成员变量 2 加载项 3 主事件项 三 最终效果 版本介绍 继上篇内容开发 该版本为ver1 1 后续版
  • go 进阶 请求代理相关: 二. ReverseProxy 基础讲解

    目录 一 ReverseProxy 基础 ReverseProxy 中提供了哪些功能 ReverseProxy 结构详解 ReverseProxy实现代理的简单示例 1 NewSingleHostReverseProxy 函数源码解释 2