微服务GRPC时代

《微服务》

诞生背景

  • 互联网行业的快速发展,需求变化快,用户数量变化快
  • 敏捷开发深入人心,用最小的代价,做最快的迭代,频繁修改、测试、上线
  • 容器技术的成熟,是微服务的技术基础

由于以上原因,催化了 服务架构 的一步一步演变:

单体架构 ===》 垂直架构 ===》 SOA架构 ===》 微服务架构

概念

  • 使用一套小服务来开发单个应用的方式,
  • 每个服务运行在 独立的进程里,
  • 一般采用 轻量级的通讯机制(rpc) 互联,
  • 可以通过自动化的方式部署

特点

  • 单一职责,此时项目专注于登录和注册
  • 轻量级的通信,通信与平台和语言无关
  • 隔离性,数据隔离
  • 有自己的数据空间
  • 技术 多样性

优点

  • 独立性
  • 使用者容易理解
  • 技术栈灵活
  • 高效团队

缺点

  • 额外的工作,服务的拆分
  • 保证数据一致性
  • 增加了沟通成本
  • 分布式的复杂性

微服务 项目 的 全生态

《1》硬件层

docker+k8s去解决

《2》通信层

  • 网络传输,用RPC(远程过程调用)

基于TCP,更靠底层,RPC基于TCP

  • 需要知道调用谁,用服务注册服务发现
1.注册
4.调用
2.询问
3.告知
客户端
注册中心
服务端

说明: 微服务的客户端 指的是 调用 微服务的 一端, 不一定是用户 或者 前端。

  • 数据传递的 序列化

通常采用protobuf进行对数据的序列化处理

《3》应用平台层

  • 云管理平台、监控平台、日志管理平台,需要他们支持
  • 服务管理平台,测试发布平台
  • 服务治理平台

《4》微服务层

  • 用 微服务框架 实现业务逻辑

《RPC》

概念

RPC(Remote Procedure Call) 远程过程调用,是一个计算机 通信协议
应用层协议(http协议同层),底层使用 TCP 实现。
简单的理解就是 访问远程的服务 的方式 就像 本地调用函数 的形式一样。

RPC 要解决的 三个问题

  1. Call ID 映射

在 本地函数调用 时, 函数体是直接通过 函数指针 来指定的。
在远程调用中,函数指针 就不能使用了, 因为 两个进程的 地址空间 是完全不一样的。
所以,在PRC中 所有的函数都必须有自己的一个ID, 这个ID在所有的进程中都是唯一确定的。
客户端在做 远程调用 时, 必须附上这个ID。
在客户端 和 服务端 分别维护一个 {函数 <—> Call ID}的对应表。

  1. 序列化 和 反序列化

在 本地函数调用 时, 只需将 参数 压到 栈中,然后让调用的函数自己读取就可以了。
在远程调用中,客户端 和 服务端 是不同的进程,所以不能通过 内存 来传递参数。
因此就需要 客户端 把参数 先转成一个 字节流, 传到 服务端 后, 再把 字节流 转成自己能读取的格式。
以上这个 转的过程 称之为 序列化 和 反序列化。

  1. 网络传输

远程过程调用,由于有 远程 在里面,所以 两端的调用 就需要 通过网络连接。
大部分PRC框架都是痛TCP协议, 其实UDP也是可以的,而gRPC干脆就用了HTTP2。

Go 中如何实现RPC

golang中实现RPC非常简单,官方提供了封装好的库,还有一些第三方的库
golang官方的 net/rpc库使用 encoding/gob 进行编解码,支持tcp和http数据传输方式,
由于其他语言不支持 gob 编解码方式,所以golang的RPC只支持golang开发的服务器与客户端之间的交互

官方还提供了net/rpc/jsonrpc库 实现RPC方法,jsonrpc采用JSON进行数据编解码,因而支持跨语言调用,
目前 jsonrpc库 是基于tcp协议实现的,暂不支持http传输方式

例题:golang实现RPC程序,实现求矩形面积和周长

  • 服务端:
package main

import (
    "log"
    "net/http"
    "net/rpc"
)

// 例题:golang实现RPC程序,实现求矩形面积和周长

type Params struct {
    Width, Height int
}

type Rect struct{}

// RPC服务端方法,求矩形面积
func (r *Rect) Area(p Params, ret *int) error {
    *ret = p.Height * p.Width
    return nil
}

// 周长
func (r *Rect) Perimeter(p Params, ret *int) error {
    *ret = (p.Height + p.Width) * 2
    return nil
}

// 主函数
func main() {
    // 1.注册服务
    rect := new(Rect)
    // 注册一个rect的服务
    rpc.Register(rect)
    // 2.服务处理绑定到http协议上
    rpc.HandleHTTP()
    // 3.监听服务
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        log.Panicln(err)
    }
}
  • 客户端:
package main

import (
    "fmt"
    "log"
    "net/rpc"
)

// 传的参数
type Params struct {
    Width, Height int
}

// 主函数
func main() {
    // 1.连接远程rpc服务
    conn, err := rpc.DialHTTP("tcp", ":8000")
    if err != nil {
        log.Fatal(err)
    }
    // 2.调用方法
    // 面积
    ret := 0
    err2 := conn.Call("Rect.Area", Params{50, 100}, &ret)
    if err2 != nil {
        log.Fatal(err2)
    }
    fmt.Println("面积:", ret)
    // 周长
    err3 := conn.Call("Rect.Perimeter", Params{50, 100}, &ret)
    if err3 != nil {
        log.Fatal(err3)
    }
    fmt.Println("周长:", ret)
}

**说明:**golang写RPC程序,必须符合4个基本条件,不然RPC用不了

  • 结构体字段首字母要大写,可以别人调用
  • 函数名必须首字母大写
  • 函数第一参数是接收参数,第二个参数是返回给客户端的参数,必须是指针类型
  • 函数还必须有一个返回值error

RPC调用流程

微服务架构下数据交互一般是对内 RPC对外 REST
微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用

RPC服务端 de 核心工作

  • 服务端接收到的数据需要包括什么?

调用的函数名、参数列表,还有一个返回值error类型

  • 服务端需要解决的问题是什么?

Map维护客户端传来调用函数,服务端知道去调谁

  • 服务端的核心功能有哪些?

维护函数map
客户端传来的东西进行解析
函数的返回值打包,传给客户端

《Protobuf》

概念

Protobuf是Google公司开发的一种数据描述语言,是一种 轻便高效 的 结构化数据 存储格式,
可以用于结构化数据串行化,或者说 序列化
习惯用json、XML 数据存储格式, 对Protocol Buffer 不免有点陌生,
Protocol Buffer 经历了2和3两个版本, 目前主流的是版本3.

优点

  • 性能
    <1> 压缩性能好,序列化后体积相比Json和XML很小
    <2> 序列化和反序列化快, 比xml和json快2~100倍
    <3> 传输速度快

  • 便捷性
    <1> 便捷简单, 可以自定生成序列化 和 反序列化 代码
    <2> 维护成本低, 只需要维护 proto 文件
    <3> 向后兼容,不必破坏旧格式
    <4> 加密性好

  • 跨语言
    <1> 跨平台
    <2> 支持各种主流语言

缺点

  • 通用性差
    json任何语言都可以支持,但是protobuf需要专门的解析库。

  • 自解释性差
    只有通过proto文件才能了解数据结构

Protobuf 语法规则

先来看一个非常简单的 proto文件:

syntax = "proto3";
message SearchRequest {
  string query = 1; // 这是 注释
  int32 page_number = 2;
  int32 result_per_page = 3;
}

说明:
1、文件以.proto做为 文件后缀,除 结构定义 外的 语句以分号结尾
2、文件的第一行指定了你正在使用 proto3语法版本 :若没有则编译器会使用proto2。
3、SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。
4、每个字段都有 名字、类型、表示号。
5、message 对应go中的struct

(1)字段规则

字段格式:
	限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值]

限定修饰符字段编码值 这两个一眼看不出 什么玩意 的概念进行说明:

  • 字段编码值(标识符)
  • 在消息定义中,每个字段都有唯一的一个 数字 标识符,这些标识符是用来在消息的二进制格式中 识别 各个字段的。
    一旦开始使用就不能够再改变。
  • [1,15] 之内的标识号在编码的时候会占用 1个字节[16,2047] 之内的标识号则占2个字节。
    所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。
    切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
  • 标识号范围: 【1~~2^29 】 2^29=536,870,911。
    不要使用 [19000-19999] 的标识号,Protobuf协议实现中对这些进行了预留
    如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
  • 限定修饰符: required、optional、repeated
  • Required

表示是一个必须字段
对于 发送方,在发送消息之前必须设置该字段的值,对于 接收方,必须能够识别该字段的意思。
发送之前没有设置required字段 或者 无法识别required字段都会引发编解码异常,导致消息被丢弃

  • Optional

表示是一个可选字段
对于 发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。
对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。
.
因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡

  • Repeated

表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值

(2)proto文件 经过编译后的 产出物

当用protocol buffer编译器来运行proto文件时,编译器将生成所选择语言的代码,
这些代码可以 操作 在proto文件中定义的消息类型,包括获取、设置字段值,
这些代码可以 将消息序列化到一个输出流中,以及从一个输入流中解析消息。

  • 对go来说,编译器会位每个消息类型生成了一个 .pd.go文件
  • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有 静态描述符的模块
    该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
  • 对C++来说,编译器会为每个.proto文件生成一个 .h文件 和一个 .cc文件 ,.proto文件中的每一个消息有一个对应的类。
  • 对Java来说,编译器为每一个消息类型生成了一个 .java文件 ,以及一个特殊的 Builder类(该类是用来创建消息类接口的)。
  • 对于Ruby来说,编译器会为每个消息类型生成了一个 .rb文件

(3)变量类型

一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,
以及与之对应的、在自动生成的访问类中定义的类型:

.proto TypeNotesPython Type

Go Type

doublefloatfloat64
floatfloatfloat32
int32使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代intint32
uint32使用变长编码intuint32
uint64使用变长编码intuint64
sint32使用变长编码,这些编码在负值时比int32高效的多intint32
sint64使用变长编码,有符号的整型值。编码时比通常的int64高效。intint64
fixed32总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。intuint32
fixed64总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。intuint64
sfixed32总是4个字节intint32
sfixed64总是8个字节intint64
boolboolbool
string一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。strstring
bytes可能包含任意顺序的字节数据。str[]byte

(4)默认值

当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,
被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:

  • 对于strings,默认是一个空string
  • 对于bytes,默认是一个空的bytes
  • 对于bools,默认是false
  • 对于数值类型,默认是0
  • 对于枚举,默认是第一个定义的枚举值,必须为0;
  • 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的

(5)其他数据类型

  • 数组
    通过 repeated 关键字 形成数组类型:
message HelloRequest {
    string name = 1;
    repeated int32 id = 2;
}
  • Map
message HelloRequest {
    string name = 1;
    map<string, string> wtt = 2;
}
  • 枚举
enum Gender {
    man = 1;
    woman = 2;
}

message HelloRequest {
    string name = 1;
    Gender wtt = 2;
}
  • message嵌套
enum Gender {
    man = 1;
    woman = 2;
}

message HelloRequest {
    string name = 1;

    message Res {
        string caller = 1;
        string school = 2;
    }

    repeated Res wtt = 2;
}

(6)引入 其他proto文件

在同目录下有 aaa.proto 和 bbb.proto 两位文件,在aaa中引入bbb:

import "bbb.proto";

注意:
当 编译 aaa 文件生成对应 编程语言的文件时, 不会将被编译的aaa 中 引入的 bbb文件一并编译,
而需要手动编译 被引入的bbb文件。

综合例子

syntax = "proto3";//正在使用`proto3`语法 // 默认是proto2
 
// 指定所在包名
package PB;
 
option go_package = "../pb";// 避免错误 unable to determine Go import path for "myproto.proto"
 
// 定义  枚举类型
enum Week{
  Monday = 0;//peoto3中,枚举值必须是从0开始,不能设置成1开始
  Tuesday = 1;
}
 
// 定义  消息体
message Student{
  int32 age = 1;//可以不从1开始,但是不能重复。19000-19999范围的数字不能用,系统保留。
  string name = 2;
  People p = 3;
  repeated int32 score = 4;//数组
  // 枚举
  Week w = 5;
  // 联合体
  oneof data{
    string teacher = 6;//不能写5 或者 3,联合体是只能取其中的一个,编号唯一,不能重复
    string class = 7;
  }
}
 
// 消息体  支持嵌套
message People{
  int32 weight = 1;
}
 
// 添加 rpc 服务
service service_say{
  rpc Say(People) returns (Student);// 注意returns 是关键字, 注意是 复数形式
}

编译:

# 命令如下,同目录自动生成    xxx.pb.go 文件
protoc --go_out=./ *proto     

《gRPC》

概念

  • gRPC 由google开发,是一款语言中立平台中立、开源的RPC框架。
  • 基于http 2.0协议设计,可以基于一个HTTP/2链接提供多个服务,对于移动设备更加友好。
  • gRPC客户端 和 服务端 可以在多种环境中运行和交互,例如用java写一个服务端,可以用go语言写客户端调用
  • gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。
    这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
  • 目前提供的 C版本是:grpc; Java版本是:grpc-java; Go版本是: grpc-go
    很多 动态语言 底层使用的是 C语言,所以C版本支持一系列使用C的动态语言。
    其中 C版本 支持的语言有: C, C++, Node.js, Python, Ruby, PHP, C#。

gRPC可以实现微服务,将大的项目拆分为多个 小且独立的 业务模块,也就是服务,
各服务间使用高效的protobuf协议进行 RPC调用,
gRPC默认使用的 protocol buffers,也是·google·开源的一套成熟的 结构数据序列化机制。
当然也可以使用其他数据格式如JSON

gRPC 初体验

python 跃跃欲试

1 依赖安装
pip3 install grpcio # grpc包
pip3 install grpcio-tools # grpc编译器

说明:

在安装的过程中, 卡在了 包不能打包成wheel,从setup.py 安装。
解决方法

pip3 install --upgrade pip
pip3 install --upgrade setuptools
pip3 install --no-cache-dir --force-reinstall -Iv grpcio==<version>

然后就可以正常安装了

目录结构

├── client
│   ├── client.py
│   └── proto
│       ├── __pycache__
│       │   ├── wtt_pb2.cpython-37.pyc
│       │   └── wtt_pb2_grpc.cpython-37.pyc
│       ├── wtt_pb2_grpc.py
│       ├── wtt_pb2.py
│       └── wtt.proto
└── server
    ├── proto
    │   ├── __pycache__
    │   │   ├── wtt_pb2.cpython-37.pyc
    │   │   └── wtt_pb2_grpc.cpython-37.pyc
    │   ├── wtt_pb2_grpc.py
    │   ├── wtt_pb2.py
    │   └── wtt.proto
    └── server.py
2 proto文件: wtt.proto
  • 创建
syntax = "proto3";

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}
  • 编译
python3 -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. 文件名.proto

注意:
会得到两个py文件:*_pb2_grpc.py 和 *_pb2.py,
pb2_grpc.py 文件中 引入 pb2.py 文件的路径方式 需要根据 实际的相对路径情况 手动改一下:

# 生成的:
import user_pb2 as user__pb2
# 改为:
from . import user_pb2 as user__pb2
3 服务端
from concurrent import futures
import grpc #来自于grpcio包

from proto import wtt_pb2, wtt_pb2_grpc

class Greeter(wtt_pb2_grpc.GreeterServicer):
    # 方法的 第二个参数 request 和 第三个参数 context是固定的
    def SayHello(self, request, context):
        return wtt_pb2.HelloReply(message = f"你好啊,小{request.name}")

if __name__ == "__main__":
    # 1 实例化server
    server = grpc.server(futures.ThreadPoolExecutor(max_workers = 10)) # 使用线程池
    # 2 注册处理逻辑
    wtt_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
    # 3 启动server
    server.add_insecure_port('0.0.0.0:50051') #如果使用证书 则 调用 add_secure_port方法
    server.start()
    server.wait_for_termination()
4 客户端
import grpc #来自于grpcio包
from proto import wtt_pb2, wtt_pb2_grpc

if __name__ == "__main__":
    with grpc.insecure_channel("localhost:50051") as channel:
        st = wtt_pb2_grpc.GreeterStub(channel)
        res = st.SayHello(wtt_pb2.HelloRequest(name="tom"))
        print(res.message)

Go 蠢蠢欲动

1 依赖安装
  • grpc编译器
    linux系统中默认已经有protoc命令,可以用which protoc 查看是否已经默认自带了,
    如果没有,则按照以下步骤安装:
    step1: 打开下载页面https://github.com/protocolbuffers/protobuf/releases
    step2: 选择 protoc-3.19.1-linux-x86_64.zip 进行下载。
    step3: 解压,并为 bin目录下的 protoc文件配置环境变量

  • grpc–>Go 编译器

go get github.com/golang/protobuf/protoc-gen-go
  1. 下载源码
    git clone https://github.com/golang/protobuf
    也可浏览器方式上https://github.com/golang/protobuf下载。
  2. 进入protoc-gen-go源码目录
    cd protobuf/protoc-gen-go
  3. 编译生成protoc-gen-go
    go build -o protoc-gen-go main.go
  4. 复制protoc-gen-go到bin目录
    cp protoc-gen-go /usr/local/go/bin
  5. 使用示例
    protoc --go_out=plugins=grpc:. hello.proto
    如果编译.proto文件时找不到protoc-gen-go,则在编译时会遇到如下错误:
    $protoc --go_out=plugins=grpc:. hello.proto
    protoc-gen-go: program not found or is not executable
    Please specify a program using absolute path or make sure the program is available in your PATH system variable
    –go_out: protoc-gen-go: Plugin failed with status code 1.

下载完成后 ,用命令which protoc-gen-go 查看一下。

例子的文件目录:

├── client
│   └── main.go
├── go.mod
├── go.sum
├── main.go
└── wttproto
    ├── a.pb.go
    └── a.proto
2 proto文件
  • 创建
syntax = "proto3";

option go_package = "./;wttproto";

// 问候
message HiReq {
    string name = 1;
}

message HiRes {
    string msg = 1;
}
// 问候

service WangTanTan {
    rpc SayHi(HiReq) returns (HiRes);
}

说明:
option go_package = "./;testproto"; 该句 只有生成 go文件 时有效,
如果Python 通过此 proto文件 生成python对应的文件时,go_package对其是不产生影响的。
同理还有:option java_package 只对该proto生成java文件时产生影响

  • 创建包目录

go_package说明: ./==》当前目录 的 proto文件;
testproto ==》生成的go文件属于 testproto 包
在工程目录下 创建 testproto目录,将test.proto文件放入

  • 编译
cd wttproto
protoc -I . a.proto --go_out=plugins=grpc:.

注意: 不要 编辑 生成的 a.pb.go文件。

3 服务端
  • main.go 文件
package main

import (
	"context"
	"fmt"
	"net"
	"testgrpcwtt/wttproto"

	"google.golang.org/grpc"
)

type WangTanTan struct{}

func (that *WangTanTan) SayHi(e context.Context, req *wttproto.HiReq) (*wttproto.HiRes, error) {
	return &wttproto.HiRes{
		Msg: "你好 老6, 说的就是你" + req.Name,
	}, nil
}

func main() {
	// 一个服务,两只手
	Server := grpc.NewServer()

	// 左手:注册逻辑
	wttproto.RegisterWangTanTanServer(Server, &WangTanTan{})

	listener, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Listen 0.0.0.0:8080")
	}

	// 右手:连接网络
	err = Server.Serve(listener)
	if err != nil {
		fmt.Println(err)
	}
}
4 客户端
  • /client/main.go 文件
package main

import (
	"context"
	"fmt"
	"testgrpcwtt/wttproto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

var conn *grpc.ClientConn

func init() {
	c, err := grpc.Dial("0.0.0.0:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fmt.Println(err)
	}
	conn = c
}

func main() {
	defer conn.Close()

	client := wttproto.NewWangTanTanClient(conn)
	res, err := client.SayHi(context.Background(), &wttproto.HiReq{Name: "tom"})
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(res.Msg)
}

gRPC流

远程函数调用 中 函数参数返回值 不能太大,否则将严重影响每次调用的 响应时间。
为此,gRPC框架针对 服务器端 和 客户端分 别提供了 流特性
也就是下面 四种模式 中的 第 二、三、四 中模式。

  • 在 proto 文件中 通过 关键字 stream 指定 启用 流特性。
  • 服务端 和 客户端的 流辅助接口 均定义了 SendRecv 方法用于 流数据的双向通信
  • 假设 服务端 循环接收 客户端 发来的 流数据,如果遇到 io.EOF 表示 客户端 流被关闭了,服务端 的 Recv 方法 就会 收到 EOF的 错误,我们的逻辑处理 就可以 根据这个错误 做出 相应处理。

四种模式

1. 简单模式

这是传统模式, 客户端 和 服务端 一问一答, 和RPC没有什么大的区别。
下面的 Go初体验gRPC就采用 简单模式。

2. 服务端 数据流 模式

这种模式, 客户端 发起一次请求, 服务端 返回一段 连续的数据流。

例子:
proto文件:

syntax = "proto3";

option go_package = "./;testproto";

service Greeter {
    rpc GetStream(StreamReqData) returns (stream StreamResData); //服务端模式
}

message StreamReqData {
    string data = 1;
}

message StreamResData {
    string data = 1;
}

服务端:

package main

import (
	"net"
	"service/testproto"
	"time"

	"google.golang.org/grpc"
)

type Server struct{}

func (that *Server) GetStream(req *testproto.StreamReqData, res testproto.Greeter_GetStreamServer) error {
	for i := 0; i < 10; i++ {
		outData := &testproto.StreamResData{
			Data: time.Now().Format("2006-01-02 15:04:05"),
		}
		_ = res.Send(outData)
		time.Sleep(time.Second)
	}
	return nil
}

func main() {
	// 1 实例化server
	g := grpc.NewServer()
	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}

客户端:

package main

import (
	"client/testproto"
	"context"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := testproto.NewGreeterClient(conn)
	res, _ := c.GetStream(context.Background(), &testproto.StreamReqData{Data: "tom"})
	for {
		a, err := res.Recv()
		if err != nil {
			println(err.Error())
			break
		}
		println(a.Data)
	}
}

3. 客户端 数据流 模式

这种模式, 客户端 源源不断的 通过 一个连接 想服务端 发送数据流,
在发送结束后, 服务端 返回一个响应。

例子:
proto文件:

syntax = "proto3";

option go_package = "./;testproto";

service Greeter {
    rpc PutStream(stream StreamReqData) returns (StreamResData); //客户端模式
}

message StreamReqData {
    string data = 1;
}

message StreamResData {
    string data = 1;
}

服务端:

package main

import (
	"net"
	"service/testproto"

	"google.golang.org/grpc"
)

type Server struct{}

func (that *Server) PutStream(cliStr testproto.Greeter_PutStreamServer) error {
	for {
		if a, err := cliStr.Recv(); err != nil {
			println(err.Error())
			break
		} else {
			println(a.Data)
		}
	}
	return nil
}

func main() {
	// 1 实例化server
	g := grpc.NewServer()
	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}

客户端:

package main

import (
	"client/testproto"
	"context"
	"fmt"
	"time"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := testproto.NewGreeterClient(conn)
	putS, _ := c.PutStream(context.Background())
	for i := 0; i < 10; i++ {
		_ = putS.Send(&testproto.StreamReqData{
			Data: fmt.Sprintf("tom %d", i),
		})
		time.Sleep(time.Second)
	}
}

4. 双向 数据流 模式

这种模式, 客户端 和 服务端 都可以想对方发送数据流。
典型的使用场景: 聊天。

  • proto文件
syntax = "proto3";

option go_package = "./;wttproto";

service Greeter {
    rpc GetStream(stream StreamReqData) returns (stream StreamResData); //服务端、客户端 双流模式
}

message StreamReqData {
    string data = 1;
}

message StreamResData {
    string data = 1;
}
// 编译:
// protoc -I . wtt.proto --go_out=plugins=grpc:.
  • 服务端
package main

import (
	"fmt"
	"gprc_test/wttproto"
	"net"
	"time"

	"google.golang.org/grpc"
)

type Server struct{}

func (that *Server) GetStream(res wttproto.Greeter_GetStreamServer) error {
	// 要回的话术
	speak := [3]string{
		"上班哪,咋了?",
		"好的,下班联系你",
		"嗯好",
	}
	for _, val := range speak {
		// 先听
		data, err := res.Recv()
		if err != nil {
			println(err.Error(), "结束会话1")
			break
		}
		fmt.Println("服务端收到==> ", data)

		time.Sleep(time.Second * 3)

		// 再说
		outData := &wttproto.StreamResData{
			Data: val,
		}
		err = res.Send(outData)
		if err != nil {
			println(err.Error(), "结束会话1")
			break
		}
	}
	fmt.Println("结束会话2")
	return nil
}

func main() {
	// 1 实例化server
	g := grpc.NewServer()
	// 2 注册处理逻辑
	wttproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}
  • 客户端
package main

import (
	"context"
	"gprc_test/wttproto"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := wttproto.NewGreeterClient(conn)
	res, err := c.GetStream(context.Background())
	if err != nil {
		panic(err)
	}
	// 要说的话术
	speak := [4]string{
		"在干嘛哪?",
		"下班给我说一声,找你有点事",
		"好,挂了,见面聊",
		"",
	}
	for i := 0; ; i++ { // 有计数的死循环
		// 先说
		err := res.Send(&wttproto.StreamReqData{Data: speak[i]})
		if err != nil {
			println(err.Error(), "结束会话1")
			break
		}

		time.Sleep(time.Second * 3)

		// 再听
		data, err := res.Recv()
		if err != nil {
			println(err.Error(), "结束会话2")
			break
		}
		println("客户端回到 --> ", data.Data)
	}
}

《gRPC 进阶》

gRPC 的 metadata

gRPC的metadata 相当于是 http协议的 header 部分,多用于 权限验证 等工作。
metadata 是以 键值对 的形式存储数据的,key是 string 类型,value是 []string 类型。
http的deader的 生命周期 是一次http请求,
同样,metadata的 生命周期 就是一次RPC调用。

说明:

MD 类型实际上就是 map, key是stirng, 键是[]string
创建metadata的方式:

  1. md := metadata.New(map[string]string{“name”:“tom” , “age”:“10”})
  2. md := metadata.Pairs(“name”,“tom”,“age”,“10”)
    这第二种方式key不区分大小写,会被统一处理为 小写

例子:
proto文件:

syntax = "proto3";

option go_package = "./;testproto";

service Greeter {
    rpc SayHello(HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

客户端:

package main

import (
	"context"
	"gprc_test/testproto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)

	// 发送 metadata数据
	md := metadata.Pairs("name", "tom", "age", "10")
	ctx := metadata.NewOutgoingContext(context.Background(), md)

	// 第一次远程调用
	r, err := c.SayHello(ctx, &testproto.HelloRequest{Name: "cat"})
	if err != nil {
		panic(err)
	}

	println(r.Message)

	md2 := metadata.New(map[string]string{
		"name": "cat",
		"age":  "12",
		"sex":  "woman",
	})
	ctx2 := metadata.NewOutgoingContext(context.Background(), md2)

	// 第二次远程调用
	r, err = c.SayHello(ctx2, &testproto.HelloRequest{Name: "cat2"})
	if err != nil {
		panic(err)
	}
	println(r.Message)
}

服务端:

package main

import (
	"context"
	"fmt"
	"gprc_test/testproto"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

type Server struct{}

func (that *Server) SayHello(ctx context.Context, request *testproto.HelloRequest) (*testproto.HelloReply, error) {
	// 接受 metadata数据
	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		name, has := md["name"]
		if has { // 做一下判断,避免对 获取不都的 字段 进行 索引取值 的 报错
			fmt.Println("Server accept metadata name:", name[0])
		}

		age, has := md["age"]
		if has {
			fmt.Println("Server accept metadata age:", age[0])
		}

		sex, has := md["sex"]
		if has {
			fmt.Println("Server accept metadata sex:", sex[0])
		}
	}

	return &testproto.HelloReply{
		Message: "hello  " + request.Name,
	}, nil
}

func main() {
	// 1 实例化server
	g := grpc.NewServer()
	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}

gRPC 的 拦截器

grpc服务端和客户端都提供了interceptor功能,功能类似middleware,很适合在这里处理验证、日志等流程。
在自定义Token认证的示例中,认证信息是由每个服务中的方法处理并认证的,如果有大量的接口方法,这种姿势就太不优雅了,每个接口实现都要先处理认证信息。这个时候interceptor就可以用来解决了这个问题,在请求被转到具体接口之前处理认证信息,一处认证,到处无忧。

  • proto文件
syntax = "proto3";

option go_package = "./;testproto";

service Greeter {
    rpc SayHello(HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}
  • 服务端
package main

import (
	"context"
	"fmt"
	"net"
	"service/testproto"

	"google.golang.org/grpc"
)

type Server struct{}

func (that *Server) SayHello(ctx context.Context, request *testproto.HelloRequest) (*testproto.HelloReply, error) {
	return &testproto.HelloReply{
		Message: "hello" + request.Name,
	}, nil
}

func main() {
	// 创建 拦截器
	var interceptor grpc.UnaryServerInterceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		fmt.Println("接收到新请求")
		res, err := handler(ctx, req)// 继续处理请求,处理完请求之后再回到此处
        // return handler(ctx, req) // 继续处理请求,处理完请求之后 无需再回到此处
		fmt.Println("请求处理完成")
		return res, err // 结束此次请求

        /*
        如果 想 拦截此次请求,不让进入到 逻辑处理函数中,直接如下处理: 
        return &testproto.HelloReply{
			Message: "已被拦截器拦截",
		}, err
        */
	}
	var opts []grpc.ServerOption
	opts = append(opts, grpc.UnaryInterceptor(interceptor))

	// 1 实例化server
	g := grpc.NewServer(opts...)
	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}
  • 客户端
package main

import (
	"client/testproto"
	"context"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)

	r, err := c.SayHello(context.Background(), &testproto.HelloRequest{Name: "tom"})
	if err != nil {
		panic(err)
	}
	println(r.Message)
}

gRPC 的 字段验证器

因为涉及到 接口之间 的请求,那么对 参数 进行适当的校验是很有必要的。
在这里我们使用 protoc-gen-govalidators 和 go-grpc-middleware 来实现。

  • 先安装:
go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
go get github.com/grpc-ecosystem/go-grpc-middleware
  • proto文件
syntax = "proto3";

// 1 将验证器 引入到 proto文件中
import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";

option go_package = "./;testproto";

service Greeter {
    rpc SayHello(HelloRequest) returns (HelloReply);
}

message HelloRequest {
    // 2 给字段 name 加入正则验证
    string name = 1 [(validator.field) = {regex: "^[z]{2,5}$"}]; //传参 name: zzz,就可以通过验证。
}

message HelloReply {
    string message = 1;
}
  • 编译proto文件
    这里需要特别注意一下,使用之前的简单命令是不行的,需要使用多个 proto_path 参数指定导入 proto 文件的目录。
    官方给了两种依赖情况,一个是 google protobuf,一个是 gogo protobuf。我这里使用的是第二种。
protoc  \
    --proto_path=${GOPATH}/pkg/mod \
    --proto_path=${GOPATH}/pkg/mod/github.com/gogo/protobuf@v1.3.2 \
    --proto_path=. \
    --govalidators_out=. --go_out=plugins=grpc:.\
    *.proto

即使使用上面的命令,也有可能会遇到这个报错:
Import "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors
大概率是引用路径的问题,一定要看好自己的安装版本,以及在 GOPATH 中的具体路径。
以上命令 会生成两个 文件:test.pb.go 和 test.validator.pb.go

  • 服务端
package main

import (
	"context"
	"net"
	"service/testproto"

	"google.golang.org/grpc"

	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
)

type Server struct{}

func (that *Server) SayHello(ctx context.Context, request *testproto.HelloRequest) (*testproto.HelloReply, error) {
	return &testproto.HelloReply{
		Message: "hello" + request.Name,
	}, nil
}

func main() {
	itor1 := grpc.UnaryInterceptor(
		grpc_middleware.ChainUnaryServer(
			grpc_validator.UnaryServerInterceptor(),
		),
	)
	itor2 := grpc.StreamInterceptor(
		grpc_middleware.ChainStreamServer(
			grpc_validator.StreamServerInterceptor(),
		),
	)

	// 1 实例化server
	g := grpc.NewServer(itor1, itor2)

	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}

  • 客户端
package main

import (
	"client/testproto"
	"context"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)
	/*
		r, err := c.SayHello(context.Background(), &testproto.HelloRequest{Name: "tom"})
		报错信息:rpc error: code = InvalidArgument desc = invalid field Name: value 'tom'
			 must be a string conforming to regex "^[z]{2,5}$"
	*/
	r, err := c.SayHello(context.Background(), &testproto.HelloRequest{Name: "zzz"})
	if err != nil {
		println(err)
	}
	println(r.Message)
}

gRPC 的 token验证

有了 验证器 的经验,那么可以采用同样的方式,写一个 拦截器 ,然后在初始化 server 时候注入。

  • 服务端:
package main

import (
	"context"
	"fmt"
	"net"
	"service/testproto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"

	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
)

type Server struct{}

func (that *Server) SayHello(ctx context.Context, request *testproto.HelloRequest) (*testproto.HelloReply, error) {
	return &testproto.HelloReply{
		Message: "hello" + request.Name,
	}, nil
}

// 认证函数
func Auth(ctx context.Context) error {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return fmt.Errorf("missing credentials")
	}

	var user string
	var password string

	if val, ok := md["user"]; ok {
		user = val[0]
	}
	if val, ok := md["password"]; ok {
		password = val[0]
	}

	if user != "admin" || password != "root" {
		return grpc.Errorf(codes.Unauthenticated, "invalid token")
	}

	return nil
}

// 自定义拦截器的拦截规则
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (resp interface{}, err error) {
	//拦截普通方法请求,验证 Token
	err = Auth(ctx)
	if err != nil {
		return
	}
	// 继续处理请求
	return handler(ctx, req)
}

func main() {
	itor1 := grpc.UnaryInterceptor(
		grpc_middleware.ChainUnaryServer(
			authInterceptor,
			grpc_validator.UnaryServerInterceptor(),
		),
	)
	itor2 := grpc.StreamInterceptor(
		grpc_middleware.ChainStreamServer(
			grpc_validator.StreamServerInterceptor(),
		),
	)

	// 1 实例化server
	g := grpc.NewServer(itor1, itor2)

	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})
	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}
  • 客户端
package main

import (
	"client/testproto"
	"context"

	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)

	// 发送 metadata数据,进行token验证
	md := metadata.New(map[string]string{
		"user":     "admin",
		"password": "root",
	})
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	r, err := c.SayHello(ctx, &testproto.HelloRequest{Name: "zzz"})
	if err != nil {
		panic(err)
	}
	println(r.Message)
}

gRPC 的 错误处理

gRPC 使用一组定义明确的状态代码作为 RPC API 的一部分。这些状态定义如下:

CodeNumberDescription
OK0Not an error; returned on success.
CANCELLED1The operation was cancelled, typically by the caller.
UNKNOWN2Unknown error. For example, this error may be returned when a Status value received from another address space belongs to an error space that is not known in this address space. Also errors raised by APIs that do not return enough error information may be converted to this error.
INVALID_ARGUMENT3The client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the system (e.g., a malformed file name).
DEADLINE_EXCEEDED4The deadline expired before the operation could complete. For operations that change the state of the system, this error may be returned even if the operation has completed successfully. For example, a successful response from a server could have been delayed long
NOT_FOUND5Some requested entity (e.g., file or directory) was not found. Note to server developers: if a request is denied for an entire class of users, such as gradual feature rollout or undocumented allowlist, NOT_FOUND may be used. If a request is denied for some users within a class of users, such as user-based access control, PERMISSION_DENIED must be used.
ALREADY_EXISTS6The entity that a client attempted to create (e.g., file or directory) already exists.
PERMISSION_DENIED7The caller does not have permission to execute the specified operation. PERMISSION_DENIED must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED instead for those errors). PERMISSION_DENIED must not be used if the caller can not be identified (use UNAUTHENTICATED instead for those errors). This error code does not imply the request is valid or the requested entity exists or satisfies other pre-conditions.
RESOURCE_EXHAUSTED8Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.
FAILED_PRECONDITION9The operation was rejected because the system is not in a state required for the operation's execution. For example, the directory to be deleted is non-empty, an rmdir operation is applied to a non-directory, etc. Service implementors can use the following guidelines to decide between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call. (b) Use ABORTED if the client should retry at a higher level (e.g., when a client-specified test-and-set fails, indicating the client should restart a read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless the files are deleted from the directory.
ABORTED10The operation was aborted, typically due to a concurrency issue such as a sequencer check failure or transaction abort. See the guidelines above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
OUT_OF_RANGE11The operation was attempted past the valid range. E.g., seeking or reading past end-of-file. Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from an offset past the current file size. There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) when it applies so that callers who are iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are done.
UNIMPLEMENTED12The operation is not implemented or is not supported/enabled in this service.
INTERNAL13Internal errors. This means that some invariants expected by the underlying system have been broken. This error code is reserved for serious errors.
UNAVAILABLE14The service is currently unavailable. This is most likely a transient condition, which can be corrected by retrying with a backoff. Note that it is not always safe to retry non-idempotent operations.
DATA_LOSS15Unrecoverable data loss or corruption.
UNAUTHENTICATED16The request does not have valid authentication credentials for the operation.
  • 服务端
package main

import (
	"context"
	"net"
	"service/testproto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type Server struct{}

func (that *Server) SayHello(ctx context.Context, request *testproto.HelloRequest) (*testproto.HelloReply, error) {
	// return &testproto.HelloReply{
	// 	Message: "hello" + request.Name,
	// }, nil

	// 返回错误的形式:
	return nil, status.Error(codes.InvalidArgument, "参数不可用")

	/* 也可以 使用 format string 的方式
	return nil, status.Errorf(codes.InvalidArgument, "参数不可用:%s", request.Name)
	*/

}
func main() {
	// 1 实例化server
	g := grpc.NewServer()

	// 2 注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})

	// 3 启动server
	lis, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}
	err = g.Serve(lis)
	if err != nil {
		panic(err)
	}
}
  • 客户端
package main

import (
	"client/testproto"
	"context"

	"google.golang.org/grpc"
	"google.golang.org/grpc/status"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)

	r, err := c.SayHello(context.Background(), &testproto.HelloRequest{Name: "zzz"})
	if err != nil {
		st, ok := status.FromError(err)
		if !ok {
			panic("解析error失败")
		}
		println(st.Message())
		println(st.Code())
	} else {
		println(r.Message)
	}
}

/*
输出:
	参数不可用
	3
*/

gRPC 的 超时处理

  • 客户端
package main

import (
	"client/testproto"
	"context"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/status"
)

func main() {
	conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)

	// 加入 超时 控制机制
	ctx, _ := context.WithTimeout(context.Background(), 3*time.Microsecond)

	r, err := c.SayHello(ctx, &testproto.HelloRequest{Name: "zzz"})
	if err != nil {
		st, ok := status.FromError(err)
		if !ok {
			panic("解析error失败")
		}
		println(st.Message())
		println(st.Code())
	} else {
		println(r.Message)
	}
}

/*
超时错误输出:
	context deadline exceeded
	4
*/

gRPC 的 证书认证

gRPC建立在HTTP/2协议之上,对TLS提供了很好的支持。
前面 gRPC的服务 都没有提供 证书支持,因此客户端 中通过grpc.WithInsecure()选项跳过了对服务器证书的验证。
没有 启用证书的gRPC服务 在和 客户端进行的是 明文通讯,信息面临被任何第三方监听的风险。
为了保障gRPC通信不被第三方监听篡改或伪造,我们可以对服务器启动 TLS加密 特性。

服务中心

consul的服务操作

go 操作 consul

  • 服务端
package main

import (
	"context"
	"fmt"
	"gprc_test/testproto"
	"net"

	"github.com/hashicorp/consul/api"
	"google.golang.org/grpc"
)

type Server struct{}

func (that *Server) SayHello(ctx context.Context,
	request *testproto.HelloRequest) (*testproto.HelloReply, error) {
	fmt.Println("被 调用")
	return &testproto.HelloReply{
		Message: "hello  " + request.Name,
	}, nil
}

func main() { // 服务器
	// 1、初始化 consul 配置
	consulConfig := api.DefaultConfig()

	// 2、 创建 consul 对象
	consulClient, err := api.NewClient(consulConfig)
	if err != nil {
		fmt.Println(err)
		panic(err)
	}

	// 3、待注册到 consul的 注册信息对象, 将该微服务 的相关信息 赋值给 注册信息对象
	RegisterObj := api.AgentServiceRegistration{
		ID:      "id_1",                     // id默认为 Name 字段值,若注册id 和 已经注册 的 服务 重复,则会进行覆盖注册
		Tags:    []string{"grpc", "consul"}, // 服务别名
		Name:    "the service name is wtt",  // 当前服务名字
		Address: "127.0.0.1",
		Port:    8080,
		Check: &api.AgentServiceCheck{
			CheckID:  "consul grpc test",
			TCP:      "127.0.0.1:8080", // 注册到 consul的 微服务的 地址
			// HTTP:                           "http://127.0.0.1:8080",
			Timeout:  "1s",
			Interval: "5s",
			DeregisterCriticalServiceAfter: "5s",
		},
	}
	// 4、服务注册(注册完成之后,可以通过 consul的 web ui localhost:8500/ui 查看 服务注册 的 具体服务)
	err = consulClient.Agent().ServiceRegister(&RegisterObj)
	if err != nil {
		fmt.Println(err)
		panic(err)
	} else {
		fmt.Println("服务注册成功")
	}

	// 服务注销
	// err = consulClient.Agent().ServiceDeregister("id_1")
	// if err != nil {
	// 	fmt.Println(err)
	// 	panic(err)
	// } else {
	// 	fmt.Println("服务注销 成功")
	// }

	// 以下是 正常的 微服务 服务代码
	// 创建 grpc服务
	g := grpc.NewServer()

	//  注册处理逻辑
	testproto.RegisterGreeterServer(g, &Server{})

	// 网络
	listener, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		panic(err)
	} else {
		println("端口监听:0.0.0.0:8080")
	}

	// grpc 联网
	err = g.Serve(listener)

	if err != nil {
		panic(err)
	}
}
  • 客户端
package main

import (
	"context"
	"fmt"
	"gprc_test/testproto"

	"github.com/hashicorp/consul/api"
	"google.golang.org/grpc"
)

func main() { // 客户端

	// creds, err := credentials.NewClientTLSFromFile("../sancert/test.pem", "localhost")
	// if err != nil {
	// 	panic(err)
	// }
	// conn, err := grpc.Dial("localhost:8080", grpc.WithTransportCredentials(creds))
	// if err != nil {
	// 	panic(err)
	// }

	// 1. 初始化 consul 配置
	consulConfig := api.DefaultConfig()
	// 2. 创建 consul对象  --(可以重新指定consul属性,ip,port,...,也可以使用默认)
	consulClient, err := api.NewClient(consulConfig)
	if err != nil {
		fmt.Println(err)
		panic(err)
	}



	// 3.1 服务发现, 从consul上获取 ==健康的== 服务     ############## 推荐使用这种方式 #####################
	services, _, err := consulClient.Health().Service("the service name is wtt", "grpc", true, nil)
	if err != nil {
		fmt.Println(err)
		panic(err)
	}
	// 这里可以添加简单的负载均衡,访问压力均摊给集群中的每个服务
	if len(services) == 0 {
		fmt.Println("未发现该服务")
		panic("再见")
	} else {
		fmt.Printf("发现%d个服务 \n", len(services))
	}

	// 从 consul中 获取的 微服务 地址信息
	addrGetFromConsul := fmt.Sprintf("%s:%d", services[0].Service.Address, services[0].Service.Port)
	fmt.Println("从consul中 获取到的信息:", addrGetFromConsul)




	// 3.2 服务发现, 从consul上获取  服务 不管 健不健康   ########################################
	data, err := consulClient.Agent().Services()
	if err != nil {
		panic(err)
	}
	fmt.Printf("%T---%+v\n", data, data)
	fmt.Println(data["id_1"].Address, data["id_1"].Port)




	// 3.2 服务发现, 从consul上获取  指定的 部分服务 ,不管 健不健康   ########################################
	data, err = consulClient.Agent().ServicesWithFilter(`Service == "the service name is wtt"`)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%T---%+v\n", data, data)
	fmt.Println(data["id_1"].Address, data["id_1"].Port)

	// 以下是 正常的 微服务 客户端代码
	conn, err := grpc.Dial(addrGetFromConsul, grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	c := testproto.NewGreeterClient(conn)

	r, err := c.SayHello(context.Background(), &testproto.HelloRequest{Name: "cat"})
	if err != nil {
		panic(err)
	}

	println(r.Message)
}

python 操作 consul

import requests as r

headers = {
    "contentType":"application/json"
}

# 注册一个服务
def register(name, id,address,port):
    url = "http://192.168.3.82:8500/v1/agent/service/register"
    rsp = r.put(url, headers=headers, json={
        "Name": name,
        "ID":id, # 默认为Name的值
        "Tags":["wtt", "man", "gopher"],
        "Address": address,
        "Port": port,
    })
    
    if rsp.status_code == 200:
        print("注册成功")
    else:
        print(f"注册失败:{rsp.status_code}")
    
# 注销一个服务
def deregister(id):
    url = f"http://192.168.3.82:8500/v1/agent/service/deregister/{id}"
    rsp = r.put(url, headers=headers)
    if rsp.status_code == 200:
        print("注销成功")
    else:
        print(f"注销失败:{rsp.status_code}")
        
# 注册一个 带有 http健康检查的 服务
def register_http_health_check(name, id,address,port):
    url = "http://192.168.3.82:8500/v1/agent/service/register"
    rsp = r.put(url, headers=headers, json={
        "Name": name,
        "ID":id, # 默认为Name的值
        "Tags":["wtt", "man", "gopher"],
        "Address": address,
        "Port": port,
        # 配置 健康检查
        "Check": {
            # 要检查的 http 的url地址
            "Http": f"http://{address}:{port}/具体路径",
            # 设置 多长时间算是 超时
            "Timeout": "5s",
            # 多长时间 检查一次
            "Interval": "5s",
            # 如果 检查不健康,一段时间后 自动注销 该服务
            "DeregisterCriticalServiceAfter": "5s",
        }
    })
    
    if rsp.status_code == 200:
        print("注册成功")
    else:
        print(f"注册失败:{rsp.status_code}")
        
# 注册一个 带有 gRPC健康检查的 服务
def register_grpc_health_check(name, id,address,port):
    url = "http://192.168.3.82:8500/v1/agent/service/register"
    rsp = r.put(url, headers=headers, json={
        "Name": name,
        "ID":id, # 默认为Name的值
        "Tags":["wtt", "man", "gopher"],
        "Address": address,
        "Port": port,
        # 配置 健康检查
        "Check": {
            # 只需 address 和 port就可以了, GRPC下 路径 会自动添加
            "GRPC": f"{address}:{port}", 
            "GRPCUserTLS":False, # 不需要使用TLS证书
            # 设置 多长时间算是 超时
            "Timeout": "5s",
            # 多长时间 检查一次
            "Interval": "5s",
            # 如果 检查不健康,一段时间后 自动注销 该服务
            "DeregisterCriticalServiceAfter": "5s",
        }
    })
    
    if rsp.status_code == 200:
        print("注册成功")
    else:
        print(f"注册失败:{rsp.status_code}")
     


if __name__ == "__main__":
    # register("mshop-web", "mshop-web", "127.0.0.1", 50051)
    deregister("mshop-web")
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值