简介
rpc是remote procedure call的简称,gprc是Google开源的rpc版本,当前在c++、java、go语言中非常流行,基本是大小公司的首选rpc框架
grpc在使用流程上非常简单:
-
首先定义一份
service描述,需详细描述了该服务提供的哪些接口/方法以及这些方法的输入输出参数。service描述以一种通用的语言来书写(被称为IDL=Interface Definition Language),grpc采用的是Protocol Buffers。service描述(一般被称为proto文件)大概长这样:// service定义了服务 包含了rpc方法 // message定义了消息结构体 service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } -
客户端和服务端利用这个proto文件分别生成SDK文件(通过protoc工具可以生成任意编程语言的SDK)。服务端负责基于SDK实现
SayHello方法,客户端利用SDK可以调用stub(由proto文件生成的结构体)的SayHello方法,就像调用本地函数一样调用服务端提供的函数, 这就是grpc(或一般rpc框架)的使用流程
为什么非得需要rpc呢?主要是三个方面:
- 接口明确:想象一下你在调用别人的http(s)接口时,是不是先得找人要接口文档,看看具体的请求和响应的数据结构,这会增加沟通成本。grpc中proto以一种标准的语言解决了这种沟通成本(标准的接口定义语言)
- 语言无关:通过一份proto文件可生成任意编程语言的sdk。这是一个非常关键的点,编程语言那么多,rpc使得服务提供的能力和编程语言解耦
- 传输效率:grpc框架基于http2协议,并且使用pb数据格式传输,这提升了通信传输效率(Protocol Buffers相较于JSON的优势,用更少的字节数传输信息),这得力于http2的头部压缩、双向推送以及pb数据压缩能力
四种Service方法
service是grpc服务描述的核心概念,service的具体定义以Protocol Buffers语言形式描述,被称为proto文件
关于proto3的介绍这里不展示 不熟悉的同学可以移步protobuf.dev/overview/查看
service主要描述了服务对外提供的方法,如上述例子中的SayHello;grpc一共有四种服务方法类型:
- 简单rpc(simple RPC):客户端(以下简称C)调用方法,服务端(以下简称S)返回响应,结束
- 服务端流rpc(server-to-client streaming RPC):C调用方法,S返回一个流(流可以理解为就是一串响应),C需要不断读取直到流结束(是不是很像open一个文件然后按行读取内容,直到文件尾)
- 客户端流rpc(client-to-server streaming RPC):和上面相反,C不断的向S写入流,S会在输入流结束时返回一个响应
- 双向流rpc(Bidirectional streaming RPC):上面的结合体,C和S都独立的写入流,直到流结束(如果流永远不结束,就相当于C和S一直保持通信了)
基本用法
下面以gprc官方的example来详细介绍下grpc go版本的使用。笔者会从三个方面依次介绍:
- proto文件
- 客户端/服务端基本结构
- 一元RPC和流RPC的客户端/服务端实现
一元RPC指不包含流消息的服务方法 即简单rpc
流RPC包括服务端流rpc、客户端流rpc、双向流rpc
proto
route_guide.proto:描述了上述四种rpc方法 带stream关键字的表示是流类型 github.com/grpc/grpc-g…
// 定义了一个RouteGuide的服务
service RouteGuide {
// 简单rpc: 根据坐标获取位置点
rpc GetFeature(Point) returns (Feature) {}
// 服务端流rpc: 根据一块区域持续获取多个Feature
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// 客户端流rpc: 输出Point流 获得统计信息
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// 双向流rpc: 路线对话
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
// Point -> 经纬度坐标
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// Feature -> 坐标特征点
message Feature {
string name = 1;
Point location = 2;
}
// RouteNote -> 坐标信息
message RouteNote {
Point location = 1;
string message = 2;
}
// Rectangle -> 经纬度矩形
message Rectangle {
Point lo = 1;
Point hi = 2;
}
// RouteSummary -> 统计信息
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
在route_guide.proto同级目录下执行:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
route_guide.proto
会生成两个文件:
- route_guide.pb.go:包含关于序列化、请求、响应的pb格式代码
- route_guide_grpc.pb.go:包含了客户端调用stub interface(对应service中提供的四种方法)以及服务端需要implement的interface(对应service中提供的四种方法)
业务层后续会基于这两文件来实现具体的客户端/服务端逻辑
基本结构
下面介绍下基于刚刚的*.pb.go和*_grpc.pb.go文件实现的服务端、客户端基本结构
服务端
// 自定义struct实现pb.UnimplementedRouteGuideServer
// 即上述service定义的方法
type routeGuideServer struct {
pb.UnimplementedRouteGuideServer
// ...
}
// GetFeature
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
// ...
}
// ListFeatures
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
// ...
}
// RecordRoute
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
// ...
}
// RouteChat
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
// ...
}
func main() {
// 服务端监听端口
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
// opts中可以自定义各种插件
grpcServer := grpc.NewServer(opts...)
// 注册service的实现-routeGuideServer
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
// 启动服务
grpcServer.Serve(lis)
}
客户端
func main() {
// 连接grpc服务器
conn, err := grpc.Dial("localhost:50051")
if err != nil {
// ...
}
defer conn.Close()
// client即为上述service定义的方法集
client := pb.NewRouteGuideClient(conn)
// 分别调用上述四个方法
client.GetFeature(ctx, point)
client.ListFeatures(ctx, rect)
client.RecordRoute(ctx)
client.RouteChat(ctx)
}
流RPC实现
下面依次介绍三种流RPC的服务端/客户端实现:
服务端流rpc(server-to-client streaming RPC)
// 服务端实现
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
// ...
// for循环+Send方法即可
if err := stream.Send(feature); err != nil {
return err
}
}
return nil
}
// 客户端实现
func main() {
// ...
stream, _ := client.ListFeatures(ctx, rect)
// 通过for循环+stream.Recv读取stream 直到stream的EOF
// stream的EOF对应上述ListFeatures函数执行完成
for {
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("client.ListFeatures failed: %v", err)
}
// ...
}
}
客户端流rpc(client-to-server streaming RPC)
// 服务端实现
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
// ...
for {
point, err := stream.Recv()
if err == io.EOF {
// 读到EOF 表示客户端本次发送完成
// 服务端需要发送响应
return stream.SendAndClose(...)
}
// ...
// stream尚未读取完成 服务端自行处理中
}
}
// 客户端实现
func main() {
// ...
stream, _ := client.RecordRoute(ctx)
// 循环向stream中Send数据
for _, point := range points {
stream.Send(point)
}
// 发送完成 关闭发送端并阻塞等待服务端的响应
reply, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("client.RecordRoute failed: %v", err)
}
}
双向流rpc(Bidirectional streaming RPC)
// 服务端实现
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
// 循环读取客户端流数据
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// ...
// 循环发送
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
// 客户端实现
func main() {
go func() {
// 循环读取
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
}
}()
// 循环发送
for _, note := range notes {
_ = stream.Send(note)
}
// 关闭发送 此时服务端会收到stream的EOF
stream.CloseSend()
}
高级用法
grpc是高度扩展的框架,下面介绍下常见的几种扩展用法:
拦截器
拦截器(interceptor)是grpc提供给开发者便于在每个RPC请求前中后进行如认证、鉴权、统计等功能的机制,熟悉Gin框架的同学的可以把拦截器理解为gin.HandlerFunc中间件,只不过grpc由于是双向通信,所以客户端和服务端都可以使用interceptor
拦截器主要划分为两种,一元拦截器(unary interceptor)和流拦截器(stream interceptor),区别就是在于有没有rpc使用stream。
一元拦截器
// 方法类型必须是
// func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 请求前做点什么
// 请求调用RPC方法
err := invoker(ctx, method, req, reply, cc, opts...)
// 请求后做点什么
}
流拦截器
// 方法类型必须是
// func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
// 请求前做点什么
s, err := streamer(ctx, desc, cc, method, opts...)
if err != nil {
return nil, err
}
// 请求中和请求后的自定义处理必须使用wrapper机制
return newWrappedStream(s), nil
}
// stream wrapper机制
type wrappedStream struct {
grpc.ClientStream
}
func (w *wrappedStream) RecvMsg(m interface{}) error {
// RecvMsg前
// rpc调用
err = w.ClientStream.RecvMsg(m)
// RecvMsg后
}
func (w *wrappedStream) SendMsg(m interface{}) error {
// SendMsg前
// rpc调用
err = w.ClientStream.SendMsg(m)
// SendMsg后
}
func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
return &wrappedStream{s}
}
客户端和服务端注册拦截器也非常简单:
// 客户端
grpc.Dial(*addr, grpc.WithUnaryInterceptor(unaryInterceptor), grpc.WithStreamInterceptor(streamInterceptor))
// 服务端
grpc.NewServer(grpc.UnaryInterceptor(unaryInterceptor), grpc.StreamInterceptor(streamInterceptor))
mTLS启用
就像http有安全版https一样,grpc也可以和tls结合实现双向认证(客户端和服务端互相验证身份,https一般都是单向的),这就是mTLS
// 服务端
func main() {
lis, err := net.Listen("tcp", "localhost:50000")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
c := &tls.Config{
Certificates: nil,
RootCAs: nil,
ClientCAs: nil,
}
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(c)))
// grpcServer.RegisterService(nil, nil)
grpcServer.Serve(lis)
}
// 客户端
func main() {
c := &tls.Config{
Certificates: nil,
RootCAs: nil,
ClientCAs: nil,
}
grpc.Dial("localhost:50000", grpc.WithTransportCredentials(credentials.NewTLS(c)))
}
Header & Trailer & Metadata
header相当于http的header,服务端必须在其他数据发送之前发送header,客户端需要在接收其他数据前接收header;trailer和header是相对的,只有在单次RPC完成后才会发送给客户端,例如如果是服务端流类型,则客户端会在收到所有的流式信息后才会收到trailer,trailer的设计主要是为了传递grpc status(grpc的状态码);这里有篇文章详细介绍了trailer的设计背景 值得一看taoshu.in/grpc-traile…
metadata就是header和trailer的具体内容,一些简单的KV对,其中可以传输二进制数据。下面代码演示了服务端/客户端如何通过header和trailer发送metadata
一元RPC
// 服务端
func (s *server) UnaryEcho(ctx context.Context, in *pb.EchoRequest) (*pb.EchoResponse, error) {
// 发送trailer
defer func() {
trailer := metadata.Pairs("timestamp", time.Now().Format(timestampFormat))
grpc.SetTrailer(ctx, trailer)
}()
// ...
// 发送header
header := metadata.New(map[string]string{"location": "MTV", "timestamp": time.Now().Format(timestampFormat)})
grpc.SendHeader(ctx, header)
// 传输数据
return &pb.EchoResponse{Message: in.Message}, nil
}
// 客户端
func unaryCallWithMetadata(c pb.EchoClient, message string) {
// ...
// 获取header和trailer
var header, trailer metadata.MD
r, err := c.UnaryEcho(ctx, &pb.EchoRequest{Message: message}, grpc.Header(&header), grpc.Trailer(&trailer))
if err != nil {
log.Fatalf("failed to call UnaryEcho: %v", err)
}
// ...
}
流RPC
// 服务端
func (s *server) ServerStreamingEcho(in *pb.EchoRequest, stream pb.Echo_ServerStreamingEchoServer) error {
// 发送trailer
defer func() {
trailer := metadata.Pairs("timestamp", time.Now().Format(timestampFormat))
stream.SetTrailer(trailer)
}()
// 从客户端读取metadata
md, ok := metadata.FromIncomingContext(stream.Context())
// 创建并发送header
header := metadata.New(map[string]string{"location": "MTV", "timestamp": time.Now().Format(timestampFormat)})
stream.SendHeader(header)
// 发送数据
for i := 0; i < streamingCount; i++ {
fmt.Printf("echo message %v\n", in.Message)
err := stream.Send(&pb.EchoResponse{Message: in.Message})
if err != nil {
return err
}
}
return nil
}
// 客户端
func serverStreamingWithMetadata(c pb.EchoClient, message string) {
// 创建metadata
md := metadata.Pairs("timestamp", time.Now().Format(timestampFormat))
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 发送
stream, err := c.ServerStreamingEcho(ctx, &pb.EchoRequest{Message: message})
if err != nil {
log.Fatalf("failed to call ServerStreamingEcho: %v", err)
}
// 阻塞等待Header metadata就绪
header, err := stream.Header()
if err != nil {
log.Fatalf("failed to get header from stream: %v", err)
}
// 读取header
if t, ok := header["timestamp"]; ok {
fmt.Printf("timestamp from header:\n")
for i, e := range t {
fmt.Printf(" %d. %s\n", i, e)
}
}
// 读取数据
for {
r, err := stream.Recv()
if err != nil {
rpcStatus = err
break
}
}
// 阻塞等待trailer 注意必须是读取stream到达EOF后才可调用
trailer := stream.Trailer()
if t, ok := trailer["timestamp"]; ok {
}
}
END
以上就是关于grpc-go的全部介绍了 小伙伴们快去试试吧~