前言
在之前的文章中,我们通常的做法都是客户端请求—服务端响应的模式,客户端收集好所有的请求信息,发送到服务端,服务端对信息进行业务处理之后再返回最终响应结果。在更多的场景中,我们传输的数据包非常大,比如,客户端需要查询大量用户的积分,然后再拿着这堆用户的积分做其他处理,如果按照之前的做法,传输的数据包会非常大,这会占用大量的带宽,并且服务端需要等待客户端全部发送之后,才能进行处理及响应。在本文中,我们将介绍gRPC的流模式,根据流传输的方向,可以分为客户端流、服务端流以及双向流,而这里所谓的 “流” 就是可以将大的数据包拆分为小的数据包多次传输,比如服务端流就表示服务端会进行多次响应,当所有的响应发送完毕之后,服务端会以元数据形式将其状态发送给客户端,从而标记流的结束。下面以客户端查询用户积分为例介绍,首先我们定义本次的模型结构以及传输的消息格式:
服务端准备
模型结构 Models.proto:
syntax = "proto3"; //proto3的语法,不写会默认为proto2
package services; //包名,通过protoc生成go文件时使用
option go_package = "../services"; //添加生成go文件的路径
message UserInfo {
int32 user_id = 1; //用户id
int32 user_score = 2; //用户积分
}
传输格式 Users.proto:
syntax = "proto3"; //proto3的语法,不写会默认为proto2
package services; //包名,通过protoc生成go文件时使用
import "Models.proto";
option go_package = "../services"; //添加生成go文件的路径
message UserScoreRequest { //请求消息格式
repeated UserInfo users = 1; // repeated表示能获取多个,传多个用户id过来,查询添加其中的积分返回
}
message UserScoreResponse { //响应消息格式
repeated UserInfo users = 1;
}
service UserScoreService{ // 定义服务端提供的积分服务
}
基于上面的定义以及上一篇文章自签CA、服务端和客户端双向认证中的双向认证知识,实现下面的几种方式传输响应。
一、传统批量操作方式
传统批量操作是客户端先收集所有请求,一次发送到服务端,服务端接收完毕之后,再进行处理及做出响应。
gRPC中,我们需要先定义好提供的服务,以及该服务的实现,因此,在上面的Users.proto
的服务定义中,我们定义好普通批量操作的接口方法:
service UserScoreService{
rpc GetUserScore(UserScoreRequest) returns (UserScoreResponse){} //普通批量操作
}
然后利用protoc生成Users.pb.go
中间文件,比如我的生成命令为:
protoc --go_out=plugins=grpc:../services Users.proto
…/services就表示了生成的go文件的路径。之后我们在Service的UserService.go中实现该接口,接口定义在Users.pb.go
中,可进行查找查看参数列表:
package services
import (
"context"
"fmt"
)
type UserScoreService struct {
}
// 普通批量操作
func (*UserScoreService) GetUserScore(ctx context.Context, request *UserScoreRequest) (*UserScoreResponse, error) {
fmt.Println("请求用户积分的用户列表GetUserScore:", request.Users)
var score int32 = 100
users := make([]*UserInfo, 0)
for _, user := range request.Users {
user.UserScore = score
score++
users = append(users, user)
}
return &UserScoreResponse{Users: users}, nil
}
接着,在服务端中注册该服务启动服务端即可(这里是建立在前篇文章中讲的双向认证的基础之上的)。
server.go
package main
import (
"google.golang.org/grpc"
"grpcpro/helper"
"grpcpro/services"
"log"
"net"
)
const (
Address = "127.0.0.1:8888" // Address gRPC服务地址
)
func main() {
rpcServer := grpc.NewServer(grpc.Creds(helper.GetServeCreds())) //实例化grpc Server
//创建带ca证书验证的服务端
services.RegisterUserScoreServiceServer(rpcServer, new(services.UserScoreService)) //注册用户积分服务
listen, _ := net.Listen("tcp", Address) //设置传输协议和监听地址
log.Println("Listen on " + Address + " with TLS")
rpcServer.Serve(listen)
}
helper.go:
package helper
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc/credentials"
"io/ioutil"
)
func GetServeCreds() credentials.TransportCredentials {
// TLS认证
//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("keys/server.pem", "keys/server.key")
certPool := x509.NewCertPool() //初始化一个CertPool
ca, _ := ioutil.ReadFile("keys/ca.pem")
certPool.AppendCertsFromPEM(ca) //解析传入的证书,解析成功会将其加到池子中
creds := credentials.NewTLS(&tls.Config{ //构建基于TLS的TransportCredentials选项
Certificates: []tls.Certificate{cert}, //服务端证书链,可以有多个
ClientAuth: tls.RequireAndVerifyClientCert, //要求必须验证客户端证书
ClientCAs: certPool, //设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
})
return creds
}
客户端部分:
将服务端的Users.pb.go
拷贝到客户端使用。
package main
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/timestamp"
"google.golang.org/grpc"
"io"
//"google/protobuf/timestamp.proto"
"grpcClient/helper"
"grpcClient/services"
"log"
"time"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:8888"
)
func main() {
// TLS连接
//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(helper.GetClientCreds())) //连接服务端
if err != nil {
log.Fatal(err)
}
defer conn.Close()
ctx := context.Background()
//传统批量方式
userScoreClient := services.NewUserScoreServiceClient(conn) // 用户积分查询服务
userScoreReq := services.UserScoreRequest{}
userScoreReq.Users = make([]*services.UserInfo, 0, 5)
for i := 1; i <= 5; i++ {
userScoreReq.Users = append(userScoreReq.Users, &services.UserInfo{UserId: int32(i)})
}
userScoreRes, _ := userScoreClient.GetUserScore(ctx, &userScoreReq) // 等待获取所有的数据
fmt.Println("userScore---", userScoreRes.Users)
helper.go
package helper
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc/credentials"
"io/ioutil"
)
func GetClientCreds() credentials.TransportCredentials {
// TLS连接
//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("keys/client.pem", "keys/client.key")
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("keys/ca.pem")
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert}, //客户端证书
ServerName: "www.p-pp.cn", //注意这里的参数为配置文件中所允许的ServerName,也就是其中配置的DNS...
RootCAs: certPool,
})
return creds
}
启动服务端,再启动客户端之后就能在客户端看到打印的响应结果了:
由于服务端和客户端建立连接认证的代码都是一样的,因此在后面中只会给出核心部分代码,最终的代码在gitee上提供。
二、服务端流
stream关键字用于定义流,用其修饰在响应结果上,则表示使用的服务端流。
service UserScoreService{
rpc GetUserScoreByServerStream(UserScoreRequest) returns (stream UserScoreResponse); //服务端流
}
在UserService.go中:
// 服务端流
func (*UserScoreService) GetUserScoreByServerStream(req *UserScoreRequest, stream UserScoreService_GetUserScoreByServerStreamServer) error {
fmt.Println("请求用户积分的用户列表GetUserScoreByServerStream:", req.Users)
var score int32 = 100
users := make([]*UserInfo, 0)
for index, user := range req.Users {
user.UserScore = score
score++
users = append(users, user)
if index > 0 && (index+1)%2 == 0 {
err := stream.Send(&UserScoreResponse{Users: users})
if err != nil { //发送出错则终止
return err
}
users = (users)[0:0] //清空
}
time.Sleep(time.Second) // 模拟耗时
}
if len(users) > 0 { //末尾有数据
err := stream.Send(&UserScoreResponse{Users: users})
if err != nil { //发送出错则终止
return err
}
}
return nil
}
客户端部分:服务端是分批响应的,因此,客户端这边需要进行循环接收,接收到文件末尾表示接收结束。
// 服务端流
userScoreClient = services.NewUserScoreServiceClient(conn)
userScoreReq = services.UserScoreRequest{}
userScoreReq.Users = make([]*services.UserInfo, 0, 5)
for i := 6; i <= 10; i++ {
userScoreReq.Users = append(userScoreReq.Users, &services.UserInfo{UserId: int32(i)})
}
userScoreResByServerStream, err := userScoreClient.GetUserScoreByServerStream(ctx, &userScoreReq) // 客户端首先发起调用,将所有的请求数据传递给服务端
if err != nil {
log.Fatal(err)
}
for {
streamRes, err := userScoreResByServerStream.Recv() //服务端以流模式进行响应,因此,客户端进行循环接收
if err == io.EOF { //说明接收完了
break
}
if err != nil {
log.Fatal(err)
}
fmt.Println(streamRes.Users) //获取到数据,开启协程去处理其他事情
}
在服务端响应中,是每两条数据响应一次,因此最终的打印结果如下:
三、客户端流
客户端流是一个和服务端流相反的操作,在客户端发送时候会进行分批发送,服务端收集到所有请求数据之后再进行处理响应。
Users.proto
service UserScoreService{
rpc GetUserScoreByClientStream(stream UserScoreRequest) returns (UserScoreResponse); //客户端流
}
UserService.go
// 客户端流
func (*UserScoreService) GetUserScoreByClientStream(stream UserScoreService_GetUserScoreByClientStreamServer) error {
var score int32 = 100
users := make([]*UserInfo, 0)
for {
req, err := stream.Recv()
if err == io.EOF { // 接收完了
return stream.SendAndClose(&UserScoreResponse{Users: users})
}
if err != nil {
log.Fatal(err)
}
fmt.Println("客户端流GetUserScoreByClientStream------》", req.Users)
for _, user := range req.Users {
user.UserScore = score
score++
users = append(users, user)
}
}
return nil
}
客户端部分:
// 客户端流
userScoreClient = services.NewUserScoreServiceClient(conn)
userScoreResByClientStream, err := userScoreClient.GetUserScoreByClientStream(ctx) //客户端首先发起流调用,之后将数据分批发送给服务端
if err != nil {
log.Fatal(err)
}
var j int32 = 11
for i := 1; i <= 3; i++ {
userScoreReq = services.UserScoreRequest{}
userScoreReq.Users = make([]*services.UserInfo, 0)
MaxNum := j + 5
for ; j <= MaxNum; j++ {
userScoreReq.Users = append(userScoreReq.Users, &services.UserInfo{UserId: j})
}
err = userScoreResByClientStream.Send(&userScoreReq)
if err != nil {
log.Println(err)
}
}
streamRes1, err := userScoreResByClientStream.CloseAndRecv()
if err != nil {
log.Fatal(err)
}
fmt.Println(streamRes1.Users)
四、双向流
双向流模式中,客户端服务端都能进行部分发送和部分响应,客户端以消息流的形式发送请求到服务端,服务端也以流的形式进行响应。因此,双向流模式中,方法参数和返回参数都要申明为stream。
Users.proto
service UserScoreService{
rpc GetUserScoreByTwoStream(stream UserScoreRequest) returns (stream UserScoreResponse); //双向流模式
}
UserService.go
// 双向流
func (*UserScoreService) GetUserScoreByTwoStream(stream UserScoreService_GetUserScoreByTwoStreamServer) error {
var score int32 = 100
for {
req, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
fmt.Println("双向流流GetUserScoreByTwoStream------》", req.Users)
users := make([]*UserInfo, 0)
for _, user := range req.Users { //接收了数据之后可以开启协程去完成其他业务工作
user.UserScore = score
score++
users = append(users, user)
}
err = stream.Send(&UserScoreResponse{Users: users})
if err != nil {
log.Println(err) //暂时不结束,打印下日志
}
}
}
客户端部分:
//双向流模式
userScoreClient = services.NewUserScoreServiceClient(conn)
userScoreResByTwoStream, err := userScoreClient.GetUserScoreByTwoStream(ctx)
if err != nil {
log.Fatal(err)
}
var k int32 = 11
for i := 1; i <= 3; i++ {
userScoreReq = services.UserScoreRequest{}
userScoreReq.Users = make([]*services.UserInfo, 0)
MaxNum := k + 5
for ; k < MaxNum; k++ {
userScoreReq.Users = append(userScoreReq.Users, &services.UserInfo{UserId: k})
}
err := userScoreResByTwoStream.Send(&userScoreReq)
if err != nil {
log.Println(err)
}
twoStreamRes, err := userScoreResByTwoStream.Recv()
if err == io.EOF {
log.Println(err)
}
if err != nil {
log.Println(err)
}
fmt.Println("twoStreamRes", twoStreamRes.Users)
}
可以看到,客户端和服务端都是以流模式工作的,客户端发送一部分数据给服务端后,服务端立即做出请求处理及响应。