基于protobuf和grpc来实现一个入门级别的微服务

2022-03-30 21:05:28 1105 技术小虫有点萌

什么是微服务

  • 微服务,又称微服务架构,是一种架构风格,它将应用程序构建为以业务领域为模型的小型自治服务集合。就像我们要写一本书,一共有三个章节,为了敏捷开发,我们让三个人 每人负责一个章节去写。那么这么做有什么好处呢?

优点

1.本来一个人写一本书,模块划分之后,每个人都可以参与进去开发,而且每个人都是独立开发的,不受限与第三人,提高开发效率

2.每个人写怎么写都可以,你可以用手机,也可以用ipad ,可以根据不同的适用场景采用不同的技术,也就是混合技术栈。

3.三个人如果有一个人没写完,或者一个人写错了,并不影响另外两个人的进度,也就是故障隔离,降低了开发风险,提高了系统的高可用。

4.每个人都可以根据业务需求,进行功能的扩展和缩减,粒度灵活缩放

缺点(缺点不是不能克服,但是你要为克服缺点付出代价)

那么微服务只有好处吗?有没有带来哪些不便呢?

  • 要求系统提供的服务很大程度上是分离的,为什么?还是刚才那个栗子,我们写一本书,如果你写西游记,一章一个故事,这没有问题,每个人都有一个妖魔鬼怪的故事,但是如果你写水浒传或者红楼梦,很多环节都是承上启下的,做不到场景的分离,就没法实现业务分离。
  • 微服务业务单一,注重单一功能,能读懂上面的例子,这个就很好懂
  • 每个模块由单个团队或者个人开发,增加了人员的交流成本,比如联调,比如编码
  • 当一个请求出现问题,排查错误比较困难,可能每个模块自检都没有问题,但是联调的时候出现各种异常,每个模块都要做好单元测试,增加链路日志
  • 由于各个服务的远程调用而延长了整个请求的时间
  • 数据的事务性和原子性保证,可能由于某种原因,积分模块扣除积分成功,而订单模块导致下单失败

protobuf

  • protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

  • 就像我们平时用到的json,xml 这种。用于数据传输的一种数据格式。但是既然有了json,为什么会再出来一种数据格式呢?google 官方测试报告显示,同一条消息数据,protobuf 序列化后占用空间是 json 的 1/10,xml 的 1/20,但性能却是几十倍。

  • 又称PB编码,但其内部是纯二进制格式,比Json,XML等格式要更精炼,主要用于数据的序列化和反序列化,目前官方提供了JAVA、Python、C++等多种语言的实现。虽然基于文本的序列化程序(XML,JSON)和基于二进制的序列化程序(Protobuf)都可以快速高效(或缓慢)。但是二进制序列化程序具有其天生的优势。 这意味着“好的”二进制序列化程序通常会比“好的”基于文本的序列化程序更快。

  • 我们上代码看一下,具体的下面会有操作

package main

import (
 person "acurd.com/m/proto/gen/go"
 "encoding/json"
 "fmt"
 "github.com/golang/protobuf/proto"
 "log"
)

func main() {
 var user person.User
 user.Id=42
 fmt.Println(&user)
 b,err:=proto.Marshal(&user)//这个就是我们传输的内容,一个二进制流
 if err!=nil {
  log.Fatal(err)
 }
 fmt.Printf("%X\n",b)

 //前面的值假设是我们服务端接到了,开始进行解码
 var user2 person.User
 err=proto.Unmarshal(b,&user2)
 fmt.Println(&user2)
 b,err=json.Marshal(&user2)//转化为json
 fmt.Printf("%s\n",b)
}
  • 执行结果

  • 你也可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

安装和使用

  • 下载地址
  • 根据不同系统下载不同的版本,我是macos
  • 下载完毕后解压,并移动到/usr/local目录
sudo  mv protoc-3.20.0-rc-1-osx-x86_64 /usr/local/protobuf
export PROTOBUF=/usr/local/protobuf
export PATH=$PROTOBUF/bin:$PATH
cd ~
source .bash_profile
  • 我们新开一个窗口,输入protoc,会显示出一些帮助命令
  • 但是我们看到,protoc可以生成 c++,c#,java,php文件,不能生成go文件,所以我们需要安装go的扩展,相关安装地址
go install \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
    google.golang.org/protobuf/cmd/protoc-gen-go \
    google.golang.org/grpc/cmd/protoc-gen-go-grpc
  • 我们来看一下GOBIN目录
  • 我们创建一个proto目录,并在里面定义一个person的结构,文件名为person.proto
//我们定义一个人的数据结构,
syntax="proto3";
package person;
option  go_package="/Users/zhangguofu/website/goproject/proto/gen/go;person";//生成的go文件存放目录在哪;包名叫什么
message Person{
//  1 和2 是说第一个字段是name 第二个字段是age,为了方便后面解析二进制流
  string  name=1;
  int64 age=2;
}
  • 执行相关命令
zhangguofu@zhangguofudeMacBook-Pro proto (main) $ pwd
/Users/zhangguofu/website/goproject/proto
zhangguofu@zhangguofudeMacBook-Pro proto (main) $ mkdir -p gen/go
zhangguofu@zhangguofudeMacBook-Pro proto (main) $ protoc -I . --go_out=paths=source_relative:gen/go person.proto
zhangguofu@zhangguofudeMacBook-Pro proto (main) $ 

  • 此时我们再看一下目录,发现生成了一个pb.go的文件 我们查看一下这个文件,里面主要有这样一段,protoc把我的注释也带来了,而且结构体的成员名称变为了大驼峰,还有json的转换名
type Person struct {
 state         protoimpl.MessageState
 sizeCache     protoimpl.SizeCache
 unknownFields protoimpl.UnknownFields

 //  1 和2 是说第一个字段是name 第二个字段是age,为了方便后面解析二进制流
 // 解释:bytes 指类型; 1指标识符; opt 可选 ;proto3指版本号 ;name是json字段的名字;omitempty意思是说如果字段值为空,该字段在json序列化时则省略
 Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
 Age  int64  `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
}
  • go代码
package main

import (
 person "acurd.com/m/proto/gen/go"
 "encoding/json"
 "fmt"
 "github.com/golang/protobuf/proto"
 "log"
)

func main() {
 var p1 person.Person
 p1.Name="小名"
 p1.Age=18
 fmt.Println(&p1)
 b,err:=proto.Marshal(&p1)//这个就是我们传输的内容,一个二进制流
 if err!=nil {
  log.Fatal(err)
 }
 fmt.Printf("%X\n",b)

 //前面的值假设是我们服务端接到了,开始进行解码
 var p2 person.Person
 err=proto.Unmarshal(b,&p2)
 fmt.Println(&p2)
 b,err=json.Marshal(&p2)//转化为json
 fmt.Printf("%s\n",b)


}

  • 执行结果

proto的写法

参考文章 那么我们具体来看一下proto的写法

  • 我们先来看看proto的类型和go类型的关系
  • 我写了一个demo 供大家参考
//我们定义一个人的数据结构,
//protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = "proto3" 标明版本。
syntax = "proto3";
//package,即包名声明符是可选的,用来防止不同的消息类型有命名冲突。比如两个包有两个Person
package person;
//生成go文件的地址和包名
option  go_package = "/Users/zhangguofu/website/goproject/proto/gen/go;person";

//    消息类型 使用 message 关键字定义 Person是类型名,name age 算是该类型的组成元素,
// 一个 .proto 文件中可以写多个消息类型,即对应多个结构体(struct)。
message Person{
  //每个字符 =后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] 。注意,生成的struct和你定义的顺序是一致的,而不会的按 1  2  3  4 等标识符调整顺序
  //  1 和2 是说第一个字段是name 第二个字段是age,为了方便后面解析二进制流
  string  name = 1;
  int64 age = 2;

}
message User{
  int64 id=1;
}

一个简单的grpc服务参考文章

  • gRPC (gRPC Remote Procedure Calls[1]) 是Google发起的一个开源远程过程调用 (Remote procedure call) 系统。该系统基于 HTTP/2 协议传输,使用Protocol Buffers 作为接口描述语言。
  • gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers
  • 支持多种开发语言

基于grpc实现商品添加和获取

  • 我们先来编写一个proto文件
//我们定义一个人的数据结构,
//protobuf 有2个版本,默认版本是 proto2,如果需要 proto3,则需要在非空非注释第一行使用 syntax = "proto3" 标明版本。
syntax = "proto3";
package goods;
option  go_package = "/Users/zhangguofu/website/goproject/proto/gen/go/goods/;goods";

service GoodsInfo{
  //一个添加商品
  rpc addGoods(Goods) returns(GoodsId);
  //一个获取商品
  rpc getGoods(GoodsId) returns(Goods);

}
//定义商品的消息类型
message Goods{
  string id = 1;
  string name = 2;
  string desc = 3;
}

//定义商品id的消息类型
message GoodsId{
  string value=1;
}

  • 然后执行(注意:不同版本好像命令还不一样,这个问题困扰了我四个小时) 先创建一个目录,用来存放proto编译后的文件mkdir /Users/zhangguofu/website/goproject/proto/gen/go/goods

执行命令

 protoc -I . --go_out=paths=source_relative:gen/go/goods/   --go-grpc_opt=require_unimplemented_servers=false --go-grpc_out==plugins=grpc,paths=source_relative:gen/go/goods/ goods.proto
  • 在相关目录下会生成两个文件

相关数据准备

  • 我们根据proto文件给goods创建一个商品表
CREATE TABLE `an_goods` (
                           `id` varchar(60)  NOT NULL DEFAULT '' COMMENT '业务主键',
                           `name` varchar(20) NOT NULL COMMENT '商品名称',
                           `desc` varchar(32) NOT NULL DEFAULT '' COMMENT '商品描述',
                           PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表'

编写grpc的服务端

  • 注意版本问题 我原来是1.14版本,编译的时候一直报错,里面的报错信息是 这个module 依赖于1.17以上版本,记得多看报错细节
# golang.org/x/net/http2
../../../../go/pkg/mod/golang.org/x/net@v0.0.0-20220127200216-cd36cc0744dd/http2/transport.go:417:45: undefined: os.ErrDeadlineExceeded
note: module requires Go 1.17
  • 注意接口继承关系,如果不完全实现接口的方法也会报错
  • 服务端代码
package main

import (
 "acurd.com/m/proto/gen/go/goods"
 "context"
 "database/sql"
 _ "github.com/go-sql-driver/mysql"
 uuid "github.com/satori/go.uuid"
 "google.golang.org/grpc"
 "google.golang.org/grpc/grpclog"
 "log"
 "net"
)

const (
 Adress = "localhost:9988"
)

type Server struct {
 goods.UnimplementedGoodsInfoServer
}

func (s Server) AddGoods(ctx context.Context, g *goods.Goods) (*goods.GoodsId, error) {
 //参数校验忽略
 db, err := sql.Open("mysql", "acurd:acurd@tcp(127.0.0.1:3306)/test")
 if err != nil {
  grpclog.Fatal(err)
 }
 defer db.Close()

 getUUID := GetUUID()
 name := g.Name
 desc := g.Desc
 sqlStr := "INSERT INTO an_goods (`id`,`name`,`desc`) VALUES (?,?,?)"
 _, err = db.Exec(sqlStr, getUUID, name, desc)
 if err != nil {
  grpclog.Fatal(err)
 }

 goodsId := goods.GoodsId{}
 goodsId.Value = getUUID

 log.Printf("add success ,the id is %s", getUUID)
 return &goodsId, nil
}

func (s Server) GetGoods(ctx context.Context, id *goods.GoodsId) (*goods.Goods, error) {

 db, err := sql.Open("mysql", "acurd:acurd@tcp(127.0.0.1:3306)/test")
 if err != nil {
  grpclog.Fatal(err)
 }
 defer db.Close()

 search_id := id.Value
 g := goods.Goods{}

 sqlStr := "SELECT `id`,`name`,`desc` FROM an_goods WHERE id=?"
 err = db.QueryRow(sqlStr, search_id).Scan(&g.Id, &g.Name, &g.Desc)

 return &g, nil
}

func GetUUID() string {
 u2 := uuid.NewV4()
 return u2.String()
}

var server Server

func main() {
 //绑定监听端口
 listener, err := net.Listen("tcp", Adress)
 if err != nil {
  log.Println("net listen err ", err)
  return
 }
 //创建grpc服务
 s := grpc.NewServer()
 //开始grpc监听
 goods.RegisterGoodsInfoServer(s, &server)
 log.Println("start gRPC listen on Adress " + Adress)

 if err := s.Serve(listener); err != nil {
  log.Println("failed to serve...", err)
  return
 }

}

编写grpc的客户端

package main

import (
 "acurd.com/m/proto/gen/go/goods"
 "context"
 uuid "github.com/satori/go.uuid"
 "google.golang.org/grpc"
 "google.golang.org/grpc/grpclog"
 "log"
)
const (
 Adress = "localhost:9988"
)
func GetUUID() string {
 u2 := uuid.NewV4()
 return u2.String()
}

func main() {
 // 声明监听
 conn, err := grpc.Dial(Adress, grpc.WithInsecure())
 if err!=nil {
  log.Fatal(err)
 }
 defer conn.Close()
 //客户端绑定连接
 client:=goods.NewGoodsInfoClient(conn)
 goodsNew := goods.Goods{
  Name: "apple",
  Desc: "i am an apple",
 }

 //添加一个商品
 res,errs:=client.AddGoods(context.Background(),&goodsNew)
 if errs!=nil{
  grpclog.Error(errs)
 }
 //返回结果
 log.Printf("%+v",res)

 //搜索一个商品
 goodsIdNew:=goods.GoodsId{}
 goodsIdNew.Value=res.Value
 res2,errs2:=client.GetGoods(context.Background(),&goodsIdNew)

 if errs2!=nil{
  grpclog.Error(errs2)
 }
 //返回结果
 log.Printf("%+v",res2)


}


  • 执行结果 server

client