项目地址:github.com/yhyddr/cadd…vue
上一次咱们学习了如何在 Caddy 中扩展本身想要的插件。博客中只提供了大体框架。这一次,咱们来根据具体插件 caddy-grpc
学习。git
选取它的缘由是,它自己是一个独立的应用,这里把它作成了一个 Caddy 的插件。或许你有进一步理解到 Caddy 的良好设计。 github
该插件的目的与Improbable-eng/grpc-web/go/grpcwebproxy目的相同,但做为 Caddy 中间件插件而不是独立的Go应用程序。web
而这个项目的做用又是什么呢?c#
这是一个小型反向代理,可使用gRPC-Web协议支持现有的gRPC服务器并公开其功能,容许从浏览器中使用gRPC服务。 特征:后端
- 结构化记录(就是 log 啦)代理请求到stdout(标准输出)
- 可调试的 HTTP 端口(默认端口
8080
)- Prometheus监视代理请求(
/metrics
在调试端点上)- Request(
/debug/requests
)和链接跟踪端点(/debug/events
)- TLS 1.2服务(默认端口
8443
):
- 具备启用客户端证书验证的选项
- 安全(纯文本)和TLS gRPC后端链接:
- 使用可自定义的CA证书进行链接
其实意思就是,把这一个反向代理作到了 caddy 服务器的中间件中。浏览器
在你须要的时候,能够经过安全
example.com
grpc localhost:9090
复制代码
第一行example.com是要服务的站点的主机名/地址。 第二行是一个名为grpc的指令,其中能够指定后端gRPC服务端点地址(即示例中的localhost:9090)。 (注意:以上配置默认为TLS 1.2到后端gRPC服务)bash
grpc backend_addr {
backend_is_insecure
backend_tls_noverify
backend_tls_ca_files path_to_ca_file1 path_to_ca_file2
}
复制代码
默认状况下,代理将使用TLS链接到后端,可是若是后端以明文形式提供服务,则须要添加此选项 服务器
默认状况下,要验证后端的TLS。若是不要验证,则须要添加此选项
用于验证后端证书的PEM证书链路径(以逗号分隔)。 若是为空,将使用 host 主机CA链。
caddy-grpc
├── LICENSE
├── README.md
├── proxy // 代理 grpc proxy 的功能实现
│ ├── DOC.md
│ ├── LICENSE.txt
│ ├── README.md
│ ├── codec.go
│ ├── director.go
│ ├── doc.go
│ └── handler.go
├── server.go // Handle 逻辑文件
└── setup.go // 安装文件
复制代码
按照咱们上次进行的 插件编写的顺序来看,若是不记得,请看:如何为 caddy 添加插件扩展
func init() {
caddy.RegisterPlugin("grpc", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
复制代码
能够知道,该插件 注册的 是 http 服务器,名字叫 grpc
而后咱们看到最重要的 setup 函数,刚才提到的使用方法中,负责分析 caddyfile 中的选项的正是它。它也会将分析到的 directive 交由 Caddy 的 controller 来配置本身这个插件
// setup configures a new server middleware instance.
func setup(c *caddy.Controller) error {
for c.Next() {
var s server
if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
return c.ArgErr()
}
tlsConfig := &tls.Config{}
tlsConfig.MinVersion = tls.VersionTLS12
s.backendTLS = tlsConfig
s.backendIsInsecure = false
//check for more settings in Caddyfile
for c.NextBlock() {
switch c.Val() {
case "backend_is_insecure":
s.backendIsInsecure = true
case "backend_tls_noverify":
s.backendTLS = buildBackendTLSNoVerify()
case "backend_tls_ca_files":
t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
if err != nil {
return err
}
s.backendTLS = t
default:
return c.Errf("unknown property '%s'", c.Val())
}
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
s.next = next
return s
})
}
return nil
}
复制代码
咱们注意到 依旧是 c.Next() 起手,用来读取配置文件,实际上这里,它读取了 grpc 这个 token 并进行下一步
而后咱们看到,紧跟着 grpc 读取的是 监听地址。
if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified
return c.ArgErr()
}
复制代码
这里正好对应 在 caddyfile 中的配置 grpc localhost:9090
注意 c.Next(), c.Args(), c.NextBlock(), 都是读取 caddyfile 中的配置的函数,在caddy 中咱们称为 token
另外是注意到 tls 的配置,前面有提到,该服务是开启 tls 1.2 的服务的
tlsConfig := &tls.Config{}
tlsConfig.MinVersion = tls.VersionTLS12
s.backendTLS = tlsConfig
s.backendIsInsecure = false
复制代码
//check for more settings in Caddyfile
for c.NextBlock() {
switch c.Val() {
case "backend_is_insecure":
s.backendIsInsecure = true
case "backend_tls_noverify":
s.backendTLS = buildBackendTLSNoVerify()
case "backend_tls_ca_files":
t, err := buildBackendTLSFromCAFiles(c.RemainingArgs())
if err != nil {
return err
}
s.backendTLS = t
default:
return c.Errf("unknown property '%s'", c.Val())
}
}
复制代码
能够看到是经过 c.NextBlock()
来进行每个新 token 的分析,使用 c.Val() 读取以后进行不一样的配置。
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
s.next = next
return s
})
复制代码
首先查看这一个插件最核心的结构。即存储了哪些数据
type server struct {
backendAddr string
next httpserver.Handler
backendIsInsecure bool
backendTLS *tls.Config
wrappedGrpc *grpcweb.WrappedGrpcServer
}
复制代码
咱们上次的文章中,这是第二重要的部分, serveHTTP 的实现表明着具体的功能。上一次咱们的内容只有用来传递给下一个 Handle 的逻辑
func (g gizmoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
return g.next.ServeHTTP(w, r)
}
复制代码
如今咱们来看 这个 grpc 中添加了什么逻辑吧。
// ServeHTTP satisfies the httpserver.Handler interface.
func (s server) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
//dial Backend
opt := []grpc.DialOption{}
opt = append(opt, grpc.WithCodec(proxy.Codec()))
if s.backendIsInsecure {
opt = append(opt, grpc.WithInsecure())
} else {
opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
}
backendConn, err := grpc.Dial(s.backendAddr, opt...)
if err != nil {
return s.next.ServeHTTP(w, r)
}
director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
}
grpcServer := grpc.NewServer(
grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
/*grpc_middleware.WithUnaryServerChain( grpc_logrus.UnaryServerInterceptor(logger), grpc_prometheus.UnaryServerInterceptor, ), grpc_middleware.WithStreamServerChain( grpc_logrus.StreamServerInterceptor(logger), grpc_prometheus.StreamServerInterceptor, ),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
)
// gRPC-Web compatibility layer with CORS configured to accept on every
wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
wrappedGrpc.ServeHTTP(w, r)
return 0, nil
}
复制代码
//dial Backend
opt := []grpc.DialOption{}
opt = append(opt, grpc.WithCodec(proxy.Codec()))
if s.backendIsInsecure {
opt = append(opt, grpc.WithInsecure())
} else {
opt = append(opt, grpc.WithTransportCredentials(credentials.NewTLS(s.backendTLS)))
}
backendConn, err := grpc.Dial(s.backendAddr, opt...)
if err != nil {
return s.next.ServeHTTP(w, r)
}
复制代码
director := func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
md, _ := metadata.FromIncomingContext(ctx)
return metadata.NewOutgoingContext(ctx, md.Copy()), backendConn, nil
}
grpcServer := grpc.NewServer(
grpc.CustomCodec(proxy.Codec()), // needed for proxy to function.
grpc.UnknownServiceHandler(proxy.TransparentHandler(director)),
/*grpc_middleware.WithUnaryServerChain( grpc_logrus.UnaryServerInterceptor(logger), grpc_prometheus.UnaryServerInterceptor, ), grpc_middleware.WithStreamServerChain( grpc_logrus.StreamServerInterceptor(logger), grpc_prometheus.StreamServerInterceptor, ),*/ //middleware should be a config setting or 3rd party middleware plugins like for caddyhttp
)
复制代码
// gRPC-Web compatibility layer with CORS configured to accept on every
wrappedGrpc := grpcweb.WrapServer(grpcServer, grpcweb.WithCorsForRegisteredEndpointsOnly(false))
wrappedGrpc.ServeHTTP(w, r)
复制代码
注意到,在上文中使用了 proxy.TransparentHandler 这是在 proxy 的 handler.go 中定义的函数。用来实现 gRPC 服务的代理。这里涉及到 关于 gRPC 的交互的实现,重点是 Client 和 Server 的 stream 传输,与本文关系不大,有兴趣能够下来了解。
思考一下把这个做为 Caddy 的插件带来了什么?
是否是一瞬间得到了不少能够扩展的配置?
而不是将 Caddy 中想要的一些插件的功能作到 最开始说的那个独立应用的项目中。
若是你也在作 HTTP 服务,还在眼馋 Caddy 中的一些功能和它的生态,就像这样接入吧。
它还涉及到了 grpc-web ,若是有兴趣,能够扩展学习一下
caddy:github.com/caddyserver…
如何写中间件:github.com/caddyserver…
caddy-grpc插件:github.com/pieterlouw/…