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

??本文來自于網(wǎng)上眾多大神的博客的集合警儒,加入了自己的理解浮庐,主要目的是把grpc和http的關(guān)系做一個全面的梳理總結(jié)姑子。

0. 寫在前面的一些說明

??本文默認你已經(jīng)學(xué)習(xí)其他博客,知道怎么寫一個簡單的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發(fā)出的消息是HTTP/2協(xié)議格式山卦,server按照HTTP/2協(xié)議解析收到的消息鞋邑。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監(jiān)聽tcp的9005端口(端口號自己選,注意不要和已有的服務(wù)沖突)嬉荆,client建立與server的tcp連接归敬。我們根本不需要處理HTTP/2相關(guān)的問題,grpc自己解決了鄙早。

2. grpc同時提供http接口

??了解的比較深的同學(xué)這里剎一下車汪茧,這一節(jié)暫時還不會講到grpc-gateway,只是讓grpc使用http連接代替直接使用TCP限番。
??我們在第一節(jié)看到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實現(xiàn)了go標準庫里面的http.Handler接口,通過在gRPC服務(wù)中查找請求的gRPC方法肢执,來響應(yīng)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服務(wù)疚颊,意味著必須使用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服務(wù)和已有的http服務(wù)共用一個端口珠插,可以使用一個前置的http服務(wù)來進行轉(zhuǎn)發(fā),如下:
//
//   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服務(wù)捻撑,這和gRPC基于HTTP/2所指的HTTP/2完全不是一個東西。他們兩的行為缤底、特征可能差異非常大顾患。
// ServeHttp并不支持gRPC的HTTP/2服務(wù)所支持的一些特性,并且ServeHTTP是實驗性質(zhì)的个唧,可能會有變化江解。
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  ...
}

這里特地翻譯了一下源碼的注釋。有三個重點:

    1. ServeHTTP實現(xiàn)了Go標準庫里面提供Http服務(wù)的接口坑鱼,所以ServeHTTP就可以對外提供Http服務(wù)了膘流,在ServeHTTP里面絮缅,把收到的請求轉(zhuǎn)發(fā)到對應(yīng)的gRPC方法,并返回gRPC方法的返回呼股。
      可以理解為ServeHTTP在gRPC外面包了一層HTTP/2協(xié)議編解碼器耕魄。因為gRPC本身就是基于HTTP/2通信的,所以原來的server彭谁、client還能正常通信吸奴,但是此時我們也可以不要client直接發(fā)HTTP/2請求就能訪問server了(實際上并不能訪問,gRPC的HTTP/2和標準的HTTP/2是有一些區(qū)別的缠局,下面會講)则奥。
    1. 因為Go標準庫的HTTP/2必須使用TLS,所以使用ServeHTTP必須使用TLS狭园,即必須使用證書和https訪問读处。但這不是gRPC的要求,第一節(jié)中我們在client.go中國看到了grpc.WithInsecure()就是不使用加密證書的意思唱矛。這個問題在18年Go的Http標準庫支持h2c之后已經(jīng)解決罚舱。
    1. ServeHTTP可以達到多個服務(wù)共用一個端口的目的。

我們修改一下服務(wù)端代碼:
./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直接關(guān)閉了連接窃肠。再次修改:
./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的內(nèi)容冤留,可以看到碧囊,這個HTTP/2請求應(yīng)該是一個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庫創(chuàng)建一個請求(這是一個測試類):

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服務(wù)共用一個端口。就是上面文檔注釋提到的根據(jù)協(xié)議版本進行轉(zhuǎn)發(fā)州既。
./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服務(wù)
            otherHandler.ServeHTTP(w, r)
        }
    }), &http2.Server{})
}

??這里有個很明顯的問題,這樣直接訪問使用的URI/service.OrderService/GetOrder很不友好谜洽,只支持POST方法,而且暴露了gRPC內(nèi)部的方法名吴叶,這就是第四節(jié)grpc-gateway出現(xiàn)的原因阐虚。

3. Go HTTP標準庫新升級,不再需要TLS證書

??參考一篇很優(yōu)秀的博客
??2018 年 6 月蚌卤,官方標準庫golang.org/x/net/http2/h2c正式推出实束,這個標準庫實現(xiàn)了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節(jié)中提到咸灿,我們可以自己實現(xiàn)一個與gRPC相同功能的http服務(wù)构订,雖然在用戶側(cè)感覺是一個服務(wù)既提供了gRPC服務(wù),也提供了http服務(wù)避矢,但是在服務(wù)器上就是部署了兩套代碼悼瘾,修改、升級之類的肯定都是不方便的审胸,所以懶人工具grpc-gateway出現(xiàn)了亥宿。
??grpc-gateway解決了標準HTTP/1.1和gRPC的HTTP/2的轉(zhuǎn)換問題。直接接收Restful請求并轉(zhuǎn)發(fā)到gRPC然后再返回響應(yīng)砂沛。只需要在proto文件中做相應(yīng)的配置(第0節(jié)給出的proto文件已經(jīng)做了配置)烫扼,另外除了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() {
  // 創(chuàng)建grpc-gateway服務(wù)材蛛,轉(zhuǎn)發(fā)到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)
    }

  // 創(chuàng)建grpc服務(wù)
    rpcServer := grpc.NewServer()
  service.RegisterOrderServiceServer(rpcServer, new(service.OrderService))
  
  // 創(chuàng)建http服務(wù),監(jiān)聽9005端口怎抛,并調(diào)用上面的兩個服務(wù)來處理請求
    http.ListenAndServe(
        ":9005",
        grpcHandlerFunc(rpcServer, gwmux),
    )
}

// grpcHandlerFunc 根據(jù)請求頭判斷是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也可以得到正確結(jié)果{"orderId":456}马绝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挣菲,隨后出現(xiàn)的幾起案子富稻,更是在濱河造成了極大的恐慌,老刑警劉巖白胀,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椭赋,死亡現(xiàn)場離奇詭異,居然都是意外死亡或杠,警方通過查閱死者的電腦和手機哪怔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來向抢,“玉大人认境,你說我怎么就攤上這事⌒” “怎么了叉信?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長艘希。 經(jīng)常有香客問我硼身,道長硅急,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任佳遂,我火速辦了婚禮营袜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘讶迁。我一直安慰自己连茧,他們只是感情好,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布巍糯。 她就那樣靜靜地躺著啸驯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祟峦。 梳的紋絲不亂的頭發(fā)上罚斗,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音宅楞,去河邊找鬼针姿。 笑死,一個胖子當著我的面吹牛厌衙,可吹牛的內(nèi)容都是我干的距淫。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼婶希,長吁一口氣:“原來是場噩夢啊……” “哼榕暇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喻杈,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤彤枢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后筒饰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缴啡,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年瓷们,在試婚紗的時候發(fā)現(xiàn)自己被綠了业栅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡谬晕,死狀恐怖式镐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情固蚤,我是刑警寧澤娘汞,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站夕玩,受9級特大地震影響你弦,放射性物質(zhì)發(fā)生泄漏惊豺。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一禽作、第九天 我趴在偏房一處隱蔽的房頂上張望尸昧。 院中可真熱鬧,春花似錦旷偿、人聲如沸烹俗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽幢妄。三九已至,卻和暖如春茫负,著一層夾襖步出監(jiān)牢的瞬間蕉鸳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工忍法, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留潮尝,地道東北人。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓饿序,卻偏偏與公主長得像勉失,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子原探,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348