微服务与rpc/grpc

发布时间 2023-03-22 21:18:36作者: Praywu

微服务(micro services)

微服务概述

微服务简介

  • 微(micro)狭义来讲就是体积小,著名的"2 pizza 团队"很好的诠释了这一解释(2 pizza 团队最早是亚马逊 CEO Bezos提出来的,意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来 只需要2个披萨就够了 )。

  • 服务(service)区别于系统,服务一个或者一组相对较小且独立的功能单元,是用户可以感知最小功能集

从广义上来讲,微服务是一种分布式系统解决方案,推动细粒度服务的使用,这些服务协同工作

微服务架构

微服务架构风格是将单个应用程序作为一组小型服务开发的方法,每个服务程序都在自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。这些服务是围绕业务功能构建的。可以通过全自动部署机器独立部署。这些服务器可以用不同的编程语言编写,使用不同的数据存储技术,并尽量不用集中式方式进行管理

  • 微服务架构是将复杂的系统使用组件化的方式进行拆分,并使用轻量级通讯方式进行整合的一种设计方法。

  • 微服务是通过这种架构设计方法拆分出来的一个独立的组件化的小应用。

微服务架构定义的精髓,就是“分而治之,合而用之”。

  • 将复杂的系统进行拆分的方法,就是“分而治之”。分而治之,可以让复杂的事情变的简单

  • 使用轻量级通讯等方式进行整合的设计,就是“合而用之”的方法,合而用之可以让微小的力量变动强大

服务拆分原则:高内聚低耦合

微服务和单体架构

单体架构的问题和缺陷

  • 复杂性逐渐变高:代码越多复杂性越高,越难解决遇到的问题

  • 技术债务逐渐上升:人员流动越大所留下的坑越多,也就是所谓的技术债务越来越多

  • 耦合度太高,维护成本大

  • 持续交付周期长

  • 技术选型成本高:单块架构倾向于采用统一的技术平台或方案来解决所有问题,如果后续想引入新的技术或框架,成本和风险都很大

  • 可扩展性差:随着功能的增加,垂直扩展的成本将会越来越大;而对于水平扩展而言,因为所有代码都运行在同一个进程,没办法做到针对应用程序的部分功能做独立的扩展

微服务架构的解决方案

单一职责

微服务架构中的每个服务,都是具有业务逻辑的,符合高内聚、低耦合原则以及单一职责原则的单元,不同的服务通过“管道”的方式灵活组合,从而构建出庞大的系统。

轻量级通信

服务之间通过轻量级的通信机制实现互通互联,而所谓的轻量级,通常指语言无关、平台无关的交互方式。

  • 对于轻量级通信的格式而言,例如 XML 和 JSON,它们是语言无关、平台无关的

  • 对于通信的协议而言,通常基于 HTTP,能让服务间的通信变得标准化、无状态化

使用轻量级通信机制,可以让团队选择更适合的语言、工具或者平台来开发服务本身。

独立性

每个服务在应用交付过程中,独立地开发、测试和部署

在单体式架构中:所有功能都在同一个代码库,功能的开发不具有独立性;当不同小组完成多个功能后,需要经过集成和回归测试,测试过程也不具有独立性;当测试完成后,应用被构建成一个包,如果某个功能存在 bug,将导致整个部署失败或者回滚

在微服务架构中:每个服务都是独立的业务单元,与其他服务高度解耦,只需要改变当前服务本身,就可以完成独立的开发、测试和部署

进程隔离

单块架构中,整个系统运行在同一个进程中,当应用进行部署时,必须停掉当前正在运行的应用,部署完成后再重启进程,无法做到独立部署。

在微服务架构中,应用程序由多个服务组成,每个服务都是高度自治的独立业务实体,可以运行在独立的进程中,不同的服务能非常容易地部署到不同的主机上。

微服务架构的缺陷

  1. 运维要求高,难度大

  2. 分布式的复杂性,导致bug不好调试

  3. 接口成本高:一旦微服务的接口发生大的变动,那么所有依赖它的微服务都要做相应的调整,由于微服务可能非常多,那么调整接口造成的成本会很高。

  4. 重复劳动:对于单体架构来讲,如果某段业务被多个模块所共同使用,便可以抽象成一个工具类,被所有模块直接调用,但是微服务却无法这样做,因为这个微服务的工具类是不能被其它微服务所直接调用的,从而我们便不得不在每个微服务上都建这么一个工具类,从而导致代码的重复。

  5. 业务不好分离

单体架构和微服务架构的对比

新功能开发所需要时间开发和实现的难易度
  传统单体架构 分布式微服务化架构
部署 不经常而且容易部署 经常发布,部署复杂
隔离性 故障影响范围大 故障影响范围小
架构设计 初期技术选型难度大 设计逻辑难度大
系统性能 相对时间快,吞吐量小 相对时间慢,吞吐量大
系统运维 运维难度简单 运维难度复杂
新人上手 学习曲线大(应用逻辑) 学习曲线大(架构逻辑)
技术 技术单一而且封闭 技术多样而且容易开发
测试和差错 简单 复杂(每个服务都要进行单独测试,还需要集群测试)
系统扩展性 扩展性差 扩展性好
系统管理 重点在于开发成本 重点在于服务治理和调度

RPC协议

RPC概述

RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数

使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图:

这样的话,如果没有统一的服务框架(RPC框架),各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。所以,统一RPC框架把上述“业务之外”的技术劳动统一处理,是服务化首要解决的问题。

go语言中使用RPC

Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。

golang 中的类型比如:channel(通道)、complex(复数类型)、func(函数)均不能进行 序列化

Server端

 func main(){
  // rpc注册服务, 注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
  rpc.RegisterName("HelloService", new(HelloService))
 
  // 设置服务监听
  listener,err := net.Listen("tcp","127.0.0.1:8888")
  if err != nil {
  panic(err)
  }
 
  // 接受传输的数据
  conn,err := listener.Accept()
  if err != nil {
  panic(err)
  }
 
  // rpc调用,并返回执行后的数据
     //   1.read,获取服务名称和方法名,获取请求数据
     //   2.调用对应服务里面的方法,获取传出数据
     //   3.write,把数据返回给client
  rpc.ServeConn(conn)
 }
  1. rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下

  2. 然后建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务

Client端

 func main(){
     //用rpc连接
  client,err := rpc.Dial("tcp","127.0.0.1:8888")
  if err != nil {
  panic(err)
  }
 
  var reply string
     //调用服务中的函数
  err = client.Call("HelloService.Hello","world",&reply)  // 这里的HelloServer就是Server注册的服务名,然后Hello就是这个服务下的Hello方法
  if err != nil {
  panic(err)
  }
 
  fmt.Println("收到的数据为,",reply)
 }
  • 首选是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法

  • 在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别定义RPC方法的两个参数。

跨语言的RPC

标准库的RPC默认采用Go语言特有的gob编码。因此,其它语言调用Go语言实现的RPC服务将比较困难,但是可以通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言RPC服务端和客户端。

无论采用何种语言,只要遵循同样的json结构,以同样的流程就可以和Go语言编写的RPC服务进行通信,这样就可以用json实现了跨语言的RPC。但是除了用json做跨语言的RPC服务之外,还会选用protobuf做跨语言的RPC服务。

基于json编码重新实现RPC服务端

 func main(){
  // rpc注册服务, 注册rpc服务,维护一个hash表,key值是服务名称,value值是服务的地址
  rpc.RegisterName("HelloService", new(HelloService))
 
  // 设置服务监听
  listener,err := net.Listen("tcp","127.0.0.1:8888")
  if err != nil {
  panic(err)
  }
 
  for {
         // 接受传输的数据
         conn,err := listener.Accept()
         if err != nil {
             panic(err)
        }
 
         //给当前连接提供针对json格式的rpc服务
         go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
         // 用rpc.ServeCodec函数替代了rpc.ServeConn函数,传入的参数是针对服务端的json编解码器
    }
 }

json版本的RPC客户端

先手工调用net.Dial函数建立TCP链接,然后基于该链接建立针对客户端的json编解码器

 func main(){
     //用rpc连接
  conn,err := rpc.Dial("tcp","127.0.0.1:8888")
  if err != nil {
  panic(err)
  }
 
  // 建立基于json编解码的rpc服务
  client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
 
  var reply string
     //调用服务中的函数
  err = client.Call("HelloService.Hello","world",&reply)
  if err != nil {
  panic(err)
  }
 
  fmt.Println("收到的数据为,",reply)
 }

RPC协议封装

未封装的代码服务名都是写死的,不够灵活,且容易写错,所以这里对RPC的服务端和客户端再次进行一次封装,来屏蔽掉服务名。

服务端封装

 package main
 
 import (
  "fmt"
  "net"
  "net/rpc"
  "net/rpc/jsonrpc"
 )
 
 var ServiceName = "login"
 
 // 定义一个接口,作用是限定我们自己定义的RegisterRPC函数的参数类型,以及它要实现的方法
 type RPCDesign interface {
  Say(string, *string) error
 }
 
 // 定义要注册到RPC的类和方法,这里Persion实现的了Say方法,所以它可以作为RegisterRPC的参数
 type Person struct{}
 
 func (p *Person) Say(in string, out *string) error {
  *out = in + "hahahahahaha"
  return nil
 }
 
 // 把RPC的注册方法再封装一层
 func RegisterRPC(srv RPCDesign) error {
  return rpc.RegisterName(ServiceName, srv)
 }
 
 func main() {
  // 设置监听
  Listener, err := net.Listen("tcp", "127.0.0.1:9999")
  if err != nil {
  fmt.Println("listen failed, err: ", err)
  return
  }
  defer Listener.Close()
  fmt.Println("Start Listening...")
 
  for {
  // 开始接受连接
  conn, err := Listener.Accept()
  if err != nil {
  fmt.Println("connection err: ", err)
  return
  }
  defer conn.Close()
  fmt.Println(conn.RemoteAddr().String() + "connect success.")
 
  // 注册RPC
  err = RegisterRPC(new(Person))
  if err != nil {
  fmt.Println("Regist RPC failed, err: ", err)
  return
  }
 
  // 把rpc服务和套接字绑定
  rpc.ServeConn(conn)
 
  // 跨语言的版本,使用json编码
  //rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
  }
 }
 

客户端封装

 package main
 
 import (
  "fmt"
  "net/rpc"
 )
 
 // rpc.Dial是*rpc.Client的方法,所以这里吧*rpc.client封装起来
 type RPCClient struct {
  client *rpc.Client
 }
 
 // 创建RPCClient的构造方法
 func NewRPCClient(addr string) *RPCClient {
  conn, err := rpc.Dial("tcp", addr)
  if err != nil {
  fmt.Println("connect failed, err: ", err)
  return new(RPCClient)
  }
  return &RPCClient{client: conn}
 }
 
 // 为RPCClient添加一个CallFunc来对Server端调用
 func (r *RPCClient) CallFunc(req string, rest *string) (err error) {
  err = r.client.Call("login.Say", req, rest)
  if err != nil {
  fmt.Println("call failed, err: ", err)
  return
  }
  return
 }
 
 func main() {
  client := NewRPCClient("127.0.0.1:9999")
  var res string
  client.CallFunc("hgzerowzh", &res)
  fmt.Println("receive value: ", res)
 }

protobuf

protobuf概述

protobuf简介

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化 。它很适合做数据存储RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

  1. protobuf是类似与json一样的数据描述语言(数据格式)

  2. protobuf非常适合于RPC数据交换格式

注意:protobuf本身并不是和gRPC绑定的。它也可以被用于非RPC场景,如存储等

protobuf的优劣势

1)优势:

  1. 序列化后体积相比Json和XML很小,适合网络传输

  2. 序列化反序列化速度很快,快于Json的处理速度

  3. 消息格式升级和兼容性还不错

  4. 支持跨平台多语言

2)劣势:

  1. 应用不够广(相比xml和json)

  2. 二进制格式导致可读性差

  3. 缺乏自描述

protoc安装(windows)

protoc就是protobuf的编译器,它把proto文件编译成不同的语言

下载安装protoc编译器(protoc)

下载protobuf:https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-win64.zip

解压后,将目录中的 bin 目录的路径添加到系统环境变量,然后打开cmd输入protoc查看输出信息,此时则安装成功

安装protocbuf的go插件(protoc-gen-go)

由于protobuf并没直接支持go语言需要我们手动安装相关插件

protocol buffer编译器需要一个插件来根据提供的proto文件生成 Go 代码,Go1.16+要使用下面的命令安装插件:

 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest  // 目前最新版是v1.3.0

安装grpc(grpc)

 go get -u -v google.golang.org/grpc@latest    // 目前最新版是v1.53.0

安装grpc的go插件(protoc-gen-go-grpc)

说明:在google.golang.org/protobuf中,protoc-gen-go纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能,所以如果要生成grpc相关的代码需要安装grpc-go相关的插件:protoc-gen-go-grpc

 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是v1.3.0

protobuf语法

protobuf语法概述

  • 类型:类型不仅可以是标量类型(intstring等),也可以是复合类型(enum等),也可以是其他message

  • 字段名:字段名比较推荐的是使用下划线/分隔名称

  • 字段编号:一个message内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名

  • 字段规则:消息字段可以是以下字段之一

    • singular:格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则

    • optional:与 singular 相同,不过可以检查该值是否明确设置

    • repeated:在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序

    • map:这是一个成对的键值对字段

  • 保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,编译器就会报错

简单语法

proto文件基本语法

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
 }
 
 enum test{
  int32 age = 0;
 }

protobuf消息的定义(或者称为描述)通常都写在一个以 .proto 结尾的文件中:

  1. 第一行指定正在使用proto3语法:如果不这样做,协议缓冲区编译器将假定正在使用proto2(这也必须是文件的第一个非空的非注释行)

  2. 第二行package指明当前是pb包(生成go文件之后和Go的包名保持一致)

  3. message关键字定义一个Person消息体,类似于go语言中的结构体,是包含一系列类型数据的集合。

    • 许多标准的简单数据类型都可以作为字段类型,包括boolint32floatdouble,和string

    • 也可以使用其他message类型作为字段类型。

在message中有一个字符串类型的value成员,该成员编码时用1代替名字。在json中是通过成员的名字来绑定对应的数据,但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,能够快速传输,缺点是不利于阅读。

message常见的数据类型与go中类型对比

.proto类型Go类型介绍
double float64 64位浮点数
float float32 32位浮点数
int32 int32 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。
int64 int64 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。
uint32 uint32 使用可变长度编码。
uint64 uint64 使用可变长度编码。
sint32 int32 使用可变长度编码。符号整型值。这些比常规int32s编码负数更有效。
sint64 int64 使用可变长度编码。符号整型值。这些比常规int64s编码负数更有效。
fixed32 uint32 总是四字节。如果值通常大于228,则比uint 32更有效
fixed64 uint64 总是八字节。如果值通常大于256,则比uint64更有效
sfixed32 int32 总是四字节。
sfixed64 int64 总是八字节。
bool bool 布尔类型
string string 字符串必须始终包含UTF - 8编码或7位ASCII文本
bytes []byte 可以包含任意字节序列

protobuff语法进阶

message嵌套

messsage除了能放简单数据类型外,还能存放另外的message类型:

 syntax = "proto3";          // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
     string name = 1;  // 名字
     int32  age = 2 ;  // 年龄
     // 定义一个message
     message PhoneNumber {
    string number = 1;
    int64 type = 2;
  }
  PhoneNumber phone = 3;
 }

message成员编号,可以不从1开始,但是不能重复,不能使用19000 - 19999

repeated关键字

repeadted关键字类似与go中的切片,编译之后对应的也是go的切片,用法如下:

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
     // 定义一个message
     message PhoneNumber {
    string number = 1;
    int64 type = 2;
  }
  repeated PhoneNumber phone = 3;
 }

默认值

解析数据时,如果编码的消息不包含特定的单数元素,则解析对象对象中的相应字段将设置为该字段的默认值

不同类型的默认值不同,具体如下:

  • 对于字符串,默认值为空字符串

  • 对于字节,默认值为空字节

  • 对于bools,默认值为false

  • 对于数字类型,默认值为零

  • 对于枚举,默认值是第一个定义的枚举值,该值必须为0

  • repeated字段默认值是空列表

  • message字段的默认值为空对象

enum关键字

在定义消息类型时,可能会希望其中一个字段有一个预定义的值列表

比如说,电话号码字段有个类型,这个类型可以是,home,work,mobile

我们可以通过enum在消息定义中添加每个可能值的常量来非常简单的执行此操作。示例如下:

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
     // 定义一个message
     message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
 
  repeated PhoneNumber phone = 3;
 }
 
 // enum为关键字,作用为定义一种枚举类型
 enum PhoneType {
  MOBILE = 0;
     HOME = 1;
     WORK = 2;
 }

如上,enum的第一个常量映射为0,每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:

  • 必须有一个零值,以便我们可以使用0作为数字默认值。

  • 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。

enum还可以为不同的枚举常量指定相同的值来定义别名。如果想要使用这个功能必须将allow_alias选项设置为true,负责编译器将报错。示例如下:

 syntax = "proto3";              // 指定版本信息,不指定会报错
 package pb; // 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
     string name = 1;   // 名字
     int32  age = 2 ;   // 年龄
     // 定义一个message
     message PhoneNumber {
         string number = 1;
         PhoneType type = 2;
    }
     repeated PhoneNumber phone = 3;
 }
 
 // enum为关键字,作用为定义一种枚举类型
 enum PhoneType {
  // 如果不设置将报错
     option allow_alias = true;
     MOBILE = 0;
     HOME = 1;
     WORK = 2;
     Personal = 2;
 }

oneof关键字

如果有一个包含许多字段的消息,并且最多只能同时设置其中的一个字段,则可以使用oneof功能,示例如下:

 message Person{
     string name = 1; // 名字
     int32  age = 2 ; // 年龄
     //定义一个message
     message PhoneNumber {
         string number = 1;
         PhoneType type = 2;
    }
 
     repeated PhoneNumber phone = 3;
     oneof data{
         string school = 5;
         int32 score = 6;
    }
 }

定义RPC服务

如果需要将message与RPC一起使用,则可以在.proto文件中定义RPC服务接口,protobuf编译器将根据你选择的语言生成RPC接口代码。示例如下:

 //定义RPC服务
 service HelloService {
     rpc Hello (Person)returns (Person);
 }

注意:默认protobuf编译期间,不编译服务,如果要想让其编译,需要使用gRPC

protobuf编译

编译器调用

protobuf 编译是通过编译器 protoc 进行的,通过这个编译器,我们可以把 .proto 文件生成 go,Java,Python,C++, Ruby或者C# 代码

可以使用以下命令来通过 .proto 文件生成go代码(以及grpc代码)

 // 将当前目录中的所有 .proto文件进行编译生成go代码
 protoc --go_out=./ --go_opt=paths=source_relative *.proto

protobuf 编译器会把 .proto 文件编译成 .pd.go 文件

--go_out 参数

作用:指定go代码生成的基本路径

  1. protocol buffer编译器会将生成的Go代码输出到命令行参数go_out指定的位置

  2. go_out标志的参数是你希望编译器编写 Go 输出的目录

  3. 编译器会为每个.proto 文件输入创建一个源文件

  4. 输出文件的名称是通过将.proto 扩展名替换为.pb.go 而创建的

--go_opt 参数

protoc-gen-go提供了--go_opt参数来为其指定参数,可以设置多个:

  1. paths=import:生成的文件会按go_package路径来生成,当然是在--go_out目录

    • 例如,go_out/$go_package/pb_filename.pb.go

    • 如果未指定路径标志,这就是默认输出模式

  2. paths=source_relative:输出文件与输入文件放在相同的目录中

    • 例如,一个protos/buzz.proto输入文件会产生一个位于protos/buzz.pb.go的输出文件。

  3. module=$PREFIX:输出文件放在以 Go 包的导入路径命名的目录中,但是从输出文件名中删除了指定的目录前缀。

    • 例如,输入文件 pros/buzz.proto,其导入路径为 example.com/project/protos/fizz 并指定example.com/projectmodule前缀,结果会产生一个名为 pros/fizz/buzz.pb.go 的输出文件。

    • 在module路径之外生成任何 Go 包都会导致错误,此模式对于将生成的文件直接输出到 Go 模块非常有用。

--proto_path 参数

--proto_path=IMPORT_PATH

  • IMPORT_PATH是 .proto 文件所在的路径,如果忽略则默认当前目录。

  • 如果有多个目录则可以多次调用--proto_path,它们将会顺序的被访问并执行导入。

使用示例:

 protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
 // 编译器将从 `src` 目录中读取输入文件 `foo.proto` 和 `bar/baz.proto`,并将输出文件 `foo.pb.go` 和 `bar/baz.pb.go` 写入 `out` 目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身

使用grpc的go插件

安装proto-gen-go-grpc

google.golang.org/protobuf中,protoc-gen-go纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能。生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc

 // 安装protoc-gen-go-grpc
 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是v1.3.0

生成grpc的go代码:

 // 主要是--go_grpc_out参数会生成go代码
 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative  *.proto

--go-grpc_out 参数

作用:指定grpc go代码生成的基本路径

命令会产生的go文件:

  1. protoc-gen-go:包含所有类型的序列化和反序列化的go代码

  2. protoc-gen-go-grpc:包含service中的用来给client调用的接口定义以及service中的用来给服务端实现的接口定义

--go-grpc_opt 参数

protoc-gen-go类似,protoc-gen-go-grpc提供 --go-grpc_opt 来指定参数,并可以设置多个

github.com/golang/protobufgoogle.golang.org/protobuf

github.com/golang/protobuf

  1. github.com/golang/protobuf 现在已经废弃

  2. 它可以同时生成pb和gRPC相关代码的

用法:

 // 它在--go_out加了plugin关键字,paths参数有两个选项,分别是 import 和 source_relative
 --go_out=plugins=grpc,paths=import:.  *.proto

google.golang.org/protobuf

  1. github.com/golang/protobuf的升级版本,v1.4.0之后github.com/golang/protobuf仅是google.golang.org/protobuf的包装

  2. 它纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能,生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc

用法:

 // 它额外添加了参数--go-grpc_out以调用protoc-gen-go-grpc插件生成grpc代码
 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative  *.proto

 

GRPC协议

grpc概述

gRPC是谷歌开源的一款高性能、支持多种开发语言的服务框架

序列化协议gRPC使用protobuf,首先使用protobuf定义服务,然后使用这个文件来生成客户端和服务端的代码。因为pb是跨语言的,因此即使服务端和客户端语言并不一致也是可以互相序列化和反序列化的

网络传输层。gRPC使用http2.0协议,http2.0相比于HTTP 1.x ,大幅度的提升了 web 性能

grpc的安装

由于通常都是配合 gRPC 来使用 protobuf ,所以我们也需要基于.proto文件生成Go代码的同时生成 gRPC 代码。

生成 gRPC 代码需要先安装grpc以及 protoc-gen-go-grpc 插件。

 // 安装grpc
 go get -u -v google.golang.org/grpc@latest    // 目前最新版是v1.53.0
 
 // 安装protoc-gen-go-grpc插件
 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是目前最新版是v1.3.0

grpc的使用

检查对应依赖的插件

 PS E:\go_learning\gogogo\20-rpc-grpc\02-grpc\pb> protoc --version
 libprotoc 3.19.4
 PS E:\go_learning\gogogo\20-rpc-grpc\02-grpc\pb> protoc-gen-go-grpc --version
 protoc-gen-go-grpc 1.3.0
 PS E:\go_learning\gogogo\20-rpc-grpc\02-grpc\pb> protoc-gen-go --version
 protoc-gen-go.exe v1.30.0
 PS E:\go_learning\gogogo\20-rpc-grpc\02-grpc\pb> go list -m google.golang.org/grpc
 google.golang.org/grpc v1.53.0

生成grpc的go代码

编译

站在Protobuf的角度来看,GRPC只不过是一个针对service接口生成代码的生成器。

1)定义 .proto 文件

 syntax = "proto3";
 package pb;
 
 option go_package = "./grpc";
 
 message Teacher {
   int32 age = 1;
   string name = 2;
 }
 
 // 定义RPC服务
 service  SayName {
   rpc SayHello (Teacher) returns (Teacher);
 }

2)对 .proto 文件进行编译

进入到刚刚定义的 .proto 文件的同级目录后,执行以下命令进行编译:

 protoc --go_out=./ --go-grpc_out=./ *.proto

对生成文件进行分析

在以 _grpc.pb.go 结尾的文件中,GRPC插件会为服务端和客户端生成不同的接口:

 // 客户端接口
 type SayNameClient interface {
  SayHello(ctx context.Context, in *Teacher, opts ...grpc.CallOption) (*Teacher, error)
 }
 
 // 服务端接口
 type SayNameServer interface {
  SayHello(context.Context, *Teacher) (*Teacher, error)
  mustEmbedUnimplementedSayNameServer()
 }

后续可以根据这两个接口重新实现SayName服务:

 // 定义一个类(结构体)
 type Children struct {
  pb.UnimplementedSayNameServer   // 注意这里需要继承这个类
 }
 
 // 按接口(SayNameServer接口)绑定类方法
 func (c *Children) SayHello(ctx context.Context, t *pb.Teacher) (*pb.Teacher, error) {
  t.Name += " hahahahahaha"
  return t, nil
 }

编写grpc服务端和客户端

grpc服务端

 package main
 
 import (
  "context"
  "fmt"
  pb "gogogo/20-rpc-grpc/02-grpc/pb/grpc"
  "google.golang.org/grpc"
  "net"
 )
 
 // 定义一个类(结构体)
 type Children struct {
  pb.UnimplementedSayNameServer
 }
 
 // 按接口(SayNameServer接口)绑定类方法
 func (c *Children) SayHello(ctx context.Context, t *pb.Teacher) (*pb.Teacher, error) {
  t.Name += " hahahahahaha"
  return t, nil
 }
 
 func main() {
  // 1.初始化一个grpc对象
  grpcServer := grpc.NewServer()
  // 2.注册服务
  pb.RegisterSayNameServer(grpcServer, new(Children))
  // 3.设置监听, 指定IP、Port
  listener, err := net.Listen("tcp", "127.0.0.1:9999")
  if err != nil {
  fmt.Println("listen failed, err: ", err)
  return
  }
  fmt.Println("Start Listening...")
  defer listener.Close()
 
  // 4.启动服务
  err = grpcServer.Serve(listener)
  if err != nil {
  fmt.Println("Err: ", err)
  return
  }
 }
 

grpc客户端

 package main
 
 import (
  "context"
  "fmt"
  pb "gogogo/20-rpc-grpc/02-grpc/pb/grpc"
  "google.golang.org/grpc"
 )
 
 func main() {
  // 1. 连接grpc服务
  grpcConn, err := grpc.Dial("127.0.0.1:9999", grpc.WithInsecure())  // 注意加这个grpc.WithInsecure()
  if err != nil {
  fmt.Println("connect grpc service failed, err: ", err)
  return
  }
  defer grpcConn.Close()
 
  // 2.初始化grpc客户端
  grpcClient := pb.NewSayNameClient(grpcConn)
 
  // 创建并初始化Teacher对象
  var teacher pb.Teacher
  teacher.Name = "hgzero"
  teacher.Age = 18
 
  // 3.调用远程服务
  t, err := grpcClient.SayHello(context.TODO(), &teacher)
  if err != nil {
  fmt.Println("Err: ", err)
  return
  }
  fmt.Println("Value: ", t)
 }