美文网首页go
grpc同时提供grpc和http接口—h2c和grpc-gat

grpc同时提供grpc和http接口—h2c和grpc-gat

作者: 猫尾草 | 来源:发表于2020-07-09 10:48 被阅读0次

      本文来自于网上众多大神的博客的集合,加入了自己的理解,主要目的是把grpc和http的关系做一个全面的梳理总结。

    0. 写在前面的一些说明

      本文默认你已经学习其他博客,知道怎么写一个简单的grpc demo,所以编译proto文件之类的都略过不提。如果你还没有,可以先看这个
    本文使用的proto文件:

    syntax = "proto3";
    package service;
    option go_package = ".;service";
    import "google/api/annotations.proto";
    
    message OrderResponse {
        int32 orderId = 1;
    }
    
    message OrderReuqest {
        int32 orderId = 1;
    }
    
    service OrderService {
        rpc NewOrder (OrderReuqest) returns (OrderResponse) {
            option (google.api.http) = {
                post: "/v1/order"
                body: "*"
            };
        }
    
        rpc GetOrder (OrderReuqest) returns (OrderResponse) {
            option (google.api.http) = {
                get: "/v1/order/{orderId}"
            };
        }
    }
    

    protoc编译后的文件太长这里就不贴出来了,以及TLS证书,可以直接下载

    1. grpc基于HTTP/2是什么意思?

      很简单,就是字面意思,grpc的client和server通信是基于HTTP/2,client发出的消息是HTTP/2协议格式,server按照HTTP/2协议解析收到的消息。grpc把这个过程包装了,你看不到。下面看一个最简单的grpc例子。
    ./server/server.go

    package main
    
    import (
        "grpc-example/service"
        "net"
    
        "google.golang.org/grpc"
    )
    
    func main() {
        rpcServer := grpc.NewServer()
        service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
        lis, _ := net.Listen("tcp", ":9005")
        rpcServer.Serve(lis)
    }
    

    ./client/client.go

    package main
    
    import (
        "context"
        "grpc-example/service"
        "log"
    
        "google.golang.org/grpc"
    )
    
    func main() {
        conn, err := grpc.Dial(":9005", grpc.WithInsecure())
        if err != nil {
            log.Fatalf("连接失败,原因:%v", err)
        }
        defer conn.Close()
        orderClient := service.NewOrderServiceClient(conn)
        orderResponse, err := orderClient.GetOrder(context.Background(), &service.OrderReuqest{OrderId: 123})
        if err != nil {
            log.Fatalf("请求收不到返回:%v", err)
        }
        log.Println(orderResponse.OrderId)
    }
    

      可以看到,server监听tcp的9005端口(端口号自己选,注意不要和已有的服务冲突),client建立与server的tcp连接。我们根本不需要处理HTTP/2相关的问题,grpc自己解决了。

    2. grpc同时提供http接口

      了解的比较深的同学这里刹一下车,这一节暂时还不会讲到grpc-gateway,只是让grpc使用http连接代替直接使用TCP。
      我们在第一节看到rpcServer.Serve(lis),这是grpc提供的方法:

    func (s *Server) Serve(lis net.Listener) error{
      ...
    }
    

      实际上还提供了另一个方法:

    // ServeHTTP implements the Go standard library's http.Handler
    // interface by responding to the gRPC request r, by looking up
    // the requested gRPC method in the gRPC server s.
    // 
    // ServeHTTP实现了go标准库里面的http.Handler接口,通过在gRPC服务中查找请求的gRPC方法,来响应gRPC请求
    //
    // The provided HTTP request must have arrived on an HTTP/2
    // connection. When using the Go standard library's server,
    // practically this means that the Request must also have arrived
    // over TLS.
    //
    // HTTP请求必须是走HTTP/2连接。如果使用的是Go标准库的http服务,意味着必须使用TLS加密方式建立http连接。
    
    // To share one port (such as 443 for https) between gRPC and an
    // existing http.Handler, use a root http.Handler such as:
    //
    // 为了让gRPC的http服务和已有的http服务共用一个端口,可以使用一个前置的http服务来进行转发,如下:
    //
    //   if r.ProtoMajor == 2 && strings.HasPrefix(
    //      r.Header.Get("Content-Type"), "application/grpc") {
    //      grpcServer.ServeHTTP(w, r)
    //   } else {
    //      yourMux.ServeHTTP(w, r)
    //   }
    //
    // Note that ServeHTTP uses Go's HTTP/2 server implementation which is totally
    // separate from grpc-go's HTTP/2 server. Performance and features may vary
    // between the two paths. ServeHTTP does not support some gRPC features
    // available through grpc-go's HTTP/2 server, and it is currently EXPERIMENTAL
    // and subject to change.
    
    // 注意,ServeHTTP使用Go的HTTP/2服务,这和gRPC基于HTTP/2所指的HTTP/2完全不是一个东西。他们两的行为、特征可能差异非常大。
    // ServeHttp并不支持gRPC的HTTP/2服务所支持的一些特性,并且ServeHTTP是实验性质的,可能会有变化。
    func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
      ...
    }
    

    这里特地翻译了一下源码的注释。有三个重点:

      1. ServeHTTP实现了Go标准库里面提供Http服务的接口,所以ServeHTTP就可以对外提供Http服务了,在ServeHTTP里面,把收到的请求转发到对应的gRPC方法,并返回gRPC方法的返回。
        可以理解为ServeHTTP在gRPC外面包了一层HTTP/2协议编解码器。因为gRPC本身就是基于HTTP/2通信的,所以原来的server、client还能正常通信,但是此时我们也可以不要client直接发HTTP/2请求就能访问server了(实际上并不能访问,gRPC的HTTP/2和标准的HTTP/2是有一些区别的,下面会讲)。
      1. 因为Go标准库的HTTP/2必须使用TLS,所以使用ServeHTTP必须使用TLS,即必须使用证书和https访问。但这不是gRPC的要求,第一节中我们在client.go中国看到了grpc.WithInsecure()就是不使用加密证书的意思。这个问题在18年Go的Http标准库支持h2c之后已经解决。
      1. ServeHTTP可以达到多个服务共用一个端口的目的。

    我们修改一下服务端代码:
    ./server/server.go

    import (
        "grpc-example/service"
        "log"
        "net/http"
        "google.golang.org/grpc"
    )
    func main() {
        rpcServer := grpc.NewServer()
        service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
        http.ListenAndServe(":9005", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Printf("收到请求%v", r)
            rpcServer.ServeHTTP(w, r)
        }))
    }
    

      这时client再访问就会报错rpc error: code = Unavailable desc = connection closed,这就是上面提到的需要使用TLS加密访问,而这里不是,所以server直接关闭了连接。再次修改:
    ./server/server.go

    http.ListenAndServeTLS(
        ":9005",
        "../cert/server.pem",
        "../cert/server.key",
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log.Printf("收到请求%v", r)
            rpcServer.ServeHTTP(w, r)
        }),
    )
    

    同时修改client端:
    ./client/client.go

    conn, err := grpc.Dial(":9005", grpc.WithTransportCredentials(util.GetClientCredentials()))
    

    这时候访问就正常了。
    如果你尝试在浏览器访问

    https://localhost:9005
    

    server收到了请求,但是浏览器端会收到报错

    invalid gRPC request method
    

      上面server代码里我们使用日志输出了*http.Request的内容,可以看到,这个HTTP/2请求应该是一个POST方法,并且URI是/service.OrderService/GetOrder,我们在Postman工具中用POST方法访问

    https://localhost:9005/service.OrderService/GetOrder
    

    得到报错

    gRPC requires HTTP/2
    

      这个报错原因是postman不支持HTTP/2,我们在日志中也可以看到使用postman访问时是HTTP/1.1。
      使用Go的http库创建一个请求(这是一个测试类):

    package asd
    
    import (
        "crypto/tls"
        "fmt"
        "io/ioutil"
        "log"
        "net/http"
        "strings"
        "testing"
    
        "golang.org/x/net/http2"
    )
    
    func TestAsd(t *testing.T) {
    
        tr := &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        }
        http2.ConfigureTransport(tr)
        client := &http.Client{Transport: tr}
    
        req, err := http.NewRequest("POST", "https://localhost:9005/service.OrderService/GetOrder", strings.NewReader("OrderId=123"))
        if err != nil {
            t.Fatal(err)
        }
        req.Header.Add("Content-type", "application/grpc")
        resp, err := client.Do(req)
        if err != nil {
            log.Fatal(err)
        }
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            t.Fatal(err)
        }
        fmt.Println(string(body))
    }
    

    访问正常,但是还是不能收到正确的返回。原因参考这里
      gRPC提供了HTTP访问方式(虽然不能直接用http访问,但是gRPC client走的是http请求),就可以和其他http服务共用一个端口。就是上面文档注释提到的根据协议版本进行转发。
    ./server/server.go

    http.ListenAndServeTLS(
        ":9005",
        "../cert/server.pem",
        "../cert/server.key",
        grpcHandlerFunc(rpcServer),
    )
    
    func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
        return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
                grpcServer.ServeHTTP(w, r)
            } else {
                            // 这就是另一个http服务
                otherHandler.ServeHTTP(w, r)
            }
        }), &http2.Server{})
    }
    

      这里有个很明显的问题,这样直接访问使用的URI/service.OrderService/GetOrder很不友好,只支持POST方法,而且暴露了gRPC内部的方法名,这就是第四节grpc-gateway出现的原因。

    3. Go HTTP标准库新升级,不再需要TLS证书

      参考一篇很优秀的博客
      2018 年 6 月,官方标准库golang.org/x/net/http2/h2c正式推出,这个标准库实现了HTTP/2的未加密模式,因此我们就可以利用该标准库在同个端口上既提供 HTTP/1.1 又提供 HTTP/2 的功能了。
    ./server/server.go

    package main
    
    import (
        "context"
        "grpc-example/service"
        "log"
        "net/http"
    
        "golang.org/x/net/http2"
        "golang.org/x/net/http2/h2c"
        "google.golang.org/grpc"
    )
    
    func main() {
        rpcServer := grpc.NewServer()
        service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
        http.ListenAndServe(
            ":9005",
            grpcHandlerFunc(rpcServer),
        )
    }
    func grpcHandlerFunc(grpcServer *grpc.Server) http.Handler {
        return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            grpcServer.ServeHTTP(w, r)
        }), &http2.Server{})
    }
    

    同时修改client端:
    ./client/client.go

    conn, err := grpc.Dial(":9005", grpc.WithInsecure())
    

    4. grpc-gateway登场

      第2节中提到,我们可以自己实现一个与gRPC相同功能的http服务,虽然在用户侧感觉是一个服务既提供了gRPC服务,也提供了http服务,但是在服务器上就是部署了两套代码,修改、升级之类的肯定都是不方便的,所以懒人工具grpc-gateway出现了。
      grpc-gateway解决了标准HTTP/1.1和gRPC的HTTP/2的转换问题。直接接收Restful请求并转发到gRPC然后再返回响应。只需要在proto文件中做相应的配置(第0节给出的proto文件已经做了配置),另外除了protoc还需要用到protoc-gen-grpc-gateway这个工具,参考
    再次修改server代码:
    ./server/server.go

    package main
    
    import (
        "context"
        "grpc-example/service"
        "log"
        "net/http"
        "strings"
    
        "github.com/grpc-ecosystem/grpc-gateway/runtime"
        "golang.org/x/net/http2"
        "golang.org/x/net/http2/h2c"
        "google.golang.org/grpc"
    )
    
    func main() {
      // 创建grpc-gateway服务,转发到grpc的9005端口
        gwmux := runtime.NewServeMux()
        opt := []grpc.DialOption{grpc.WithInsecure()}
        err := service.RegisterOrderServiceHandlerFromEndpoint(context.Background(), gwmux, "localhost:9005", opt)
        if err != nil {
            log.Fatal(err)
        }
    
      // 创建grpc服务
        rpcServer := grpc.NewServer()
      service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
      
      // 创建http服务,监听9005端口,并调用上面的两个服务来处理请求
        http.ListenAndServe(
            ":9005",
            grpcHandlerFunc(rpcServer, gwmux),
        )
    }
    
    // grpcHandlerFunc 根据请求头判断是grpc请求还是grpc-gateway请求
    func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
        return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
                grpcServer.ServeHTTP(w, r)
            } else {
                otherHandler.ServeHTTP(w, r)
            }
        }), &http2.Server{})
    }
    

    client不需要修改,访问正常。
      此时在浏览器访问http://localhost:9005/v1/order/123也可以得到正确结果{"orderId":456}

    相关文章

      网友评论

        本文标题:grpc同时提供grpc和http接口—h2c和grpc-gat

        本文链接:https://www.haomeiwen.com/subject/mippcktx.html