【gRPC】批量操作、服务端、客户端、双向流介绍及使用案例

5 篇文章 3 订阅
本文深入探讨gRPC的流传输模式,包括客户端流、服务端流和双向流。通过举例说明如何在服务端和客户端实现这些流模式,详细阐述了每个模式的工作原理和代码实现,展示了如何在gRPC中优化大数据包传输,减少带宽占用并提高处理效率。
摘要由CSDN通过智能技术生成

前言

在之前的文章中,我们通常的做法都是客户端请求—服务端响应的模式,客户端收集好所有的请求信息,发送到服务端,服务端对信息进行业务处理之后再返回最终响应结果。在更多的场景中,我们传输的数据包非常大,比如,客户端需要查询大量用户的积分,然后再拿着这堆用户的积分做其他处理,如果按照之前的做法,传输的数据包会非常大,这会占用大量的带宽,并且服务端需要等待客户端全部发送之后,才能进行处理及响应。在本文中,我们将介绍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)
	}

在这里插入图片描述
可以看到,客户端和服务端都是以流模式工作的,客户端发送一部分数据给服务端后,服务端立即做出请求处理及响应。

代码地址https://gitee.com/tonghuaing/g-rpc-stream

gRPC是一个高性能、开源的远程过程调用(RPC)框架,它由Google开发,支持多种语言。以下是构建gRPC服务端客户端的基本步骤: ### **服务端搭建**: 1. **定义.proto文件**: 创建一个.proto文件,定义服务和消息结构,类似这样: ```proto service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } ``` 2. **生成代码**: 使用`protoc`编译器将.proto文件转换成目标语言的代码,如`protoc greeter.proto -o greeter_pb.py`(Python示例)。 3. **编写服务端**: 实现服务端服务,并使用生成的库(如`GreeterServicer`),监听指定的端口,处理请求: ```python import greeter_pb2_grpc from greeter_pb2 import HelloRequest, HelloReply class Greeter(greeter_pb2_grpc.GreeterServicer): def SayHello(self, request, context): return HelloReply(message=f'Hello, {request.name}!') server = greeter_pb2_grpc.add_GreeterServicer_to_server(Greeter(), ...) server.start() ``` 4. **启动服务器**: 运行服务端程序,监听并接受连接。 ### **客户端搭建**: 1. **再次生成代码**: 如果需要,也需要为客户端生成代码,例如: ```bash protoc greeter.proto --python_out=. ``` 2. **创建客户端**: 导入生成的客户端模块,创建客户端实例并调用服务: ```python import greeter_pb2 import greeter_pb2_grpc def main(): channel = grpc.insecure_channel('localhost:50051') stub = greeter_pb2_grpc.GreeterStub(channel) response = stub.SayHello(HelloRequest(name='World')) print(response.message) if __name__ == '__main__': main() ``` ### **相关问题--:** 1. gRPC支持哪些编程语言? 2. 如何设置gRPC的安全性? 3. gRPC服务和HTTP RESTful API的主要区别是什么?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

童话ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值