項目地址:https://github.com/yhyddr/caddy-grpcvue
上一次咱們學習瞭如何在 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 服務器的中間件中。bash
在你須要的時候,能夠經過服務器
example.com grpc localhost:9090
第一行example.com是要服務的站點的主機名/地址。 第二行是一個名爲grpc的指令,其中能夠指定後端gRPC服務端點地址(即示例中的localhost:9090)。 (注意:以上配置默認爲TLS 1.2到後端gRPC服務)
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 }
if !c.Args(&s.backendAddr) { //loads next argument into backendAddr and fail if none specified return c.ArgErr() }
這裏正好對應 在 caddyfile 中的配置 grpc localhost:9090
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:https://github.com/caddyserver/caddy
如何寫中間件:https://github.com/caddyserver/caddy/wiki/Writing-a-Plugin:-HTTP-Middleware
caddy-grpc插件:https://github.com/pieterlouw/caddy-grpc