本文来自于网上众多大神的博客的集合,加入了自己的理解,主要目的是把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) {
...
}
这里特地翻译了一下源码的注释。有三个重点:
- 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是有一些区别的,下面会讲)。
- ServeHTTP实现了Go标准库里面提供Http服务的接口,所以ServeHTTP就可以对外提供Http服务了,在ServeHTTP里面,把收到的请求转发到对应的gRPC方法,并返回gRPC方法的返回。
- 因为Go标准库的HTTP/2必须使用TLS,所以使用ServeHTTP必须使用TLS,即必须使用证书和https访问。但这不是gRPC的要求,第一节中我们在client.go中国看到了
grpc.WithInsecure()
就是不使用加密证书的意思。这个问题在18年Go的Http标准库支持h2c之后已经解决。
- 因为Go标准库的HTTP/2必须使用TLS,所以使用ServeHTTP必须使用TLS,即必须使用证书和https访问。但这不是gRPC的要求,第一节中我们在client.go中国看到了
- 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}
。
有疑问加站长微信联系(非本文作者)