• gRPC 介绍和简单实现


    gRPC介绍

      gRPC是Google公司基于Protobuf开发的跨语言的开源RPC框架。gRPC基于HTTP/2协议设计,可以基于一个HTTP/2链接提供多个服务,对于移动设备更加友好。本节将讲述gRPC的简单用法。

    gRPC的技术栈:

       最底层为TCP或Unix Socket协议,在此之上是HTTP/2协议的实现,然后在HTTP/2协议之上又构建了针对Go语言的gRPC核心库。应用程序通过gRPC插件生产的Stub代码和gRPC核心库通信,也可以直接和gRPC核心库通信。

     如果从Protobuf的角度看,gRPC只不过是一个针对service接口生成代码的生成器。我们在本章的第二节(《Go 语言高级编程》)中手工实现了一个简单的Protobuf代码生成器插件,只不过当时生成的代码是适配标准库的RPC框架的。现在我们将学习gRPC的用法。

    创建在项目的proto/hello.proto文件,定义HelloService接口:

    syntax = "proto3";
    
    package proto;
    
    // 服务传递的参数
    message String {
        string value = 1;
    }
    
    // 区别于RPC服务,gRPC可以在proto文件中定义服务方法接口,从而生成给客户端和服务端两个用的接口
    service HelloService {
        rpc Hello (String) returns (String);
    }
    

    进入proto目录下使用如下命令生成相应的接口go文件:protoc --go_out=plugins=grpc:. hello.proto, 这条命令会调用protoc-gen-go 内置的gRPC插件生成相应的文件。我们可以简单分析哈生成的go文件中有什么?

    ....
    
    // 用于数据传输的结构体
    type String struct {
    	Value                string   `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
    	XXX_NoUnkeyedLiteral struct{} `json:"-"`
    	XXX_unrecognized     []byte   `json:"-"`
    	XXX_sizecache        int32    `json:"-"`
    }
    
    // 获取参数的方法
    func (m *String) GetValue() string {
    	if m != nil {
    		return m.Value
    	}
    	return ""
    }
    
    
    // 注册类型
    func init() {
    	proto.RegisterType((*String)(nil), "proto.String")
    }
    
    // 上下文
    // Reference imports to suppress errors if they are not otherwise used.
    var _ context.Context
    var _ grpc.ClientConn
    
    ...
    // 客户端接口约束
    type HelloServiceClient interface {
    	Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
    }
    
    type helloServiceClient struct {
    	cc *grpc.ClientConn
    }
    
    func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
    	return &helloServiceClient{cc}
    }
    
    func (c *helloServiceClient) Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error) {
    	out := new(String)
    	err := c.cc.Invoke(ctx, "/proto.HelloService/Hello", in, out, opts...)
    	if err != nil {
    		return nil, err
    	}
    	return out, nil
    }
    ...
    
    // HelloServiceServer is the server API for HelloService service.服务端
    type HelloServiceServer interface {
    	Hello(context.Context, *String) (*String, error)
    }
    
    // UnimplementedHelloServiceServer can be embedded to have forward compatible implementations.
    type UnimplementedHelloServiceServer struct {
    }
    
    func (*UnimplementedHelloServiceServer) Hello(ctx context.Context, req *String) (*String, error) {
    	return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
    }
    
    // 注册服务端函数
    func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
    	s.RegisterService(&_HelloService_serviceDesc, srv)
    }
    

    接着我们可以编写服务端代码:service.go

    package main
    
    import (
    	"context"
    	"fmt"
    	"gRPC_demo/proto"
    	"google.golang.org/grpc" // 要么 go get google.golang.org/grpc, 要么go mod tidy
    	"log"
    	"net"
    )
    
    type HelloServiceImp struct {
    }
    
    func (p *HelloServiceImp) Hello(ctx context.Context, arg *proto.String) (*proto.String, error) {
    	reply := &proto.String{Value: "hello: " + arg.GetValue()}
    	return reply, nil
    }
    
    /*
    和启动标准RPC服务流程类似
    首先是通过grpc.NewServer()构造一个gRPC服务对象,然后通过gRPC插件生成的RegisterHelloServiceServer函数注册我们实现的HelloServiceImpl服务。
    然后通过grpcServer.Serve(lis)在一个监听端口上提供gRPC服务。*/
    func main() {
    	// 创建服务初始化
    	grpcServer := grpc.NewServer()
    	// 调用接口文件生成的服务端要实现的函数,完成服务注册
    	proto.RegisterHelloServiceServer(grpcServer, new(HelloServiceImp))
    
    	fmt.Println("service starting....")
    	lis, err := net.Listen("tcp",":1234")
    	if err != nil {
    		log.Fatal(err)
    	}
    	grpcServer.Serve(lis)
    } 

    gRPC通过context.Context参数,为每个方法调用提供了上下文支持。客户端在调用方法的时候,可以通过可选的grpc.CallOption类型的参数提供额外的上下文信息。紧接着就是客户端代码:client.go

    package main
    
    import (
    	"context"
    	"fmt"
    	"gRPC_demo/proto"
    	"google.golang.org/grpc"
    	"log"
    )
    
    func main() {
    	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer conn.Close()
    
    	client := proto.NewHelloServiceClient(conn)
    	reply, err := client.Hello(context.Background(), &proto.String{Value:"Wang"})
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Println(reply.GetValue())
    }

    其中grpc.Dial负责和gRPC服务建立链接,然后NewHelloServiceClient函数基于已经建立的链接构造HelloServiceClient对象。返回的client其实是一个HelloServiceClient接口对象,通过接口定义的方法就可以调用服务端对应的gRPC服务提供的方法。

    gRPC和标准库的RPC框架有一个区别,gRPC生成的接口并不支持异步调用。不过我们可以在多个Goroutine之间安全地共享gRPC底层的HTTP/2链接,因此可以通过在另一个Goroutine阻塞调用的方式模拟异步调用。

    gRPC流

      RPC是远程函数调用,因此每次调用的函数参数和返回值不能太大,否则将严重影响每次调用的响应时间。因此传统的RPC方法调用对于上传和下载较大数据量场景并不适合。同时传统RPC模式也不适用于对时间不确定的订阅和发布模式。为此,gRPC框架针对服务器端和客户端分别提供了流特性。

    服务端或客户端的单向流是双向流的特例,我们在HelloService增加一个支持双向流的Channel方法,hello_str.proto:

    syntax = "proto3";
    
    package proto;
    
    // 服务传递的参数
    message String {
      string value = 1;
    }
    
    // 区别于RPC服务,gRPC可以在proto文件中定义服务方法接口,从而生成给客户端和服务端两个用的接口
    // 关键字stream指定启用流特性,参数部分是接收客户端参数的流,返回值是返回给客户端的流。
    service HelloService {
      rpc Hello (String) returns (String);
      rpc Channel (stream String) returns (stream String);
    }

    完成好proto文件后,我们使用如下命令生成go文件:protoc --go_out=plugins=grpc:. hello_str.proto, 紧接着我们分析生成的go文件:

    // 服务传递的参数
    type String struct {
    	Value                string   `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
    	XXX_NoUnkeyedLiteral struct{} `json:"-"`
    	XXX_unrecognized     []byte   `json:"-"`
    	XXX_sizecache        int32    `json:"-"`
    }
    
    ...
    // 为客户端接口添加了Channel方法
    type HelloServiceClient interface {
    	Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error)
    	Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
    }
    
    type helloServiceClient struct {
    	cc *grpc.ClientConn
    }
    
    // 返回一个grpc连接对象
    func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
    	return &helloServiceClient{cc}
    }
    
    // 客户端实现hello
    func (c *helloServiceClient) Hello(ctx context.Context, in *String, opts ...grpc.CallOption) (*String, error) {
    	out := new(String)
    	err := c.cc.Invoke(ctx, "/proto.HelloService/Hello", in, out, opts...)
    	if err != nil {
    		return nil, err
    	}
    	return out, nil
    }
    
    // 客户端实现channel
    func (c *helloServiceClient) Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error) {
    	stream, err := c.cc.NewStream(ctx, &_HelloService_serviceDesc.Streams[0], "/proto.HelloService/Channel", opts...)
    	if err != nil {
    		return nil, err
    	}
    	x := &helloServiceChannelClient{stream}
    	return x, nil
    }
    
    // 新增的结构体,用以标识流
    type HelloService_ChannelClient interface {
    	Send(*String) error
    	Recv() (*String, error)
    	grpc.ClientStream
    }
    
    type helloServiceChannelClient struct {
    	grpc.ClientStream
    }
    
    // 新结构体的收发函数实现
    func (x *helloServiceChannelClient) Send(m *String) error {
    	return x.ClientStream.SendMsg(m)
    }
    
    func (x *helloServiceChannelClient) Recv() (*String, error) {
    	m := new(String)
    	if err := x.ClientStream.RecvMsg(m); err != nil {
    		return nil, err
    	}
    	return m, nil
    }
    
    // HelloServiceServer is the server API for HelloService service.
    type HelloServiceServer interface {
    	Hello(context.Context, *String) (*String, error)  
            // 传递的HelloService_ChannelServer是一个新的结构体,可以用于和客户端和向通信,而客户端调用Channel方法后返回的HelloService_ChannelServer用于和服务端通信
    	Channel(HelloService_ChannelServer) error
    }
    
    // UnimplementedHelloServiceServer can be embedded to have forward compatible implementations.
    type UnimplementedHelloServiceServer struct {
    }
    
    func (*UnimplementedHelloServiceServer) Hello(ctx context.Context, req *String) (*String, error) {
    	return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
    }
    func (*UnimplementedHelloServiceServer) Channel(srv HelloService_ChannelServer) error {
    	return status.Errorf(codes.Unimplemented, "method Channel not implemented")
    }
    
    func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
    	s.RegisterService(&_HelloService_serviceDesc, srv)
    }

     下面我们实现服务端代码:str_service.go文件:

    package main
    
    import (
    	"context"
    	"fmt"
    	"gRPC_demo/proto"
    	"google.golang.org/grpc" // 要么 go get google.golang.org/grpc, 要么go mod tidy
    	"io"
    	"log"
    	"net"
    )
    
    type HelloServiceImps struct {
    }
    
    func (p *HelloServiceImps) Hello(ctx context.Context, arg *proto.StringX) (*proto.StringX, error) {
    	reply := &proto.StringX{Value: "hello: " + arg.GetValue()}
    	return reply, nil
    }
    
    // 服务端使用Channel对来进行收发消息,如果遇到io.EOF表示客户端发送完毕
    func (p *HelloServiceImps) Channel(stream proto.HelloServiceStr_ChannelServer) error {
    	for {
    		args, err := stream.Recv()
    		if err != nil {
    			if err == io.EOF {
    				return nil
    			}
    			return err
    		}
    
    		reply := &proto.StringX{Value: "hello:" + args.GetValue()}
    		err = stream.Send(reply)
    		if err != nil {
    			return err
    		}
    	}
    }
    /*
    和启动标准RPC服务流程类似
    首先是通过grpc.NewServer()构造一个gRPC服务对象,然后通过gRPC插件生成的RegisterHelloServiceServer函数注册我们实现的HelloServiceImpl服务。
    然后通过grpcServer.Serve(lis)在一个监听端口上提供gRPC服务。*/
    func main() {
    	// 创建服务初始化
    	grpcServer := grpc.NewServer()
    	// 调用接口文件生成的服务端要实现的函数,完成服务注册
    	proto.RegisterHelloServiceStrServer(grpcServer, new(HelloServiceImps))
    
    	fmt.Println("service starting....")
    	lis, err := net.Listen("tcp",":1234")
    	if err != nil {
    		log.Fatal(err)
    	}
    	grpcServer.Serve(lis)
    }
    

     客户端代码:str_client.go文件:

    package main
    
    import (
    	"context"
    	"fmt"
    	"gRPC_demo/proto"
    	"google.golang.org/grpc"
    	"io"
    	"log"
    	"time"
    )
    
    func main() {
    	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer conn.Close()
    
    	client := proto.NewHelloServiceStrClient(conn)
    	stream, err := client.Channel(context.Background())
    	if err != nil {
    		log.Fatal(err)
    	}
    	go func() {
    		for {
    			if err := stream.Send(&proto.StringX{Value:"hi li"}); err != nil {
    				log.Fatal(err)
    			}
    			time.Sleep(time.Second)
    		}
    	}()
    	for {
    		reply, err := stream.Recv()
    		if err != nil {
    			if err == io.EOF {
    				break
    			}
    			log.Fatal(err)
    		}
    		fmt.Println(reply.GetValue())
    	}
    }
    

      

     

  • 相关阅读:
    C# TCP异步服务/客户端
    Entity Framework Batch Update and Future Queries
    用c#编写socks代理服务器,大白话细述协议的最重要部分。
    ArraySegment AsEnumerable
    c#数据包拦截+网络编程
    为.Net Remoting增加基于SKYPE API的P2P传输通道
    如何看PDA的IP
    基本搞定PDA HTTP Server
    PDA SOAP Server探索历程(1)
    疑难问题之PDA SOAP Server
  • 原文地址:https://www.cnblogs.com/double-W/p/12760246.html
Copyright © 2020-2023  润新知