避免被面试官吊打!一文教你入门grpc-go

2,425 阅读9分钟

简介

rpc是remote procedure call的简称,gprc是Google开源的rpc版本,当前在c++、java、go语言中非常流行,基本是大小公司的首选rpc框架

grpc在使用流程上非常简单:

  1. 首先定义一份service描述,需详细描述了该服务提供的哪些接口/方法以及这些方法的输入输出参数。service描述以一种通用的语言来书写(被称为IDL=Interface Definition Language),grpc采用的是Protocol Buffersservice描述(一般被称为proto文件)大概长这样:

    // service定义了服务 包含了rpc方法
    // message定义了消息结构体
    
    service Greeter {
      rpc SayHello (HelloRequest) returns (HelloReply) {}
    }
    
    message HelloRequest {
      string name = 1;
    }
    
    message HelloReply {
      string message = 1;
    }
    
  2. 客户端和服务端利用这个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一共有四种服务方法类型:

  1. 简单rpc(simple RPC):客户端(以下简称C)调用方法,服务端(以下简称S)返回响应,结束
  2. 服务端流rpc(server-to-client streaming RPC):C调用方法,S返回一个流(流可以理解为就是一串响应),C需要不断读取直到流结束(是不是很像open一个文件然后按行读取内容,直到文件尾)
  3. 客户端流rpc(client-to-server streaming RPC):和上面相反,C不断的向S写入流,S会在输入流结束时返回一个响应
  4. 双向流rpc(Bidirectional streaming RPC):上面的结合体,C和S都独立的写入流,直到流结束(如果流永远不结束,就相当于C和S一直保持通信了)

基本用法

github.com/grpc/grpc-g…

下面以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是高度扩展的框架,下面介绍下常见的几种扩展用法:

拦截器

github.com/grpc/grpc-g…

拦截器(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

github.com/grpc/grpc-g…

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的全部介绍了 小伙伴们快去试试吧~