【微服务网关——https与http2代理实现】

1.https与http2代理

1.1 重新认识https与http2

  • https是http安全版本
  • http2是一种传输协议
  • 两者并没有本质联系

1.1.1 https与http的区别

HTTP(超文本传输协议)和 HTTPS(安全超文本传输协议)是用于在网络上交换数据的两种协议。HTTPS 是 HTTP 的扩展,它在 HTTP 的基础上增加了加密层SSL/TLS ,以保护数据的安全。

  • HTTPS
    • 加密: HTTPS 使用 SSL/TLS 协议来加密传输的数据,保护数据免受窃听。
    • 验证身份: HTTPS 通过数字证书验证服务器的身份,确保数据被发送到正确的服务器。
    • 数据完整性: HTTPS 通过加密和校验机制,确保传输的数据不会被篡改。
    • 端口:443
https请求流程

1.1.2 http1.1与http2区别

  • HTTP/1.1
    • 文本协议:数据以明文格式传输,HTTP/1.1 请求和响应是人类可读的文本。
    • 单一请求-响应通道:每次请求和响应通过单独的 TCP 连接传输,连接复用有限。
    • 队头阻塞:由于请求是按顺序处理的,前一个请求处理未完成时,后续请求会被阻塞。
    • 连接管理:需要为每个请求创建一个新的连接,或者使用连接保持(keep-alive)来重用连接。
  • HTTP/2
    • 二进制协议:数据以二进制格式传输,增加了解析和处理的效率。
    • 多路复用:一个 TCP 连接可以同时承载多个请求和响应,消除了队头阻塞(Head-of-Line Blocking)的问题。
    • 头部压缩:使用 HPACK 算法对 HTTP 头部进行压缩,减少了冗余数据的传输。
    • 优先级控制:可以设置请求的优先级,优化资源的分配和加载顺序。

1.2 http2与https的关系

  • http2代表多路复用的传输协议
  • https代表http服务器使用了加密协议
  • 一个启用https的服务器不一定使用http2
  • 但是使用http2的服务器必须启用https(浏览器强制)

1.3 http2设计目标

  • 感知延迟有实质上改进
  • 解决HTTP1.1中的"队首阻塞"问题
  • 并行操作无需与服务器建立多个连接
  • 保持HTTP1.1语义,只是标准拓展并非替代

2.https与http2代理实现

2.1 创建下游测试服务器

2.1.1 代码实现

go 复制代码
package main

import (
	"fmt"
	"github.com/e421083458/gateway_demo/demo/proxy/reverse_proxy_https/testdata"
	"golang.org/x/net/http2"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)
func main() {
	rs1 := &RealServer{Addr: "127.0.0.1:3003"}
	rs1.Run()
	rs2 := &RealServer{Addr: "127.0.0.1:3004"}
	rs2.Run()

	//监听关闭信号
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
}

type RealServer struct {
	Addr string
}

func (r *RealServer) Run() {
	log.Println("Starting httpserver at " + r.Addr)
	mux := http.NewServeMux()
	mux.HandleFunc("/", r.HelloHandler)
	mux.HandleFunc("/base/error", r.ErrorHandler)
	server := &http.Server{
		Addr:         r.Addr,
		WriteTimeout: time.Second * 3,
		Handler:      mux,
	}
	go func() {
		// 开启http2
		http2.ConfigureServer(server, &http2.Server{})
		// 开启https
		log.Fatal(server.ListenAndServeTLS(testdata.Path("server.crt"), testdata.Path("server.key")))
	}()
}

func (r *RealServer) HelloHandler(w http.ResponseWriter, req *http.Request) {
	upath := fmt.Sprintf("http://%s%s\n", r.Addr, req.URL.Path)
	io.WriteString(w, upath)
}

func (r *RealServer) ErrorHandler(w http.ResponseWriter, req *http.Request) {
	upath := "error handler"
	w.WriteHeader(500)
	io.WriteString(w, upath)
}

2.1.2 证书签名生成方式

bash 复制代码
/*
//CA私钥
openssl genrsa -out ca.key 2048
//CA数据证书
openssl req -x509 -new -nodes -key ca.key -subj "/CN=example1.com" -days 5000 -out ca.crt

//服务器私钥(默认由CA签发)
openssl genrsa -out server.key 2048
//服务器证书签名请求:Certificate Sign Request,简称csr(example1.com代表你的域名)
openssl req -new -key server.key -subj "/CN=example1.com" -out server.csr
//上面2个文件生成服务器证书(days代表有效期)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000
*/

2.1.3 如何开启http2

goLang服务器支持https与http

  • ListenAndServer调整成ListenAndServerTLS即可支持https
  • https服务器设置http2.ConfigureServer支持http2
    • 设置http2对应的handler回调
    • http.Server中设置了对http2的回调
    • http2回调方法负责从帧数据流中转换数据为http请求体
go 复制代码
// ConfigureServer 为 net/http 的 Server 添加 HTTP/2 支持。
//
// 参数 conf 可以为 nil。
//
// ConfigureServer 必须在 s 开始服务之前调用。
func ConfigureServer(s *http.Server, conf *Server) error {
    if s == nil {
        panic("nil *http.Server") // 如果传入的 *http.Server 是 nil,程序会崩溃
    }
    if conf == nil {
        conf = new(Server) // 如果传入的 Server 配置是 nil,则新建一个默认的 Server 配置
    }
    // 初始化 conf 的内部状态
    conf.state = &serverInternalState{activeConns: make(map[*serverConn]struct{})}
    
    // 如果 conf 中的 IdleTimeout 没有设置,则使用 s 的 IdleTimeout 或 ReadTimeout
    if h1, h2 := s, conf; h2.IdleTimeout == 0 {
        if h1.IdleTimeout != 0 {
            h2.IdleTimeout = h1.IdleTimeout
        } else {
            h2.IdleTimeout = h1.ReadTimeout
        }
    }
    // 注册优雅关机的回调函数
    s.RegisterOnShutdown(conf.state.startGracefulShutdown)

    // 如果没有 TLS 配置,创建一个默认的 TLS 配置
    if s.TLSConfig == nil {
        s.TLSConfig = new(tls.Config)
    } else if s.TLSConfig.CipherSuites != nil {
        // 如果已提供了 CipherSuite 列表,检查它的顺序或是否缺少必要的 CipherSuite
        haveRequired := false
        sawBad := false
        for i, cs := range s.TLSConfig.CipherSuites {
            switch cs {
            case tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
                // 另一种 MTI 密码套件,防止只支持 ECDSA 的服务器。
                // 详情请参见 http://golang.org/cl/30721。
                tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
                haveRequired = true
            }
            if isBadCipher(cs) {
                sawBad = true
            } else if sawBad {
                return fmt.Errorf("http2: TLSConfig.CipherSuites 索引 %d 包含 HTTP/2 批准的密码套件 (%#04x),但它在未批准的密码套件之后。这样配置,可能会导致不支持先前批准的密码套件的客户端被分配一个未批准的套件并拒绝连接。", i, cs)
            }
        }
        if !haveRequired {
            return fmt.Errorf("http2: TLSConfig.CipherSuites 缺少 HTTP/2 所需的 AES_128_GCM_SHA256 密码(至少需要 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 或 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 之一)。")
        }
    }

    // 注意:这里没有设置 MinVersion 为 tls.VersionTLS12,
    // 因为我们不想干扰用户服务器上的 HTTP/1.1 流量。我们稍后在接受连接时强制执行 TLS 1.2。
    // 理想情况下,这应该在下一步协议选择中完成,但使用 TLS <1.2 与 HTTP/2 仍然是客户端的错误。

    s.TLSConfig.PreferServerCipherSuites = true // 服务器优先选择密码套件

    haveNPN := false
    for _, p := range s.TLSConfig.NextProtos {
        if p == NextProtoTLS {
            haveNPN = true
            break
        }
    }
    if !haveNPN {
        s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, NextProtoTLS) // 添加 HTTP/2 协议
    }

    if s.TLSNextProto == nil {
        s.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){} // 初始化 TLSNextProto 映射
    }
    // 定义 protoHandler,用于处理新连接
    protoHandler := func(hs *http.Server, c *tls.Conn, h http.Handler) {
        if testHookOnConn != nil {
            testHookOnConn() // 测试钩子,如果存在,则调用
        }
        // TLSNextProto 接口早于 context 出现,因此 net/http 包通过 Handler 上一个未公开的方法传递每个连接的基础 context。
        // 这仅供内部 net/http<=>http2 使用。
        var ctx context.Context
        type baseContexter interface {
            BaseContext() context.Context
        }
        if bc, ok := h.(baseContexter); ok {
            ctx = bc.BaseContext() // 获取 base context
        }
        // 服务连接
        conf.ServeConn(c, &ServeConnOpts{
            Context:    ctx,
            Handler:    h,
            BaseConfig: hs,
        })
    }
    s.TLSNextProto[NextProtoTLS] = protoHandler // 设置 protoHandler
    return nil // 成功返回 nil
}

2.2 https代理整合网关

go 复制代码
package public

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"github.com/e421083458/gateway_demo/demo/proxy/reverse_proxy_https/testdata"
	"golang.org/x/net/http2"
	"io/ioutil"
	"math/rand"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"
)

var transport = &http.Transport{
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second, //连接超时
		KeepAlive: 30 * time.Second, //长连接超时时间
	}).DialContext,
	//TLSClientConfig:       &tls.Config{InsecureSkipVerify: true},
	TLSClientConfig: func() *tls.Config {
		pool := x509.NewCertPool()
		caCertPath := testdata.Path("ca.crt")
		caCrt, _ := ioutil.ReadFile(caCertPath)
		pool.AppendCertsFromPEM(caCrt)
		return &tls.Config{RootCAs: pool}
	}(),
	MaxIdleConns:          100,              //最大空闲连接
	IdleConnTimeout:       90 * time.Second, //空闲超时时间
	TLSHandshakeTimeout:   10 * time.Second, //tls握手超时时间
	ExpectContinueTimeout: 1 * time.Second,  //100-continue 超时时间
}

func NewMultipleHostsReverseProxy(targets []*url.URL) *httputil.ReverseProxy {
	//请求协调者
	director := func(req *http.Request) {
		targetIndex := rand.Intn(len(targets))
		target := targets[targetIndex]
		targetQuery := target.RawQuery
		fmt.Println("target.Scheme")
		fmt.Println(target.Scheme)
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		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", "user-agent")
		}
	}
	http2.ConfigureTransport(transport)
	return &httputil.ReverseProxy{Director: director, Transport: transport,}
}

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
}
相关推荐
Lee川14 小时前
深度拆解:基于面向对象思维的“就地编辑”组件全模块解析
javascript·架构
勤劳打代码14 小时前
Flutter 架构日记 — 状态管理
flutter·架构·前端框架
子兮曰19 小时前
后端字段又改了?我撸了一个 BFF 数据适配器,从此再也不怕接口“屎山”!
前端·javascript·架构
卓卓不是桌桌1 天前
如何优雅地处理 iframe 跨域通信?这是我的开源方案
javascript·架构
Qlly1 天前
DDD 架构为什么适合 MCP Server 开发?
人工智能·后端·架构
用户881586910912 天前
AI Agent 协作系统架构设计与实践
架构
鹏北海2 天前
Qiankun 微前端实战踩坑历程
前端·架构
货拉拉技术2 天前
货拉拉海豚平台-大模型推理加速工程化实践
人工智能·后端·架构
RoyLin2 天前
libkrun 深度解析:架构设计、模块实现与 Windows WHPX 后端
架构
CoovallyAIHub3 天前
实时视觉AI智能体框架来了!Vision Agents 狂揽7K Star,延迟低至30ms,YOLO+Gemini实时联动!
算法·架构·github