grpc介绍及go使用grpc详解

常用网站

grpc官网:https://grpc.io/

grpc中文文档:https://doc.oschina.net/grpc

grpc github地址:https://github.com/grpc/grpc

grpc-go github地址:https://github.com/grpc/grpc-go

grpc介绍

微服务

单体架构缺点:

  • 一旦某个服务宕机,会引起整个应用不可用,隔离性差
  • 只能整体进行伸缩,浪费资源,可伸缩性差
  • 代码耦合度高,可维护性差

微服务架构解决了单体架构的弊端,但是产生了新的问题:

  • 代码冗余
  • 服务和服务之间的调用关系复杂,安全性降低

服务拆分后,服务与服务之间发生的是进程与进程之间的相互调用,服务器与服务器之间的调用就需要发起网络调用,在网络调用中我们能立马想起的就是http,但在微服务架构中,http虽然方便快捷,但是性能较低,这就是就需要引入RPC(远程过程调用),通过自定义协议发起TCP调用来加快传输效率。

RPC(Remote Procedure Call 远程过程调用),是一种协议,是用来屏蔽分布式计算中的各种调用细节,使得你可以像是本地调用一样直接调用一个远程的函数。

客户端与服务端沟通的过程

  1. 客户端发送数据(以字节流的方式)
  2. 服务端接收数据并解析,根据业务执行相应的逻辑处理,然后把结果返回给客户

RPC:

  1. RPC就是将上述过程封装,优化操作
  2. 使用一些大家都认可的协议进行规范化处理
  3. 做成一些框架,产生利益

gRPC

gRPC是一个高性能的、开源的通用的RPC框架

在gRPC中,我们称调用方为client客户端,被调用方为server服务端,与其它的RPC框架相同,gRPC也是基于“服务定义”的思想,简单来说,就是我们通过某张方式来描述一个服务,这种描述方式是语言无关的,在这个“服务定义”的过程中,我们描述了我们提供的服务名是什么,有哪些方法可以被调用,这些方法有什么样的参数,有什么样的返回值。

也就是说,在定义好这些服务、方法之后,gRPC会屏蔽底层的细节,client只需要直接调用定义好的方法,就能拿到预期的返回结果,对于server来说,还需要实现我们定义的方法,同样的,gRPC也会帮我们屏蔽底层的细节,我们只需要实现定义好的方法的具体逻辑即可。

你可以发现,在上面的描述过程中,所谓的“服务定义”,就跟定义接口的语义是很接近的。我们更愿意理解为这是一种“约定”,双方约定好接口,然后server实现这个接口,client调用这个接口的代理对象,至于其它的细节,交给gRPC。

此外,gRPC还是语言无关的,你可以用C++作为server,golang、java等作为client,为了实现这一点,我们在“定义服务”和在编码和解码的过程中,应该是做到语言无关的。

在这里插入图片描述

因此,gRPC使用了Protocol Buffers,这是谷歌开源的一套成熟的数据结构序列化机制

可以把它当成一个代码生成工具以及序列化工具,这个工具可以把我们定义的方法,转换成特定语言的代码,比如你定义了一种类型的参数,它会帮你转换成golang中的struct结构体,你定义的方法,它会帮你转换成func函数,此外,在发送请求和接收响应的时候,这个工具还会完成对应的编码和解码的工作,将你即将发送的数据编码成grpc能够传输的形式,又或者将即将接收到的数据解码为编程语言能够理解的数据格式。

序列化:将数据结构或对象转换成二进制串的过程

反序列化:将序列化产生的二进制串转换成数据结构或对象的过程

protobuf是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景,因为protobuf是二进制数据格式,需要编码和解码,数据本身不具有可读性,因此只能反序列化后得到真正刻度的数据

优势:

  1. 序列化后体积相比json和xml很小,适合网络传输
  2. 支持跨平台多语言
  3. 消息格式升级和兼容性较好
  4. 序列化和反序列化的速度快

go的序列化和反序列化的代码:

package main

import (
	"fmt"
	"github.com/golang/protobuf/proto"
	"grpc-demo/service"
)

func main() {

	user := service.User{
		Username: "zhangsan",
		Age:      18,
	}

	// 序列化
	marshal, err := proto.Marshal(&user)
	if err != nil {
		panic(err)
	}

	// 反序列化
	newUser := service.User{}
	err = proto.Unmarshal(marshal, &newUser)
	if err != nil {
		panic(err)
	}

	fmt.Println(newUser.String())

}

安装Protobuf

  1. 下载protocol buffers地址:https://github.com/protocolbuffers/protobuf/releases
  • Protocol buffers,通常成为protobuf,是谷歌开发的一种协议,用于允许对结构化数据进行序列化和反序列化,用于程序开发中通过网络相互通信和存储数据,谷歌开发的目的是提供一种比xml更好的方式进行通信

  • 根据自己的系统选择合适的安装包进行下载
    在这里插入图片描述

  • 配置环境变量

  • 检查,打开cmd,输入protoc --verion

  1. 安装gRPC的核心库
go get -u google.golang.org/grpc
  1. 上面安装的是protocol编辑器,它可以生成各种不同语言的代码,因此,除了这个编译器,我们还需要配合各个语言的代码生成工具,go的工具是protoc-gen-go,不过这里有个小小的坑,github.com/golang/protobuf/protoc-gen-gogoogle.golang.org/protobuf/cmd/protoc-gen-go是不同的,区别在于前者是旧版本,后者是谷歌接管后的新版本,它们之间的API是不同,也就是说用于生成的命令,以及生成的文件都是不一样的,因为目前的gRPC-go源码中的example用的是后者的生成方式,所以我们也采取最新的方式,需要安装两个库:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

因为这些文件在安装grpc的时候就已经下载下来了,因此使用install命令即可,不需要使用get命令

安装后打开你的$GOPATH/bin目录下,应该有以下两个.exe文件

在这里插入图片描述

proto文件编写

定义一个proto目录,用于存放编写的.proto文件,编写hello.proto

// 指定当前proto语法的版本,有2和3
syntax = "proto3";
// option go_package = "path;name";
// path表示生成的go文件的存放地址,会自动生成目录,.表示当前目录
// name表示生成的go文件的包名
option go_package = ".;service"; // 生成的go文件在上一层目录下的service包里
// 指定文件生成出来的package
package service;

// 然后需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接收客户端的参数,再返回给服务端的响应
// 其实很容易可以看出,我们定义了一个service,成为SayHello,这个服务中有一个rpc方法,名为SayHello
// 这个方法会发送一个HelloRequest,然后返回一个HelloResponse
service SayHello {
	rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
// 定义消息的类型,可以理解为go中的struct,后面的数字是序列号,指的是这个变量在这个message中的位置
message HelloRequest {
  string requestName = 1;
  // int64 id = 2;
}

message HelloResponse {
  string responseMsg = 1;
}

编写完成后,在proto目录下执行命令:

protoc --go_out=. --go-grpc_out=. hello.proto

系统会根据go_out指定的目录再拼接proto文件中go_package指定的目录生成对应的包名

proto文件介绍

message

用于protobuf中定义一个消息类型

消息就是需要进行传输的数据格式的定义,类似于go中的struct

在消息中的数据分别对应每一个字段,其中每一个字段都有一个名字和一种类型

一个proto文件中可以定义多个消息类型

例如:

message User {
  string username = 1;
  int64 age = 2;
}

在消息中承载的数据分别对应每一个字段

其中每个字段都有一个名字和类型

在一个proto文件中message可以定义多个

字段规则

  • required:消息体中必填字段,不设置会导致编码解码的异常
  • optional:消息体中可选字段
  • repeated:消息体中可重复字段,重复的值的顺序会被保留,在go中重复的字段会定义为切片类型。

默认为required必填字段

例如:

message User {
  string username = 1;
  int64 age = 2;
  optional string password = 3;
  repeated string addresses = 4;
}

生成的go文件的User结构体

type User struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Username  string   `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
	Age       int64    `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
	Password  *string  `protobuf:"bytes,3,opt,name=password,proto3,oneof" json:"password,omitempty"`
	Addresses []string `protobuf:"bytes,4,rep,name=addresses,proto3" json:"addresses,omitempty"`
}

字段映射

.proto TypeNotesC++ TypePython TypeGo Type
doubledoublefloatfloat64
floatfloatfloatfloat32
int32使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代int32intint32
uint32使用变长编码uint32int/longuint32
uint64使用变长编码uint64int/longuint64
sint32使用变长编码,这些编码在负值时比int32高效的多int32intint32
sint64使用变长编码,有符号的整型值。编码时比通常的 int64高效。int64int/longint64
fixed32总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。uint32intuint32
fixed64总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。uint64int/longuint64
sfixed32总是4个字节int32intint32
sfixed32总是4个字节int32intint32
sfixed64总是8个字节int64int/longint64
boolboolboolbool
string一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。stringstr/unicodestring
bytes可能包含任意顺序的字节数据。stringstr[]byte

默认值

protobuf3删除了protobuf2中用来设置默认值的default关键字,为各类型定义的默认值

类型默认值
boolfalse
整型0
string空字符串""
枚举enum第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0;
message不是null,而是DEFAULT_INSTANCE

标识号

在消息体的定义中,每个字段都必须有一个唯一的标识号,范围是[1,2^29-1]

例如上面user结构体中username的标识号为1,age为2

嵌套消息

可以在其它消息类型中定义,使用消息类型,也可以使用外部定义的消息

message PersonInfo {
  message Person {
    string name = 1;
    int32  height = 2;
    repeated int32 weight = 3;
  }
  repeated Person info = 1;
  repeated User user = 2;
}

生成的go文件中的结构体

type PersonInfo struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Info []*PersonInfo_Person `protobuf:"bytes,1,rep,name=info,proto3" json:"info,omitempty"`
	User []*User              `protobuf:"bytes,2,rep,name=user,proto3" json:"user,omitempty"`
}
type PersonInfo_Person struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Name   string  `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	Height int32   `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
	Weight []int32 `protobuf:"varint,3,rep,packed,name=weight,proto3" json:"weight,omitempty"`
}

如果要在它的父消息类型的外部重用这个消息类型,需要PersonInfo.Person的形式使用它

message PersonMessage {
	PersonInfo.Person info = 1;
}

定义服务

如果想要将消息类型用在rpc中,可以使用service定义一个服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码及存根

service SayHello {
	// rpc 服务函数名(参数) returns (返回参数){}
	rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

上述代码表示,定义了一个rpc服务方法,该方法接收参数为HelloRequest返回HelloResponse

import使用

import用于导入其它的proto文件

// 从执行protoc这个命令的当前目录开始算起
import "user.proto";

any任意类型

需要导入any.proto,属性使用google.protobuf.Any定义

import "google/protobuf/any.proto";

message HelloAny {
  google.protobuf.Any data = 1;
}

结构体中的类型:

Data *anypb.Any `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`

快速开始

服务端编写

  • 创建grpc server对象,可以理解为server端的抽象对象
  • 将server注册到grpc server的内部注册中心,这样可以在接收到请求是,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理
  • 创建Listen,监听TCP端口
  • grpc server开始lis.Accept,知道Stop
package main

import (
	"context"
	"google.golang.org/grpc"
	pb "grpc-study/hello-server/proto"
	"log"
	"net"
)

type server struct {
	pb.UnimplementedSayHelloServer
}

func (s server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{ResponsonMsg: "hello " + req.RequestName}, nil
}

func main() {
	// 创建端口
	listen, _ := net.Listen("tcp", ":9090")
	// 创建grpc服务
	grpcServer := grpc.NewServer()
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

客户端编写

  • 创建与服务端的连接交互
  • 创建server的客户端对象
  • 发送rpc请求,等待同步响应,得到回调后返回响应结果
  • 输出响应结果
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-study/hello-server/proto"
	"log"
)

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

	// 建立连接
	client := pb.NewSayHelloClient(conn)
	// 执行rpc调用
	resp, err := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "zhangsan"})
	if err != nil {
        log.Fatal("调用失败:", err)
		return
	}
	fmt.Println(resp.GetResponsonMsg())
}

认证-安全传输

grpc是一个典型的C/S模型,需要客户端和服务端,客户端与服务端需要达成相同的协议,以确保在传输数据的过程中不会被外界破坏,grpc通常默认是使用protobuf来作为传输协议,当然也是可以使用其它自定义的方式。

在这里插入图片描述

那么,客户端与服务端要通信前,客户端如何知道自己的数据是发送给哪一个明确的服务端?反过来,服务端是不是也需要一个方式来判断自己的数据要返回给谁?

那么就需要grpc的认证来保证客户端与服务端之间可以进行安全的数据传输,认证方式有如下几种:

  • SSL/TLS认证方式(采用http2协议)
  • 基于token的认证方式(基于安全连接)
  • 不采用任何措施的连接,这是不安全的连接(默认HTTP1)
  • 自定义的身份认证

TLS(Transport Layer Security 安全传输层)是建立在TCP协议之上的协议,服务于应用层,它的前身是SSL(Secure Socket Layer 安全套接字层),它实现了将应用层的报文进行加密后在交由TCP进行传输的功能。

TLS协议主要解决如下三个网络安全问题。

  • 保密,通过加密encryption实现,所有信息都加密传输
  • 完整性,通过MAC校验机制,一旦被篡改,通信双方会立刻发现
  • 认证,双方认证都可以配备证书,防止被冒充

生产环境可以购买证书或使用一些平台发放的免费证书

  • key:服务器上的私钥文件,用于对发送给客户端数据的贾母,以及对从客户端接收到数据的解密
  • csr:证书签名请求文件,用于提交给证书颁发机构对证书签名
  • crt:由证书颁发机构签名后的证书,或是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息
  • pem:是基于Base64编码的证书格式,扩展名包括PEM,CRT和CER

什么是SAN

SAN(Subject AlterNative Name)是SSL标准x509中定义的一个扩展,是用来 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

SSL/TLS认证方式

首先通过openssl生成证书和私钥

  1. 官网下载:https://www.openssl.org/source/

或其他人做的便捷版安装包:http://slproweb.com/products/Win32OpenSSL.html

  1. 推荐使用便捷版安装包,一直下一步
  2. 配置环境变量
  3. 命令行测试openssl,安装成功后开始
  • 打开项目,新建key目录用于储存认证文件
  • 生成证书
# 1.生成私钥
openssl genrsa -out server.key 2048
# 2.生成证书
openssl req -new -x509 -key server.key -out server.crt -days 36500
# 3.生成csr
openssl req -new -key server.key -out server.csr
  • 更改openssl.cnf(linux是openssl.cfg)
# 1.复制一份安装的openssl.cnf到key目录
# 2.找到[ CA_default ],打开copy_extensions = copy(就是把前面的#去掉)
# 3.找到[ req ],打开req_extensions = v3_req
# 4.找到[ v3_req ],添加subjectAltName = @alt_names
# 5.添加新的标签[ alt_names ]和标签字段
DNS.1 = *.com
# 生成证书私钥test.key
openssl genpkey -algorithm RSA -out test.key

# 通过私钥test.key生成证书请求文件test.csr(注意cfg和cnf)
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cnf -extensions v3_req
# test.csr是上面生成的证书请求文件。ca.crt/server.key是CA证书文件和key,用来对test.csr进行签名认证。这两个文件在第一部分生成。

# 生成SAN证书 pem
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
  • 客户端代码:
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-study/hello-server/proto"
	"log"
)

func main() {
	cred, _ := credentials.NewClientTLSFromFile("../key/test.pem",
		"*.com")
	// 连接到server端,此处禁用安全传输,没有加密和验证
	conn, err := grpc.Dial("127.0.0.1:9090", cred)
	if err != nil {
		log.Fatalf("连接失败:%v", err)
		return
	}
	defer conn.Close()

	// 建立连接
	client := pb.NewSayHelloClient(conn)
	// 执行rpc调用
	resp, err := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "zhangsan"})
	if err != nil {
		fmt.Println("调用失败", err)
		return
	}
	fmt.Println(resp.GetResponsonMsg())
}

  • 服务端代码:
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "grpc-study/hello-server/proto"
	"net"
)

type server struct {
	pb.UnimplementedSayHelloServer
}

func (s server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	return &pb.HelloResponse{ResponsonMsg: "hello " + req.RequestName}, nil
}

func main() {
	cred, _ := credentials.NewServerTLSFromFile("../key/test.pem",
		"../key/test.key")
	// 创建端口
	listen, _ := net.Listen("tcp", ":9090")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(cred))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		fmt.Printf("failed to serve: %v", err)
		return
	}
}

token认证

我们先看一个grpc提供的一个接口,这个接口中有两个方法,接口位于credentials包下,这个接口需要客户端来实现

type PerRPCCredentials interface {
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
	RequireTransportSecurity() bool
}

第一个方法作用是获取元数据信息,也就是客户端提供的key-value对,context用于控制超时和取消,uri是请求入口处的uri

第二个方法作用是是否需要基于TLS认证进行安全传输,如果返回值是true,则必须加上TLS验证,返回值是false则不用

  • 客户端代码:
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-study/hello-server/proto"
	"log"
)

type ClientTokenAuth struct {
}

func (c *ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"username": "zhangsan",
		"password": "123456",
	}, nil
}
func (c *ClientTokenAuth) RequireTransportSecurity() bool {
	return false
}

func main() {
	// 连接到server端,此处禁用安全传输,没有加密和验证
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	opts = append(opts, grpc.WithPerRPCCredentials(&ClientTokenAuth{}))
	conn, err := grpc.Dial("127.0.0.1:9090", opts...)
	if err != nil {
		log.Fatalf("连接失败:%v", err)
		return
	}
	defer conn.Close()

	// 建立连接
	client := pb.NewSayHelloClient(conn)
	// 执行rpc调用
	resp, err := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "zhangsan"})
	if err != nil {
		fmt.Println("调用失败", err)
		return
	}
	fmt.Println(resp.GetResponsonMsg())
}
  • 服务端代码
package main

import (
	"context"
	"errors"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	pb "grpc-study/hello-server/proto"
	"log"
	"net"
)

type server struct {
	pb.UnimplementedSayHelloServer
}

func (s server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	// 获取元数据的信息
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("未传输token")
	}
	var username string
	var password string
	if v, ok := md["username"]; ok {
		username = v[0]
	}
	if v, ok := md["password"]; ok {
		password = v[0]
	}
	if username != "zhangsan" || password != "123456" {
		return nil, errors.New("token错误")
	}
	return &pb.HelloResponse{ResponsonMsg: "hello " + req.RequestName}, nil
}

func main() {
	// 创建端口
	listen, _ := net.Listen("tcp", ":9090")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

服务端进行token验证的方式有两种:

  • 在业务逻辑中进行token的验证
  • 在创建grpc服务时添加拦截器,在拦截器里验证

上述代码是在业务逻辑中进行token验证的方案

stream流

在 HTTP/1.1 的时代,同一个时刻只能对一个请求进行处理或者响应,换句话说,下一个请求必须要等当前请求处理完才能继续进行。

HTTP/1.1需要注意的是,在服务端没有response的时候,客户端是可以发起多个request的,但服务端依旧是顺序对请求进行处理, 并按照收到请求的次序予以返回。

HTTP/2 的时代,多路复用的特性让一次同时处理多个请求成为了现实,并且同一个 TCP 通道中的请求不分先后、不会阻塞,HTTP/2 中引入了流(Stream) 和 帧(Frame) 的概念,当 TCP 通道建立以后,后续的所有操作都是以流的方式发送的,而二进制帧则是组成流的最小单位,属于协议层上的流式传输。

HTTP/2 在一个 TCP 连接的基础上虚拟出多个 Stream, Stream 之间可以并发的请求和处理, 并且 HTTP/2 以二进制帧 (frame) 的方式进行数据传送, 并引入了头部压缩 (HPACK), 大大提升了交互效率

定义

// 普通 RPC
rpc SayHello (HelloRequest) returns (HelloResponse) {}

// 客户端流式 RPC
rpc ClientStream (stream HelloRequest) returns (HelloResponse) {}

// 服务器端流式 RPC
rpc ServerStream (HelloRequest) returns (stream HelloResponse) {}

// 双向流式 RPC
rpc BothStream (stream HelloRequest) returns (stream HelloResponse) {}

stream关键字,当该关键字修饰参数时,表示这是一个客户端流式的 gRPC 接口;当该参数修饰返回值时,表示这是一个服务器端流式的 gRPC 接口;当该关键字同时修饰参数和返回值时,表示这是一个双向流式的 gRPC 接口。

客户端流

客户端代码:

package main

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

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

	// 建立连接
	client := pb.NewSayHelloClient(conn)
	// 执行rpc调用
	clientStream, err := client.ClientStream(context.Background())
	if err != nil {
		log.Fatal("调用失败", err)
		return
	}
	helloch := make(chan struct{}, 1)
	go helloRequest(clientStream, helloch)
	select {
	case <-helloch:
		resp, err := clientStream.CloseAndRecv()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("客户端收到响应:", resp.ResponsonMsg)
	}
}

func helloRequest(stream pb.SayHello_ClientStreamClient, rsp chan struct{}) {
	count := 0
	for {
		err := stream.Send(&pb.HelloRequest{RequestName: "zhangsan"})
		if err != nil {
			log.Fatal(err)
		}
		time.Sleep(time.Second)
		count++
		if count > 10 {
			rsp <- struct{}{}
			break
		}
	}
}

服务端代码:

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-demo/proto"
	"io"
	"log"
	"net"
)

type server struct {
	pb.UnimplementedSayHelloServer
}

func (s server) ClientStream(stream pb.SayHello_ClientStreamServer) error {
	count := 0
	for {
		//源源不断的去接收客户端发来的信息
		req, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return err
		}
		fmt.Println("服务端接收到的流", req.RequestName, count)
		count++
		if count > 10 {
			resp := &pb.HelloResponse{ResponsonMsg: req.RequestName}
			err := stream.SendAndClose(resp)
			if err != nil {
				return err
			}
			return nil
		}
	}
}

func main() {
	listen, _ := net.Listen("tcp", ":9090")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

服务端流

客户端代码:

package main

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

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

	// 建立连接
	client := pb.NewSayHelloClient(conn)
	// 执行rpc调用
	serverStream, err := client.ServerStream(context.Background(), &pb.HelloRequest{RequestName: "zhangsan"})
	if err != nil {
		log.Fatal("获取流出错", err)
	}
	for {
		resp, err := serverStream.Recv()
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端数据接收完成")
				err := serverStream.CloseSend()
				if err != nil {
					log.Fatal(err)
				}
				break
			}
			log.Fatal(err)
		}
		fmt.Println("客户端收到的流", resp.ResponsonMsg)
	}
}

服务端代码:

package main

import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-demo/proto"
	"log"
	"net"
	"time"
)

type server struct {
	pb.UnimplementedSayHelloServer
}

func (s server) ServerStream(req *pb.HelloRequest, stream pb.SayHello_ServerStreamServer) error {
	count := 0
	for {
		resp := &pb.HelloResponse{ResponsonMsg: req.RequestName}
		err := stream.Send(resp)
		if err != nil {
			return err
		}
		time.Sleep(time.Second)
		count++
		if count > 10 {
			return nil
		}
	}
}

func main() {
	listen, _ := net.Listen("tcp", ":9090")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

双向流

客户端代码:

package main

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

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

	// 建立连接
	client := pb.NewSayHelloClient(conn)
	// 执行rpc调用
	bothStream, err := client.BothStream(context.Background())
	if err != nil {
		log.Fatal("获取流出错", err)
	}
	for {
		err = bothStream.Send(&pb.HelloRequest{RequestName: "zhangsan"})
		if err != nil {
			log.Fatal(err)
		}
		time.Sleep(time.Second)
		resp, err := bothStream.Recv()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("客户端收到的流信息", resp.ResponsonMsg)
	}
}

服务端代码:

package main

import (
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "grpc-demo/proto"
	"log"
	"net"
	"time"
)

type server struct {
	pb.UnimplementedSayHelloServer
}

func (s server) BothStream(stream pb.SayHello_BothStreamServer) error {
	for {
		req, err := stream.Recv()
		if err != nil {
			return nil
		}
		fmt.Println("服务端收到客户端的消息", req.RequestName)
		time.Sleep(time.Second)
		resp := &pb.HelloResponse{ResponsonMsg: req.RequestName}
		err = stream.Send(resp)
		if err != nil {
			return nil
		}
	}
}

func main() {
	listen, _ := net.Listen("tcp", ":9090")
	// 创建grpc服务
	grpcServer := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
	// 注册服务
	pb.RegisterSayHelloServer(grpcServer, &server{})
	// 启动服务
	err := grpcServer.Serve(listen)
	if err != nil {
		log.Fatal("服务启动失败:", err)
		return
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一弓虽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值