Golang gRPC 入门
- gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
- 在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得能够更容易地创建分布式应用和服务。
- gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据序列化机制,它的作用与 XML、json 类似,但它是二进制格式,性能好、效率高(缺点:可读性差)。
gRPC采用protobuf描述 接口和数据, 可以把他理解为: protobuf ON HTTP2 的一种RPC
Hello gRPC
环境安装
从Protobuf的角度看,gRPC只不过是一个针对service接口生成代码的生成器。因此需要提前安装gRPC的代码生成插件
# protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# 安装protoc-gen-go-grpc插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.1.0
代码生成
基于protoc-gen-go-grpc来生产gPRC代码
protobuf 定义接口的语法:
service <service_name> {
rpc <function_name> (<request>) returns (<response>);
}
- service: 用于申明这是个服务的接口
- service_name: 服务的名称,接口名称
- function_name: 函数的名称
- request: 函数参数, 必须的
- response: 函数返回, 必须的, 不能没有
syntax = "proto3";
package hello;
option go_package = "github.com/ProtoDemo/grpc/service";
service HelloService {
rpc Hello(Request) returns (Response);
}
message Request {
string value = 1;
}
message Response {
string value = 1;
}
编译生产代码,指定gRPC插件对应参数
$ protoc -I=./ --go_out=./grpc/service --go_opt=module="github.com/ProtoDemo/grpc/service" --go-grpc_out=./grpc/service --go-grpc_opt=module="github.com/ProtoDemo/grpc/service" grpc/service/service.proto
生成的代码:
// 客户端
// HelloServiceClient is the client API for HelloService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloServiceClient interface {
Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}
// 服务端
// HelloServiceServer is the server API for HelloService service.
// All implementations must embed UnimplementedHelloServiceServer
// for forward compatibility
type HelloServiceServer interface {
Hello(context.Context, *Request) (*Response, error)
mustEmbedUnimplementedHelloServiceServer()
}
gRPC服务端
基于服务端的HelloServiceServer接口可以重新实现HelloService服务
首先构建一个服务实体,实现gRPC定义的接口
package main
import (
"context"
"fmt"
"github.com/ProtoDemo/grpc/service"
"google.golang.org/grpc"
"net"
)
// 通过接口约束HelloService服务
var _ service.HelloServiceServer = (*HelloService)(nil)
type HelloService struct {
service.UnimplementedHelloServiceServer
}
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error) {
resp := &service.Response{}
resp.Value = "hello:" + req.Value
return resp, nil
}
func main() {
// 通过grpc.NewServer()构造一个gRPC服务对象
gRPCServer := grpc.NewServer()
// 然后通过gRPC插件生成的RegisterHelloServiceServer函数注册实现的HelloServiceImpl服务
service.RegisterHelloServiceServer(gRPCServer, new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
fmt.Println("ListenTCP error:", err)
}
// 通过gRPCServer.Serve(listener)在一个监听端口上提供gRPC服务
gRPCServer.Serve(listener)
}
gRPC客户端
package main
import (
"context"
"fmt"
"github.com/ProtoDemo/grpc/service"
"google.golang.org/grpc"
)
func main() {
//grpc.Deal负责和gRPC服务建立链接
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
}
defer conn.Close()
//NewHelloServiceClient函数基于已经简建立链接构造HelloServiceClient对象
//返回client其实是一个helloServiceClient接口对象
client := service.NewHelloServiceClient(conn)
//通过结构定义的方法就可以调用微服务端对应的gRPC服务提供的方法
req := &service.Request{Value: "hello"}
reply, err := client.Hello(context.Background(), req)
if err != nil {
fmt.Println(err)
}
fmt.Println(reply.Value)
}
gRPC流
RPC是远程函数调用,因此每次调用的函数参数和返回值不能太大,否则将严重影响每次调用的响应时间。因此传统的RPC方法调用对于上传和下载较大数据量场景并不适合。为此,gRPC框架针对服务器端和客户端分别提供了流特性
服务端或客户端的单向流是双向流的特例,在HelloService增加一个支持双向流的Channel方法
关键字stream指定启用流特性,参数部分是接收客户端参数的流,返回值是返回给客户端的流。
所以 定义streaming RPC 的语法如下:
rpc <function_name> (stream <type>) returns (stream <type>) {}
syntax = "proto3";
package hello;
option go_package = "github.com/ProtoDemo/grpc/service";
service HelloService {
rpc Hello(Request) returns (Response);
rpc Channel (stream Request) returns (stream Response);
}
message Request {
string value = 1;
}
message Response {
string value = 1;
}
生成Streaming RPC
接口变化
// 客户端的Channel方法返回一个HelloService_ChannelClient类型的返回值,可以用于服务端进行双向通信
// HelloServiceClient is the client API for HelloService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloServiceClient interface {
Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
}
// 在服务端的channel方法参数是一个新的HelloService_ChannelServer类型的参数,可以用于和客户端双向通信
// HelloServiceServer is the server API for HelloService service.
// All implementations must embed UnimplementedHelloServiceServer
// for forward compatibility
type HelloServiceServer interface {
Hello(context.Context, *Request) (*Response, error)
Channel(HelloService_ChannelServer) error
mustEmbedUnimplementedHelloServiceServer()
}
HelloService_ChannelClient 和 HelloService_ChannelServer 接口定义:
// Request --->
// Response <---
type HelloService_ChannelClient interface {
Send(*Request) error
Recv() (*Response, error)
grpc.ClientStream
}
// Request <---
// Response --->
type HelloService_ChannelServer interface {
Send(*Response) error
Recv() (*Request, error)
grpc.ServerStream
}
可以发现服务端和客户端的流辅助接口均定义了Send和Recv方法用于流数据的双向通信
服务端
server端逻辑:
-
接收一个Request
-
响应一个Response
package main
import (
"context"
"fmt"
"github.com/ProtoDemo/grpc/service"
"google.golang.org/grpc"
"io"
"net"
)
// 通过接口约束HelloService服务
var _ service.HelloServiceServer = (*HelloService)(nil)
type HelloService struct {
service.UnimplementedHelloServiceServer
}
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error) {
resp := &service.Response{}
resp.Value = "hello:" + req.Value
return resp, nil
}
func (p *HelloService) Channel(stream service.HelloService_ChannelServer) error {
// 服务端在循环中接收客户端发来的数据
for {
// 接收一个请求
args, err := stream.Recv()
if err != nil {
// 如果遇到io.EOF表示客户端流被关闭
if err == io.EOF {
return nil
}
return err
}
// 响应一个请求
// 生成返回的数据通过流发送给客户端
resp := &service.Response{Value: "hello:" + args.GetValue()}
err = stream.Send(resp)
if err != nil {
// 服务端发送异常, 函数退出, 服务端流关闭
return err
}
}
}
func main() {
// 通过grpc.NewServer()构造一个gRPC服务对象
gRPCServer := grpc.NewServer()
// 然后通过gRPC插件生成的RegisterHelloServiceServer函数注册实现的HelloServiceImpl服务
service.RegisterHelloServiceServer(gRPCServer, new(HelloService))
listener, err := net.Listen("tcp", ":1234")
if err != nil {
fmt.Println("ListenTCP error:", err)
}
gRPCServer.Serve(listener)
}
双向流数据的发送和接收都是完全独立的行为。需要注意的是,发送和接收的操作并不需要一一对应,根据真实场景进行组织代码
客户端
package main
import (
"context"
"fmt"
"github.com/ProtoDemo/grpc/service"
"google.golang.org/grpc"
"io"
"time"
)
func main() {
//grpc.Deal负责和gRPC服务建立链接
conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
if err != nil {
fmt.Println(err)
}
defer conn.Close()
//NewHelloServiceClient函数基于已经简建立链接构造HelloServiceClient对象
//返回client其实是一个helloServiceClient接口对象
client := service.NewHelloServiceClient(conn)
// 客户端需要先调用Channel方法获取返回的流对象
stream,err :=client.Channel(context.Background())
if err != nil {
fmt.Println(err)
}
// 在客户端将发送和接收操作放到两个独立的Goroutine。
// 向服务端发送数据
go func() {
for {
if err := stream.Send(&service.Request{Value: "Hi"}); err != nil {
fmt.Println(err)
}
time.Sleep(time.Second)
}
}()
// 循环中接收服务端返回的数据
for {
reply ,err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
fmt.Println(err)
}
fmt.Println(reply.GetValue())
}
}