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接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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文件中有什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | .... // 用于数据传输的结构体 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | // 服务传递的参数 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文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | 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文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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()) } } |
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Ai满嘴顺口溜,想考研?浪费我几个小时
· Browser-use 详细介绍&使用文档
· 软件产品开发中常见的10个问题及处理方法