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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342