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())
}
}