grpc同时提供grpc和http接口—h2c和grpc-gateway等的使用

猫仙草 · · 940 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

  本文来自于网上众多大神的博客的集合,加入了自己的理解,主要目的是把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/v1/order/123

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

invalid gRPC request method

  因为proto文件中看到的/v1/order/这个URI是为后面会提到的grpc-gateway准备的,grpc并不知道这个URI。
  上面server代码里我们使用日志输出了*http.Request的内容,可以看到,这个HTTP/2请求应该是一个POST方法,并且URI是/service.OrderService/GetOrder,我们在Postman工具中用POST方法访问

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

得到报错

gRPC requires HTTP/2

  这个报错原因参考这里,前面其实也提到了一点,就是gRPC的HTTP/2和标准HTTP/2还是有一些区别的。
  到此,虽然gRPC用这种方式提供了HTTP访问方式,但是又不能通过http访问,你可能很疑惑,这有啥用???
  答案是可以让grpc服务和其他http服务共用一个端口。我们可以自己实现一个http服务,提供与gRPC相同的功能。这样在外部访问时,就是相当于及提供了gRPC服务,也提供了http服务。
./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()))

这时候访问就正常了。

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还需要用到这个工具
再次修改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不需要修改,访问正常。
  此时在浏览器访问https://localhost:9005/v1/order/123也可以得到正确结果{"orderId":456}


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:猫仙草

查看原文:grpc同时提供grpc和http接口—h2c和grpc-gateway等的使用

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

940 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传