??本文來自于網(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) {
...
}
這里特地翻譯了一下源碼的注釋。有三個重點:
- 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ū)別的缠局,下面會講)则奥。
- ServeHTTP實現(xiàn)了Go標準庫里面提供Http服務(wù)的接口坑鱼,所以ServeHTTP就可以對外提供Http服務(wù)了膘流,在ServeHTTP里面絮缅,把收到的請求轉(zhuǎn)發(fā)到對應(yīng)的gRPC方法,并返回gRPC方法的返回呼股。
- 因為Go標準庫的HTTP/2必須使用TLS,所以使用ServeHTTP必須使用TLS狭园,即必須使用證書和https訪問读处。但這不是gRPC的要求,第一節(jié)中我們在client.go中國看到了
grpc.WithInsecure()
就是不使用加密證書的意思唱矛。這個問題在18年Go的Http標準庫支持h2c之后已經(jīng)解決罚舱。
- 因為Go標準庫的HTTP/2必須使用TLS,所以使用ServeHTTP必須使用TLS狭园,即必須使用證書和https訪問读处。但這不是gRPC的要求,第一節(jié)中我們在client.go中國看到了
- 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}
马绝。