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 远程过程调用),是一种协议,是用来屏蔽分布式计算中的各种调用细节,使得你可以像是本地调用一样直接调用一个远程的函数。
客户端与服务端沟通的过程
- 客户端发送数据(以字节流的方式)
- 服务端接收数据并解析,根据业务执行相应的逻辑处理,然后把结果返回给客户
RPC:
- RPC就是将上述过程封装,优化操作
- 使用一些大家都认可的协议进行规范化处理
- 做成一些框架,产生利益
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
是二进制数据格式,需要编码和解码,数据本身不具有可读性,因此只能反序列化后得到真正刻度的数据
优势:
- 序列化后体积相比json和xml很小,适合网络传输
- 支持跨平台多语言
- 消息格式升级和兼容性较好
- 序列化和反序列化的速度快
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
- 下载
protocol buffers
地址:https://github.com/protocolbuffers/protobuf/releases
-
Protocol buffers,通常成为protobuf,是谷歌开发的一种协议,用于允许对结构化数据进行序列化和反序列化,用于程序开发中通过网络相互通信和存储数据,谷歌开发的目的是提供一种比xml更好的方式进行通信
-
根据自己的系统选择合适的安装包进行下载
-
配置环境变量
-
检查,打开cmd,输入
protoc --verion
- 安装gRPC的核心库
go get -u google.golang.org/grpc
- 上面安装的是protocol编辑器,它可以生成各种不同语言的代码,因此,除了这个编译器,我们还需要配合各个语言的代码生成工具,go的工具是
protoc-gen-go
,不过这里有个小小的坑,github.com/golang/protobuf/protoc-gen-go
和google.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 Type | Notes | C++ Type | Python Type | Go Type |
---|---|---|---|---|
double | double | float | float64 | |
float | float | float | float32 | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 | int32 | int | int32 |
uint32 | 使用变长编码 | uint32 | int/long | uint32 |
uint64 | 使用变长编码 | uint64 | int/long | uint64 |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int32 |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 | int64 | int/long | int64 |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 | uint32 | int | uint32 |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 | uint64 | int/long | uint64 |
sfixed32 | 总是4个字节 | int32 | int | int32 |
sfixed32 | 总是4个字节 | int32 | int | int32 |
sfixed64 | 总是8个字节 | int64 | int/long | int64 |
bool | bool | bool | bool | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 | string | str/unicode | string |
bytes | 可能包含任意顺序的字节数据。 | string | str | []byte |
默认值
protobuf3删除了protobuf2中用来设置默认值的default关键字,为各类型定义的默认值
类型 | 默认值 |
---|---|
bool | false |
整型 | 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
生成证书和私钥
或其他人做的便捷版安装包:http://slproweb.com/products/Win32OpenSSL.html
- 推荐使用便捷版安装包,一直下一步
- 配置环境变量
- 命令行测试
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
}
}