云原生时代崛起的编程语言Go远程调用gRPC实战

news2024/11/25 19:11:08

文章目录

  • 概述
    • 定义
    • 背景
    • 特点
    • 四种服务方法
  • 实战
    • 环境配置
    • proto文件
    • 简单RPC
    • Token认证
    • 服务器流式RPC
    • 客户端流式RPC
    • 双向流式RPC

概述

定义

gRPC 官网地址 https://grpc.io/ 源码release最新版本v1.55.1

gRPC 官网文档地址 https://grpc.io/docs/

gRPC 源码地址 https://github.com/grpc/grpc

gRPC是一个现代的开源高性能远程过程调用(RPC)框架,可以在任何环境中运行。它可以高效地连接数据中心内和跨数据中心的服务,支持负载平衡、跟踪、运行状况检查和身份验证;同时也是一个CNCF孵化项目。

简单说gRPC是基于tcp协议使用http2.0,采用Protocol Buffers定义接口,因而相对于传统的Restful API来说,速度更快,数据更小,接口要求更严谨。

背景

在当前分布式和微服务主宰时代,服务拆分后服务与服务之间的通信就是进程与进程或服务器与服务器之间的调用,或许你马上就说可以采用http,http虽然便捷方便,但性能较低,这时间我们可以采用RPC(Remote Procredure Call)来实现,通过自定义协议发起TCP调用来提高传输效率;

RPC是一款语言中立、平台中立、开源的远程过程调用技术,客户端和服务端可以在多种环境中运行和交互,客户端和服务端可以采用不同语言开发。数据在进行网络传输的时候需要先进行序列化,而序列化协议有很多种,比如XML、Json、Thrift、Avro、Hessian、Kryo、Protocol Buffers、ProtoStuff。

  • 序列化(serialization)就是将对象序列化为二进制形式(字节数组),一般也将序列化称为编码(Encode),主要用于网络传输、数据持久化等;
  • 反序列化(deserialization)则是将从网络、磁盘等读取的字节数组还原成原始对象,以便后续业务的进行,一般也将反序列化称为解码(Decode),主要用于网络传输对象的解码,以便完成远程调用。

原生rpc(在go标准库net/rpc包下)编写相对复杂,需要自己去关注实现过程,没有代码提示。因此更多会使用gRPC。在gRPC中,客户机应用程序可以直接调用不同机器上的服务器应用程序上的方法,就像它是本地对象一样,使得更容易创建分布式应用程序和服务。gRPC基于定义服务的思想,指定可以远程调用的方法及其参数和返回类型。在服务器端,服务器实现这个接口,并运行gRPC服务器来处理客户端调用。在客户端,客户端有一个提供相同方法的存根(在某些语言中仅称为客户端)。gRPC客户端和服务器可以在各种环境中运行并相互通信——从Google内部的服务器到您自己的桌面——并且可以用任何gRPC支持的语言编写;如可以轻松地用Java创建gRPC服务器,用Go、Python或Ruby创建客户端。

image-20230612104248859

特点

  • 简单服务定义:使用Protocol Buffers(一种强大的二进制序列化工具集和语言)定义服务,设计一个新的协议,需要准确,高效和语言独立。
  • 低延迟、高可扩展、分布式系统:快速开始并扩大规模,用一行代码安装运行时和开发环境,还可以使用框架扩展到每秒数百万个rpc。序列化后体积小,序列化和反序列化速度快。
  • 跨语言和平台工作:以各种语言和平台为您的服务自动生成惯用的客户机和服务器存根.开发与云服务器通信的移动客户端。
  • 双向流和集成验证:双向流和完全集成的基于HTTP/2传输的可插拔身份验证。
  • 分层设计:支持扩展。身份验证、负载平衡、日志记录和监控等

四种服务方法

proto中rpc业务实际上是一个函数,由服务端重写(overwrite)的函数,根据rpc函数的入参和出参简单分为普通RPC、服务端流RPC、客户端流RPC、双端流RPC。

  • 一元rpc:其中客户端向服务器发送单个请求并获得单个响应,就像普通的函数调用一样。
    • 一旦客户机调用了存根方法,服务器就会收到RPC调用的通知,其中包含该调用的客户机元数据、方法名称和指定的截止日期(如果适用)。
    • 然后,服务器可以直接发送回自己的初始元数据(必须在任何响应之前发送),或者等待客户端的请求消息。首先发生的是特定于应用程序的。
    • 一旦服务器获得了客户机的请求消息,它就会执行创建和填充响应所需的任何工作。然后将响应(如果成功)连同状态详细信息(状态代码和可选状态消息)以及可选的尾随元数据一起返回给客户机。
    • 如果响应状态为OK,则客户机将获得响应,从而完成客户机端的调用。
rpc SayHello(HelloRequest) returns (HelloResponse);
  • 服务器流式rpc:其中客户端向服务器发送请求并获得读取消息序列的流。客户端从返回的流中读取,直到没有更多的消息。gRPC保证了单个RPC调用中的消息排序。服务器流RPC类似于一元RPC,不同之处在于服务器在响应客户端的请求时返回消息流。发送完所有消息后,服务器的状态详细信息(状态码和可选状态消息)和可选的尾随元数据被发送到客户端。这就完成了服务器端的处理。客户机在拥有所有服务器消息后完成。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
  • 客户端流式rpc:其中客户端写入消息序列并将它们发送到服务器,同样使用提供的流。一旦客户机完成了消息的写入,它将等待服务器读取消息并返回其响应。gRPC再次保证了单个RPC调用中的消息排序。客户端流式RPC类似于一元RPC,不同之处是客户端向服务器发送消息流而不是单个消息。服务器用一条消息(连同它的状态详细信息和可选的尾随元数据)进行响应,通常是在它接收到所有客户机的消息之后,但不一定是这样。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  • 双向流式rpc:其中双方使用读写流发送消息序列。这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序进行读写:例如,服务器可以等待接收到所有客户端消息后再写入响应,或者它可以交替地读取消息然后写入消息,或者其他一些读写组合,保留每个流中的消息顺序。在双向流RPC中,调用由调用方法的客户机和接收客户机元数据、方法名称和截止日期的服务器发起。服务器可以选择发回其初始元数据,或者等待客户端开始流式传输消息。客户端和服务器端流处理是特定于应用程序的。由于这两个流是独立的,客户端和服务器可以以任何顺序读写消息。例如,服务器可以等到接收到客户端的所有消息后再写入消息,或者服务器收到请求,然后发回响应,然后客户端根据响应发送另一个请求,以此类推。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

实战

环境配置

  • 安装Protobuf
# 下载最新版本23.2的protoc,这个是protobuf代码生成工具,通过proto文件生成对应的代码,根据自己操作系统下载相应文件,这里以windows 64位系统为例
wget https://github.com/protocolbuffers/protobuf/releases/download/v23.2/protoc-23.2-win64.zip
# 解压并放在windows本地目录,并配置在Path路径下如D:\Program Files\protoc-23.2-win64\bin,在windows下命令行执行protoc --version检查是否安装配置正确

image-20230612134600000

  • 使用go get获取grpc的官方软件包
# 创建go项目grpc-demo,在GoLand IDE编写,并通过下面命令安装grpc核心库protoc,可以GoLand IDE安装protoc插件,实现语法高亮
go get google.golang.org/grpc

image-20230612154210430

  • 安装go的protoc代码生成工具。上面安装的是protocol编译器,它可以生成多种开发语言代码,这里使用go语言的工具是protoc-gen-go
# 在实际开发中最好指定具体的版本,这里是演示使用就直接用latest,在命令行中执行下面两条命令
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

在GOPATH目录下的bin目录就已经有刚刚安装的两个文件

image-20230612142741106

proto文件

默认情况下gRPC使用Protocol Buffers(尽管它可以与JSON等其他数据格式一起使用),Protocol Buffers是Google公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议。使用协议缓冲区时的第一步是定义要在proto文件中序列化的数据的结构:这是一个扩展名为.proto的普通文本文件。协议缓冲区数据被结构化为消息,其中每个消息都是包含一系列称为字段的名称-值对的信息的小逻辑记录。在普通的原型文件中定义gRPC服务,使用RPC方法参数和返回类型指定为协议缓冲区消息。一般来说,虽然可以使用proto2(当前默认协议缓冲区版本),但建议将proto3与gRPC一起使用,因为它允许您使用所有gRPC支持的语言,并且避免了proto2客户端与proto3服务器通信的兼容性问题。

  • message:用于在protobuf中定义消息类型,而消息就是需要进行传输的数据格式,类似于go中的struct,在消息中的数据字段由字段类型、字段名称、消息号,一个proto文件中可以定义多个消息类型,也即是多服务。
  • 消息号:在消息体的定义中,每个字段都必须有一个唯一的标识号,范围是[1,2^29-1]
  • 字段规则
    • required:默认规则,消息体中必填字段,不设置会导致编码解码的异常
    • optional:消息体中可选字段
    • repeated:消息体中可重复字段,重复的值的顺序会被保留,在go中重复的字段会定义为切片类型。
  • 字段映射

image-20230612145023008

  • 默认值

image-20230612145124895

  • 嵌套消息:可以在其它消息类型中定义,使用消息类型,也可以使用外部定义的消息。
  • import使用:import用于导入其它的proto文件。也即是类似有common.proto、user.proto、order.proto,在user.proto、order.proto可以通过import common.proto实现多proto文件之类引用管理。
  • any任意类型:需要导入any.proto,属性使用google.protobuf.Any定义

简单RPC

简单RPC也叫一元RPC,其中客户端向服务器发送单个请求并获得单个响应,就像普通的函数调用一样。在go项目的src目录下创建simple目录,在simple创建proto目录,在创建user.proto

// 语法版本,指定使用proto3
syntax = "proto3";

// 指定生成的go_package,生成的go代码使用什么包package proto
option go_package = ".;proto";

// 服务定义,此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
service UserService {
  // 远程调用方法定义
  rpc GetUser(UserRequest) returns (UserResponse) {}
}

// 包含用户编号的请求消息
message UserRequest {
  // 每个字典=最后序号1为唯一的标识号,必填
  int32 id = 1;
}

// 包含消息内容的响应消息
message UserResponse {
  int32 id = 1;
  string name = 2;
}

在命令行中执行如下操作用于生成go的代码文件

# 进入simple目录
cd src/simple
# 执行protoc命令
protoc --go_out=. --go-grpc_out=. proto/user.proto

如果client和server不在同一工程项目,proto/user.pb.go和proto/user_grpc.pb.go需要都复制到对应client和server相应项目下,这里先以此例说明。执行后生成proto/user.pb.go和proto/user_grpc.pb.go两个文件,其中包含用于填充、序列化和检索UserRequest和UserResponse消息类型的代码,生成客户机和服务器代码。

image-20230612155505970

  • 服务端流程及代码

    • 创建gRPC Server对象,可以简单理解为Server端抽象对象。
    • 将server(其包含被调用的服务端接口)注册到gRPC Server的内部注册中心,这样可以在接收到请求时,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理。
    • 创建Listen,监听TCP端口。
    • gRPC Server开始Listen、Accept,直到Stop。

    将proto文件下的user.pb.go和user_grpc.pb.go复制一份放在src/simple/server目录,在src/simple/server创建main.go文件,内容如下

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"grpc-demo/src/simple/proto"
	"net"
)

type server struct {
	proto.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *proto.UserRequest) (*proto.UserResponse, error) {
	// 服务端接口实现的业务逻辑
	fmt.Println("client端远程调用成功......, 当前请求传入id参数为", req.GetId())
	return &proto.UserResponse{
		Id:   req.GetId(),
		Name: "itxiaoshen",
	}, nil
}

func main() {
	//1. 开启端口
	listen, _ := net.Listen("tcp", ":7070")

	//2. 创建grpc服务
	grpcServer := grpc.NewServer()

	//3. 将编写好的服务注册到grpc
	proto.RegisterUserServiceServer(grpcServer, &server{})

	//4. 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		fmt.Printf("failed to server: %v", err)
		return
	}
}
  • 客户端流程及代码
    • 创建与给定目标(服务端)的连接交互。
    • 创建对应Client对象。
    • 发送RPC请求,等待同步响应,得到回调后返回响应结果。
    • 输出响应结果。

将proto文件下的user.pb.go和user_grpc.pb.go复制一份放在src/simple/client目录,在src/simple/client创建main.go文件,内容如下

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-demo/src/simple/server/proto"
	"log"
)

func main() {
	//1. 与Server建立连接,此处禁用安全传输,这里没有使用加密验证
	conn, err := grpc.Dial("127.0.0.1:7070", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close() //延时关闭连接

	//2. 与对应服务建立连接
	client := pb.NewUserServiceClient(conn)

	//3. 执行grpc调用
	resp, _ := client.GetUser(context.Background(), &pb.UserRequest{Id: 1})
	fmt.Println("client get user,id=", resp.GetId(), ",name=", resp.GetName())
}

启动server和client,客户端正确返回结果,服务端也打印请求日志。

image-20230612163637962

Token认证

gRPC提供了一个接口PerRPCCredentials,接口位于credentials包下,接口中有两个方法,方法需要由客户端来实现

image-20230612171753868

client的main.go

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-demo/src/simple/server/proto"
	"log"
)

type ClientTokenAuth struct {
}

func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"appId":  "itxs",
		"appKey": "11223344",
	}, nil
}

func (c ClientTokenAuth) RequireTransportSecurity() bool {
	return false
}

func main() {
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) //这里我们不使用TLS,因此这里传入空
	opts = append(opts, grpc.WithPerRPCCredentials(new(ClientTokenAuth)))         //传入我们自定义的验证方式【Token】
	conn, err := grpc.Dial("127.0.0.1:7070", opts...)

	//1. 与Server建立连接,此处禁用安全传输,这里没有使用加密验证
	//conn, err := grpc.Dial("127.0.0.1:7070", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close() //延时关闭连接

	//2. 与对应服务建立连接
	client := pb.NewUserServiceClient(conn)

	//3. 执行grpc调用
	resp, err := client.GetUser(context.Background(), &pb.UserRequest{Id: 1})
	if err != nil {
		fmt.Println("client get user error=", err.Error())
	} else {
		fmt.Println("client get user,id=", resp.GetId(), ",name=", resp.GetName())
	}

}

server的main.go

package main

import (
	"context"
	"errors"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"grpc-demo/src/simple/proto"
	"net"
)

type server struct {
	proto.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *proto.UserRequest) (*proto.UserResponse, error) {
	fmt.Println("接收client端远程调用请求")
	//获取客户端传入的元数据信息
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("未传输token")
	}
	var appId string
	var appKey string
	if v, ok := md["appid"]; ok {
		appId = v[0]
	}
	if v, ok := md["appkey"]; ok {
		appKey = v[0]
	}
	if appId != "itxs" || appKey != "11223344" {
		fmt.Println("token 不正确")
		return nil, errors.New("token 不正确")
	}
	fmt.Println("token 验证正确")
	// 服务端接口实现的业务逻辑
	fmt.Println("client端远程调用成功......, 当前请求传入id参数为", req.GetId())
	return &proto.UserResponse{
		Id:   req.GetId(),
		Name: "itxiaoshen",
	}, nil
}

func main() {
	//1. 开启端口
	listen, _ := net.Listen("tcp", ":7070")

	//2. 创建grpc服务
	grpcServer := grpc.NewServer()

	//3. 将编写好的服务注册到grpc
	proto.RegisterUserServiceServer(grpcServer, &server{})

	//4. 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		fmt.Printf("failed to server: %v", err)
		return
	}
}

启动server和client,返回正确的结果,反之如果客户端输入不正确appId或appKey则会返回token 不正确。

image-20230612173919338

gRPC将各种认证方式浓缩到一个凭证(credentials)上,可以单独使用一种拼争,比如只使用TLS或者只使用自定义凭证,也可以多种凭证组合,gRPC提供统一的gRPC验证机制,使得研发人员使用方便,这也是gRPC设计的巧妙之处。

服务器流式RPC

服务器流式RPC这里使用文件下载的案例来演示,创建file.proto文件

// 语法版本,指定使用proto3
syntax = "proto3";

// 指定生成的go_package,生成的go代码使用什么包package proto
option go_package = "./proto;proto";

// 服务定义,此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
service FileService {
  // 文件下载远程调用方法定义
  rpc DownLoad(FileRequest) returns (stream FileResponse) {}
}

// 包含文件名的文件请求消息
message FileRequest {
  // 每个字典=最后序号1为唯一的标识号,必填
  string name = 1;
}

// 包含消息内容的响应消息
message FileResponse {
  string name = 1;
  bytes content = 2;
}

在命令行中执行如下操作用于生成go的代码文件

# 进入simple目录
cd src/stream
# 执行protoc命令
protoc --go_out=. --go-grpc_out=. proto/file.proto

创建server_stream_server.go实现服务端文件下载

package main

import (
	"fmt"
	"google.golang.org/grpc"
	"grpc-demo/src/stream/proto"
	"io"
	"log"
	"net"
	"os"
)

type FileService struct {
	proto.UnimplementedFileServiceServer
}

func (FileService) DownLoad(req *proto.FileRequest, stream proto.FileService_DownLoadServer) error {
	fmt.Println(req)
	file, err := os.Open("src\\stream\\static\\winutils-master.zip")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	for {
		buf := make([]byte, 1024)
		_, err = file.Read(buf)
		if err == io.EOF {
			break
		}

		if err != nil {
			panic(err)
		}
		stream.Send(&proto.FileResponse{
			Content: buf,
		})
	}
	return nil
}

func main() {
	listen, _ := net.Listen("tcp", ":7070")
	// 创建grpc服务
	grpcServer := grpc.NewServer()
	// 注册服务
	proto.RegisterFileServiceServer(grpcServer, &FileService{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

创建server_stream_client.go实现客户端文件下载

package main

import (
	"bufio"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"grpc-demo/src/stream/proto"
	"io"
	"log"
	"os"
)

func main() {
	conn, err := grpc.Dial("127.0.0.1:7070", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("连接失败:", err)
		return
	}
	defer conn.Close()

	// 建立连接
	client := proto.NewFileServiceClient(conn)
	// 执行rpc调用
	serverStream, err := client.DownLoad(context.Background(), &proto.FileRequest{Name: "hello.zip"})
	if err != nil {
		log.Fatalln("获取流出错", err)
	}

	file, err := os.OpenFile("winutils-master-new.zip", os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	writer := bufio.NewWriter(file)

	for {
		resp, err := serverStream.Recv()
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端数据接收完成")
				err := serverStream.CloseSend()
				if err != nil {
					log.Fatal(err)
				}
				break
			}
		}
		writer.Write(resp.Content)
	}
	writer.Flush()
}

运行服务端和客户端,最终按照预期下载文件

image-20230613174021785

客户端流式RPC

客户端流式RPC这里使用文件上传的案例来演示,修改file.proto文件,增加UpFileService相关内容

// 语法版本,指定使用proto3
syntax = "proto3";

// 指定生成的go_package,生成的go代码使用什么包package proto
option go_package = "./proto;proto";

// 服务定义,此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
service FileService {
  // 文件下载远程调用方法定义
  rpc DownLoad(FileRequest) returns (stream FileResponse) {}
}

// 服务定义,此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
service UpFileService {
  // 文件下载远程调用方法定义
  rpc UpLoad(stream UpFileRequest) returns (UpFileResponse) {}
}

// 包含文件名的文件请求消息
message FileRequest {
  // 每个字典=最后序号1为唯一的标识号,必填
  string name = 1;
}

// 包含消息内容的响应消息
message FileResponse {
  string name = 1;
  bytes content = 2;
}

// 包含文件名的文件请求消息
message UpFileRequest {
  string name = 1;
  bytes content = 2;
}

// 包含文件名的文件请求消息
message UpFileResponse {
  string state = 1;
}

在命令行中执行如下操作用于生成go的代码文件

# 进入simple目录
cd src/stream
# 执行protoc命令
protoc --go_out=. --go-grpc_out=. proto/file.proto

创建client_stream_server.go实现服务端文件下载

package main

import (
	"bufio"
	"google.golang.org/grpc"
	"grpc-demo/src/stream/proto"
	"io"
	"log"
	"net"
	"os"
)

type UpFileService struct {
	proto.UnimplementedUpFileServiceServer
}

func (UpFileService) UpLoad(stream proto.UpFileService_UpLoadServer) error {
	file, err := os.OpenFile("src/stream/static/apache-maven-3.8.6-bin-new.zip", os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		panic(err)
	}
	defer file.Close()

	writer := bufio.NewWriter(file)

	for {
		resp, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
		}
		writer.Write(resp.Content)
	}

	writer.Flush()

	stream.SendAndClose(&proto.UpFileResponse{
		State: "success",
	})

	return nil
}

func main() {
	listen, _ := net.Listen("tcp", ":7070")
	// 创建grpc服务
	grpcServer := grpc.NewServer()
	// 注册服务
	proto.RegisterUpFileServiceServer(grpcServer, &UpFileService{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

创建client_stream_client.go实现客户端文件下载

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"grpc-demo/src/stream/proto"
	"io"
	"log"
	"os"
)

func main() {
	conn, err := grpc.Dial("127.0.0.1:7070", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("连接失败:", err)
		return
	}
	defer conn.Close()

	// 建立连接
	client := proto.NewUpFileServiceClient(conn)
	// 执行rpc调用
	clientStream, err := client.UpLoad(context.Background())
	if err != nil {
		log.Fatalln("获取流出错", err)
	}

	file, err := os.Open("g:\\apache-maven-3.8.6-bin.zip")
	if err != nil {
		log.Fatalln(err)
	}
	defer file.Close()

	for {
		buf := make([]byte, 1024)
		_, err = file.Read(buf)
		if err == io.EOF {
			break
		}

		if err != nil {
			panic(err)
		}
		clientStream.Send(&proto.UpFileRequest{
			Name:    "apache-maven-3.8.6-bin.zip",
			Content: buf,
		})
	}

	resp, err := clientStream.CloseAndRecv()
	fmt.Println(resp, err)
}

运行服务端和客户端,最终按照预期上传文件

image-20230614104240139

双向流式RPC

双向流式RPC这里类似聊天的场景,双方可以随时收发,修改file.proto文件,增加ChatService相关内容

// 语法版本,指定使用proto3
syntax = "proto3";

// 指定生成的go_package,生成的go代码使用什么包package proto
option go_package = "./proto;proto";

// 服务定义,此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
service FileService {
  // 文件下载远程调用方法定义
  rpc DownLoad(FileRequest) returns (stream FileResponse) {}
}

// 服务定义,此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
service UpFileService {
  // 文件下载远程调用方法定义
  rpc UpLoad(stream UpFileRequest) returns (UpFileResponse) {}
}

service ChatService {
  // 文件下载远程调用方法定义,文字聊天
  rpc TextChat(stream TextRequest) returns (stream TextResponse) {}
}

// 包含文件名的文件请求消息
message FileRequest {
  // 每个字典=最后序号1为唯一的标识号,必填
  string name = 1;
}

// 包含消息内容的响应消息
message FileResponse {
  string name = 1;
  bytes content = 2;
}

// 包含文件名的文件请求消息
message UpFileRequest {
  string name = 1;
  bytes content = 2;
}

// 包含文件名的文件请求消息
message UpFileResponse {
  string state = 1;
}

// 包含文件名的文件请求消息
message TextRequest {
  // 每个字典=最后序号1为唯一的标识号,必填
  string message = 1;
}

// 包含消息内容的响应消息
message TextResponse {
  string message = 1;
}

在命令行中执行如下操作用于生成go的代码文件

# 进入simple目录
cd src/stream
# 执行protoc命令
protoc --go_out=. --go-grpc_out=. proto/file.proto

创建both_stream_server.go实现服务端文件下载

package main

import (
	"fmt"
	"google.golang.org/grpc"
	"grpc-demo/src/stream/proto"
	"log"
	"net"
)

type ChatService struct {
	proto.UnimplementedChatServiceServer
}

func (ChatService) TextChat(stream proto.ChatService_TextChatServer) error {
	for i := 0; i < 10; i++ {
		req, _ := stream.Recv()
		fmt.Println(req)
		stream.Send(&proto.TextResponse{
			Message: fmt.Sprintf("server send world to client!i=%d", i),
		})
	}
	return nil
}

func main() {
	listen, _ := net.Listen("tcp", ":7070")
	// 创建grpc服务
	grpcServer := grpc.NewServer()
	// 注册服务
	proto.RegisterChatServiceServer(grpcServer, &ChatService{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

创建both_stream_client.go实现客户端文件下载

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"grpc-demo/src/stream/proto"
	"log"
)

func main() {
	conn, err := grpc.Dial("127.0.0.1:7070", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("连接失败:", err)
		return
	}
	defer conn.Close()

	// 建立连接
	client := proto.NewChatServiceClient(conn)
	// 执行rpc调用
	stream, err := client.TextChat(context.Background())
	if err != nil {
		log.Fatalln("获取流出错", err)
	}

	for i := 0; i < 10; i++ {
		stream.SendMsg(&proto.TextRequest{
			Message: fmt.Sprintf("client send hello to server!i=%d", i),
		})
		resp, err := stream.Recv()
		fmt.Println(resp, err)
	}

}

运行服务端和客户端,最终按照预期实现双方文字聊天消息发送

image-20230614112111468

  • 本人博客网站IT小神 www.itxiaoshen.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/647542.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【vue3】13-前端路由-Vue-Router的详解: 从入门到掌握

前端路由 前端路由的发展历程认识前端路由&#xff1a;后端路由阶段前后端分离阶段 Vue-Router的使用vue-router的基本使用vue-router的使用过程基本使用的细节补充 路由懒加载分包处理路由的其他属性动态路由路由嵌套路由的编程式导航动态管理路由对象路由导航守卫钩子 前端路…

【毕业N年系列】 毕业第五年

距离上一篇 【毕业N年系列】已经过去一年时间了。没想到自己当初的一个简单的想法竟然能坚持到第五年&#xff0c;希望自己能坚持下去吧&#xff0c;在耄耋古稀之年依然能回看自己的人生。当然本来想着今天能早点下班回来写这篇文章&#xff0c;显然又没有实现&#xff0c;加班…

termux安卓手机 无线adb调试安卓手机

调试机安转termux app Apk下载 安装 https://f-droid.org/packages/com.termux 打开termux 挂载内部存储到~/storage 目录 $ temux-setup-storage 换源加快下载与更新速度 termux-change-repo 选 Main 然后 清华 下载基础软件 pkg install git vim perl nodejs-lts wget…

竞赛无人机搭积木式编程(三)---用户自定义航点自动飞行功能(全局定位,指哪打哪)

竞赛无人机搭积木式编程&#xff08;三&#xff09;---用户自定义航点自动飞行功能&#xff08;全局定位&#xff0c;指哪打哪&#xff09; 无名小哥 2023年6月10日 用户通过对前面两讲中全国大学生电子设计竞赛真题植保无人机&#xff08;2021&#xff09;、送货无人机&#…

新人如何学习性能测试?一文5个步骤带你入门性能测试!

写在前面&#xff1a;测试岗位细化后分为很多&#xff0c;功能测试、性能测试、自动化测试、测试开发&#xff0c;这些都叫测试。性能测试在所有测试分工中&#xff0c;地位和薪资都比较高&#xff0c;可见下方图片&#xff1a; 中级第3条&#xff0c;高级第1条&#xff0c;都是…

深度学习入门——感知机

1 感知机是什么 感知机接收多个输入信号&#xff0c;输出一个信号。这里所说的“信号”可以想象成电流或河流那样具备“流动性”的东西。像电流流过导线&#xff0c;向前方输送电子一样&#xff0c;感知机的信号也会形成流&#xff0c;向前方输送信息。但是&#xff0c;和实际…

Java集合-Collection Map

文章目录 概念IterableCollection接口List接口ArrayListVectorLinkedListArrayList 和 LinkedList 比较 SetHashSet LinkedHashSetMapHashMap 概念 1.集合主要是两组:单列集合(Collection) , 双列集合(Map) 2.Collection 接口有两个重要的子接口 List ,Set . 他们的实现子类都…

SWIG介绍和使用

官网&#xff1a;https://www.swig.org/ github&#xff1a;https://github.com/swig SWIG 是一种软件开发工具&#xff0c;可将用 C 和 C 编写的程序与各种高级编程语言连接起来。 SWIG 与不同类型的目标语言一起使用&#xff0c;包括常见的脚本语言&#xff0c;如 Javascri…

redis集群-Redis(六)

上篇文章介绍了哨兵架构以及集群搭建。 哨兵架构&redisCluster-Redis&#xff08;五&#xff09;https://blog.csdn.net/ke1ying/article/details/131178880 redisCluster 接着上篇文章继续讲解&#xff0c;前面一个linux服务器已经有了8001和8004节点&#xff0c;现在继…

八大典型的黑盒测试方法已来袭,快快接住!

前言 有了软件缺陷的暴露&#xff0c;我们就需要通过各种软件测试的方法来查找出软件的漏洞&#xff0c;编写出测试用例&#xff0c;及时修改bug。 在下面的这篇文章中&#xff0c;我们将谈论八大典型的黑盒测试方法&#xff0c;一起来学习⑧&#x1f4a1; 一、等价类划分法…

【Python实战】Python采集王者皮肤图片

前言 我们上一篇介绍了&#xff0c;如何采集王者最低战力&#xff0c;本文就来给大家介绍如何采集王者皮肤&#xff0c;买不起皮肤&#xff0c;当个桌面壁纸挺好的。下面&#xff0c;我和大家介绍如何获取数据。 环境使用 python 3.9pycharm 模块使用 requests 模块介绍 re…

c++返回值使用引用的注意事项

c返回值使用引用的注意事项 问题原因返回函数的引用形参另外一个问题解决方法 问题 引用变量b接受到的值为无意义的数字。 原因 int& b returnYingyong();相当于&#xff1a; int i2 3; int& b i2; 销毁 i2;将局部变量作为返回值&#xff0c;导致引用b数据无意义…

【LInux】进程间通信(共享内存)

文章目录 system V共享内存shmget&#xff08;创建共享内存&#xff09;ftok&#xff08;key&#xff09;ipcs指令shmctl&#xff08;销毁共享内存&#xff09;shmat代码 共享内存 system V共享内存 共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间&am…

新能源突围,需要发力被忽视的中端品牌战场

作者 |张祥威 编辑 |德新 今年2月&#xff0c;受特斯拉等竞品降价影响&#xff0c;深蓝SL03销量下滑至4000台。深蓝的市场部上报长安中国&#xff0c;十天后开始降价&#xff0c;3月SL03销量重回正轨。 在巨头和新势力之间灵活腾挪&#xff0c;深蓝向外界展示了爆款制造的素质…

网络安全知识点梳理(期末不挂科)

网络安全复习知识点太多梳理不清晰&#xff1f;&#x1f44f;&#x1f3fb;&#x1f44f;&#x1f3fb;&#x1f44f;&#x1f3fb; 本文一篇帮你梳理清晰&#xff0c;内容覆盖整个大学网络安全知识点&#x1f44c;&#x1f3fb; 干货满满不看后悔&#x1f44d;&#x1f44…

leetcode 309.最佳买卖股票时机含冷冻期

题目描述 给定一个整数数组prices&#xff0c;其中第 prices[i] 表示第 i 天的股票价格 。​ 设计一个算法计算出最大利润。在满足以下约束条件下&#xff0c;你可以尽可能地完成更多的交易&#xff08;多次买卖一支股票&#xff09;: 卖出股票后&#xff0c;你无法在第二天买入…

基于深度学习的高精度足球检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度足球检测识别系统可用于日常生活中或野外来检测与定位足球目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的足球目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型…

【Java基础学习打卡09】JRE与JDK

目录 引言一、JRE二、JDK三、JDK、JRE和JVM关系总结 引言 本文将介绍JRE、JDK是什么&#xff0c;以及JDK、JRE和JVM关系三者之间的关系。 一、JRE JRE全称为Java Runtime Environment&#xff0c;是Java应用程序的运行时环境。JRE包括Java虚拟机&#xff08;JVM&#xff09;、…

【数据结构】二叉树经典题目

1. 二叉树创建字符串 相信大部分人看了题目描述之后&#xff0c;都会和我一样一脸的懵逼。直到我看到了一个描述才恍然大悟 分为3种情况&#xff1a; 左右都为空 --省略右为空&#xff0c;左不为空 – 省略左为空&#xff0c;右不为空–不省略 这里复习一下二叉树的前序遍历…

Linux基本指令(四)

文章目录 一、新建(adduser)与删除(userdel)普通用户二、date指令三、find指令四、grep指令 一、新建(adduser)与删除(userdel)普通用户 前面我Linux登录时都是以root的身份登录的&#xff0c;从现在开始以普同用户登录&#xff0c;那么普通用户哪里来&#xff0c;是由root用户…