gRPC入门
1. 什么是gRPC
gRPC是谷歌公司基于protobuf开发的跨语言的开源RPC框架,基于http/2协议设计,对移动设备更加友好。
go语言gRPC技术栈最底层为TCP或者Unix套接字协议,在此之上是http/2协议的实现,然后在http/2协议之上有构建了针对go语言的gRPC核心库。应用程序通过gRPC插件生成的Stub代码和gRPC核心库通信,也可以直接和gRPC核心库通信。
2. gRPC入门
创建hello.proto文件,定义需要的HelloService接口:
syntax="proto3"; package pb; option go_package="../pb"; message String{ string value=1; } service HelloService{ rpc Hello (String) returns (String); }
使用gRPC生成指令
protoc --go_out=plugins=grpc:. hello.proto
我们可以在生成的代码文件中看到gRPC插件会为服务器和客户端生成不同的接口
gRPC通过context参数,为每个方法调用提供了上下文支持。客户端在调用方法的时候,可以通过可选的CallOption类型的参数提供额外的上下文信息。
我们可以构建一个gRPC的客户端:
type HelloServiceImpl struct { } func (p *HelloServiceImpl) Hello(ctx context.Context, args *pb.String) (*pb.String, error) { reply := &pb.String{Value: "hello: " + args.GetValue()} return reply, nil } func main() { // 构造一个grpc服务对象 grpcServer := grpc.NewServer() // 注册grpc服务,和rpc很类似 pb.RegisterHelloServiceServer(grpcServer, new(HelloServiceImpl)) listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal(err) } // 在指定端口提供grpc服务 grpcServer.Serve(listener) }
然后构建一个gRPC客户端接收gRPC服务:
func main() { // 和grpc服务建立连接 conn, err := grpc.Dial("localhost:1234", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } defer conn.Close() // 构造client对象,client可以调用服务端注册的grpc服务提供的方法 client := pb.NewHelloServiceClient(conn) // 调用grpc服务 reply, err := client.Hello(context.Background(), &pb.String{Value: "hello"}) if err != nil { log.Fatal(err) } fmt.Println(reply.GetValue()) }
现在就可以启用grpc服务了
gRPC和标准库RPC有一个区别,即gRPC生成的接口并不支持异步调用。不过我们可以在多个goroutine之间安全的共享gRPC底层的http/2链接,因此可以通过在另一个goroutine阻塞调用的方法模拟异步调用。
3. gRPC流
远程调用要求每次传输的数据量不能太大,否则响应时间带来的问题将损害rpc的高效性,所以传统rpc对上传下载数据量较大的场景并不合适,同时传统rpc也不适用于时间不确定的订阅和发布模式。
为此,gRPC框架针对服务端和客户端分别提供了流特性。
服务端或客户端的单项流是双向流的特例,我们在HelloService增加一个支持双向流的Channel()方法
关键字stream指定启用的流特性,参数部分是接受客户端的流,返回值是返回给客户端的流。
service HelloService{ rpc Hello (String) returns (String); rpc Channel(stream String) returns (stream String); }
重新生成代码,可以看到接口中新增的Channel()方法的定义
现在我们可以开始实现服务端流服务,基本的方法就是Recv() 和Send()
func (p *HelloServiceImpl) Channel(stream pb.HelloService_ChannelServer) error { // 循环接收客户端发来的数据 for { args, err := stream.Recv() if err != nil { // 如果遇到EOF表示客户端流关闭 if err == io.EOF { return nil } return err } // 生成数据 reply := &pb.String{Value: "hello: " + args.GetValue()} // 通过流发送给客户端 err = stream.Send(reply) if err != nil { return err } } }
然后让客户端在两个不同的协程中处理接收和发送:
func main() { // 和grpc服务建立连接 conn, err := grpc.Dial("localhost:1234", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } defer conn.Close() // 构造client对象,client可以调用服务端注册的grpc服务提供的方法 client := pb.NewHelloServiceClient(conn) stream, err := client.Channel(context.Background()) if err != nil { log.Fatal(err) } // 将发送和接收数据放到两个独立的goroutine中 // 发送数据 go func() { for { if err := stream.Send(&pb.String{Value: "hi"}); 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()) } }