项目地址:https://github.com/yhyddr/caddy-grpc


前言

上一次我们学习了如何在 Caddy 中扩展自己想要的插件。博客中只提供了大致框架。这一次,我们来根据具体插件 caddy-grpc 学习。

选取它的原因是,它本身是一个独立的应用,这里把它做成了一个 Caddy 的插件。或许你有进一步理解到 Caddy 的良好设计。

插件作用

该插件的目的与Improbable-eng/grpc-web/go/grpcwebproxy目的相同,但作为 Caddy 中间件插件而不是独立的Go应用程序。

而这个项目的作用又是什么呢?

这是一个小型反向代理,可以使用gRPC-Web协议支持现有的gRPC服务器并公开其功能,允许从浏览器中使用gRPC服务。 特征:

  • 结构化记录(就是 log 啦)代理请求到stdout(标准输出)
  • 可调试的 HTTP 端口(默认端口8080
  • Prometheus监视代理请求(/metrics在调试端点上)
  • Request(/debug/requests)和连接跟踪端点(/debug/events
  • TLS 1.2服务(默认端口8443):
    • 具有启用客户端证书验证的选项
  • 安全(纯文本)和TLS gRPC后端连接:
    • 使用可自定义的CA证书进行连接

其实意思就是,把这一个反向代理做到了 caddy 服务器的中间件中。

使用

在你需要的时候,可以通过

  1. example.com
  2. grpc localhost:9090

第一行example.com是要服务的站点的主机名/地址。 第二行是一个名为grpc的指令,其中可以指定后端gRPC服务端点地址(即示例中的localhost:9090)。 (注意:以上配置默认为TLS 1.2到后端gRPC服务)

Caddyfile 语法

  1. grpc backend_addr {
  2. backend_is_insecure
  3. backend_tls_noverify
  4. backend_tls_ca_files path_to_ca_file1 path_to_ca_file2
  5. }

backend_is_insecure

默认情况下,代理将使用TLS连接到后端,但是如果后端以明文形式提供服务,则需要添加此选项

backend_tls_noverify

默认情况下,要验证后端的TLS。如果不要验证,则需要添加此选项

backend_tls_ca_files

用于验证后端证书的PEM证书链路径(以逗号分隔)。 如果为空,将使用 host 主机CA链。

源码

目录结构

  1. caddy-grpc
  2. ├── LICENSE
  3. ├── README.md
  4. ├── proxy // 代理 grpc proxy 的功能实现
  5. ├── DOC.md
  6. ├── LICENSE.txt
  7. ├── README.md
  8. ├── codec.go
  9. ├── director.go
  10. ├── doc.go
  11. └── handler.go
  12. ├── server.go // Handle 逻辑文件
  13. └── setup.go // 安装文件

Setup.go

按照我们上次进行的 插件编写的顺序来看,如果不记得,请看:如何为 caddy 添加插件扩展

首先看 安装的 setup.go 文件

init func

  1. func init() {
  2. caddy.RegisterPlugin("grpc", caddy.Plugin{
  3. ServerType: "http",
  4. Action: setup,
  5. })
  6. }

可以知道,该插件 注册的 是 http 服务器,名字叫 grpc

setup func

然后我们看到最重要的 setup 函数,刚才提到的使用方法中,负责分析 caddyfile 中的选项的正是它。它也会将分析到的 directive 交由 Caddy 的 controller 来配置自己这个插件

  1. // setup configures a new server middleware instance.
  2. func setup(c *caddy.Controller) error {
  3. for c.Next() {
  4. var s server
  5. if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
  6. return c.ArgErr()
  7. }
  8. tlsConfig := &tls.Config{}
  9. tlsConfig.MinVersion = tls.VersionTLS12
  10. s.backendTLS = tlsConfig
  11. s.backendIsInsecure = false
  12. //check for more settings in Caddyfile
  13. for c.NextBlock() {
  14. switch c.Val() {
  15. case "backend_is_insecure":
  16. s.backendIsInsecure = true
  17. case "backend_tls_noverify":
  18. s.backendTLS = buildBackendTLSNoVerify()
  19. case "backend_tls_ca_files":
  20. t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
  21. if err != nil {
  22. return err
  23. }
  24. s.backendTLS = t
  25. default:
  26. return c.Errf("unknown property '%s'", c.Val())
  27. }
  28. }
  29. httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
  30. s.next = next
  31. return s
  32. })
  33. }
  34. return nil
  35. }
  1. 我们注意到 依旧是 c.Next() 起手,用来读取配置文件,实际上这里,它读取了 grpc 这个 token 并进行下一步

  2. 然后我们看到,紧跟着 grpc 读取的是 监听地址。

    1. if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
    2. return c.ArgErr()
    3. }

    这里正好对应 在 caddyfile 中的配置 grpc localhost:9090

    1. 注意 c.Next(), c.Args(), c.NextBlock(), 都是读取 caddyfile 中的配置的函数,在caddy 中我们称为 token
  3. 另外是注意到 tls 的配置,前面有提到,该服务是开启 tls 1.2 的服务的

    1. tlsConfig := &tls.Config{}
    2. tlsConfig.MinVersion = tls.VersionTLS12
    3. s.backendTLS = tlsConfig
    4. s.backendIsInsecure = false
  4. 然后是上面所说的 caddyfile 语法中的配置读取

    1. //check for more settings in Caddyfile
    2. for c.NextBlock() {
    3. switch c.Val() {
    4. case "backend_is_insecure":
    5. s.backendIsInsecure = true
    6. case "backend_tls_noverify":
    7. s.backendTLS = buildBackendTLSNoVerify()
    8. case "backend_tls_ca_files":
    9. t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
    10. if err != nil {
    11. return err
    12. }
    13. s.backendTLS = t
    14. default:
    15. return c.Errf("unknown property '%s'", c.Val())
    16. }
    17. }

    可以看到是通过 c.NextBlock() 来进行每一个新 token 的分析,使用 c.Val() 读取之后进行不同的配置。

  5. 最后,别忘了我们要把它加入 整个 caddy 的中间件中去

    1. httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
    2. s.next = next
    3. return s
    4. })

server.go

下面进行第二步。

struct

首先查看这一个插件最核心的结构。即存储了哪些数据

  1. type server struct {
  2. backendAddr string
  3. next httpserver.Handler
  4. backendIsInsecure bool
  5. backendTLS *tls.Config
  6. wrappedGrpc *grpcweb.WrappedGrpcServer
  7. }
  • backendAddr 是 grpc 服务的监听地址
  • next 是下一个插件的 Handler 的处理
  • backendIsInsecure 和 backendTLS 都是后台服务是否启用了不同的安全策略。
  • wrappedGrpc 是这个插件的关键,它实现的是 grpcweb protocol,来让 grpc 服务能够被浏览器访问。

serveHTTP

我们上次的文章中,这是第二重要的部分, serveHTTP 的实现代表着具体的功能。上一次我们的内容只有用来传递给下一个 Handle 的逻辑

  1. func (g gizmoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
  2. return g.next.ServeHTTP(w, r)
  3. }

现在我们来看 这个 grpc 中添加了什么逻辑吧。

  1. // ServeHTTP satisfies the httpserver.Handler interface.
  2. func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
  3. //dial Backend
  4. opt := []grpc.DialOption{}
  5. opt = append(opt, grpc.WithCodec(proxy.Codec()))
  6. if s.backendIsInsecure {
  7. opt = append(opt, grpc.WithInsecure())
  8. } else {
  9. opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
  10. }
  11. backendConn, err := grpc.Dial(s.backendAddr, opt...)
  12. if err != nil {
  13. return s.next.ServeHTTP(w, r)
  14. }
  15. director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
  16. md, _ := metadata.FromIncomingContext(ctx)
  17. return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
  18. }
  19. grpcServer := grpc.NewServer(
  20. grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
  21. grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
  22. /*grpc_middleware.WithUnaryServerChain(
  23. grpc_logrus.UnaryServerInterceptor(logger),
  24. grpc_prometheus.UnaryServerInterceptor,
  25. ),
  26. grpc_middleware.WithStreamServerChain(
  27. grpc_logrus.StreamServerInterceptor(logger),
  28. grpc_prometheus.StreamServerInterceptor,
  29. ),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
  30. )
  31. // gRPC-Web compatibility layer with CORS configured to accept on every
  32. wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
  33. wrappedGrpc.ServeHTTP(w, r)
  34. return 0, nil
  35. }
  • 首先是 grpc 的配置部分,如果你了解 grpc ,你就会知道这是用来配置 grpc 客户端的选项。这里为我们的客户端增添了 Codec 编解码和不同的安全策略选项。

    1. //dial Backend
    2. opt := []grpc.DialOption{}
    3. opt = append(opt, grpc.WithCodec(proxy.Codec()))
    4. if s.backendIsInsecure {
    5. opt = append(opt, grpc.WithInsecure())
    6. } else {
    7. opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
    8. }
    9. backendConn, err := grpc.Dial(s.backendAddr, opt...)
    10. if err != nil {
    11. return s.next.ServeHTTP(w, r)
    12. }
  • 然后是设置了 grpc 服务器的选项

    1. director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
    2. md, _ := metadata.FromIncomingContext(ctx)
    3. return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
    4. }
    5. grpcServer := grpc.NewServer(
    6. grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
    7. grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
    8. /*grpc_middleware.WithUnaryServerChain(
    9. grpc_logrus.UnaryServerInterceptor(logger),
    10. grpc_prometheus.UnaryServerInterceptor,
    11. ),
    12. grpc_middleware.WithStreamServerChain(
    13. grpc_logrus.StreamServerInterceptor(logger),
    14. grpc_prometheus.StreamServerInterceptor,
    15. ),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
    16. )
  • 最后是使用 grpcweb.WrapServer 来实现 web 服务的调用

    1. // gRPC-Web compatibility layer with CORS configured to accept on every
    2. wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
    3. wrappedGrpc.ServeHTTP(w, r)

Proxy

注意到,在上文中使用了 proxy.TransparentHandler 这是在 proxy 的 handler.go 中定义的函数。用来实现 gRPC 服务的代理。这里涉及到 关于 gRPC 的交互的实现,重点是 Client 和 Server 的 stream 传输,与本文关系不大,有兴趣可以下来了解。

结语

思考一下把这个作为 Caddy 的插件带来了什么?

是不是一瞬间获得了很多可以扩展的配置?
而不是将 Caddy 中想要的一些插件的功能做到 最开始说的那个独立应用的项目中。

如果你也在做 HTTP 服务,还在眼馋 Caddy 中的一些功能和它的生态,就像这样接入吧。

它还涉及到了 grpc-web ,如果有兴趣,可以扩展学习一下

grpc-web client implementations/examples:

Vue.js
GopherJS

参考

caddy:https://github.com/caddyserver/caddy
如何写中间件:https://github.com/caddyserver/caddy/wiki/Writing-a-Plugin:-HTTP-Middleware
caddy-grpc插件:https://github.com/pieterlouw/caddy-grpc