【Go】十六、protobuf构建基础服务信息、grpc服务启动的基础信息
商品服务
服务结构
创建 goods 服务,将之前 user 服务的基本结构迁移到 goods 服务上,完整目录是:
mxshop_srvs
user_srv
…
tmp
…
goods_srv
config
config.go 配置的读取表
global
global.go 数据库、日志初始化、全局变量定义
handler
xxx.go 拦截器
initialize
xxx.go 初始化信息
model
xxx.go 数据库表、数据库对象
proto proto 相关信息
xxx.proto
xxx.pb.go
xxx_grpc.pb.go
tests 测试信息
utils
xxx.go 工具
config-debug.yaml 配置信息
main.go 启动类
数据表结构
在 model 目录中创建需要的表结构对应的数据对象:
- 创建基础数据类:base.go:
package model
import (
"gorm.io/gorm"
"time"
)
type BaseModel struct {
ID int32 `gorm:"primarykey";type:int` // 注意这里对应数据库的int,我们进行统一定义,避免出现问题,若数据量过大也可以采用bigint
CreatedAt time.Time `gorm:"column:add_time"`
UpdatedAt time.Time `gorm:"column:update_time"`
DeletedAt gorm.DeletedAt
IsDeleted bool
}
- 创建商品表
package model
// 商品分类数据对象:一级分类、二级分类...
type Category struct {
BaseModel
Name string `gorm:"type:varchar(20);not null;"` // 分类名
ParentCategoryID int32 // 父分类ID
ParentCategory *Category // 父分类对象 此处因为是自己指向自己,必须使用指针
Level int32 `gorm:"type:int;not null;default:1"` // 分类级别
IsTab bool `gorm:"default:false;not null"` // 是否显示在 Tab 栏
}
// 品牌数据对象
type Brands struct {
BaseModel
Name string `gorm:"type:varchar(20);not null"`
Logo string `gorm:"type:varchar(200);default:'';not null"`
}
// 品牌 / 类型 对应表
type GoodsCategoryBrand struct {
BaseModel
CategoryID int32 `gorm:"type:int;index:idx_category_brand,unique"`
Category Category
BrandsID int32 `gorm:"type:int;index:idx_category_brand,unique"`
Brands Brands
}
// 轮播图数据对象
type Banner struct {
BaseModel
Image string `gorm:"type:varchar(200);not null"`
Url string `gorm:"type:varchar(200);not null"`
Index int32 `gorm:"type:int;default:1;not null"`
}
- 商品对象的创建,注意:商品对象的创建较为复杂,单独拎出来处理:
// 商品表
type Goods struct {
BaseModel
CategoryID int32 `gorm:"type:int;not null"`
Category Category
BrandsID int32 `gorm:"type:int;not null"`
Brands Brands
OnSale bool `gorm:"default:false;not null"` // 是否上架
ShipFree bool `gorm:"default:false;not null"` // 是否xxx
ISNew bool `gorm:"default:false;not null""`
IsHot bool `gorm:"default:false;not null"`
Name string `gorm:"type:varchar(50);not null"`
GoodsSn string `gorm:"type:varchar(50);not null"`
ClickNum int32 `gorm:"type:int;default:0;not null"`
SoldNum int32 `gorm:"type:int;default:0;not null"`
FavNum int32 `gorm:"type:int;default:0;not null"`
MarketPrice float32 `gorm:"not null"`
ShopPrice float32 `gorm:"not null"`
GoodsBrief string `gorm:"type:varchar(100);not null"`
Images GormList `gorm:"type:varchar(1000);not null"`
DescImages GormList `gorm:"type:varchar(1000);not null"`
GoodsFrontImage string `gorm:"type:varchar(200);not null"`
}
这里需要注意的是 对于 图片列表的处理,我们单独存储一个图片是没问题的,但是如果需要存储多个图片的话,我们就有两种方式选择了:
- 建立一个图片表,表里是所有的图片,每个图片存储一个归属商品,但这样的缺陷是无法避免连表操作,到后期数据量极大的时候,这种程度的连表能够造成极大的性能隐患。
- 直接将图片路径形成 json 格式的字符串,存储在表中,在 代码中通过 marshal 和 unmarshal 进行编码和解码,再进行图片的存取,这种方式有效规避了连表带来的性能损耗。
故而这里选用第二种方式。
这里就需要我们在 model/Base.go 中添加编解码的工具代码
type GormList []string
// 设定这种变量在插入数据库的时候怎么插入:
// 这里是 将json格式的内容转换为 字符串再进行插入
func (g GormList) Value() (driver.Value, error) {
return json.Marshal(g)
}
// 设定在从数据库中取数据时,自动将数据转换为 []string 的列表
// 从数据库中取出来的时候是 GormList 数据类型,并将它的地址传入这个方法,直接修改其地址中的内容,将其修改为 []string
func (g *GormList) Scan(value interface{}) error {
return json.Unmarshal(value.([]byte), &g)
}
type BaseModel struct {
ID int32 `gorm:"primarykey";type:int` // 注意这里对应数据库的int,我们进行统一定义,避免出现问题,若数据量过大也可以采用bigint
CreatedAt time.Time `gorm:"column:add_time"`
UpdatedAt time.Time `gorm:"column:update_time"`
DeletedAt gorm.DeletedAt
IsDeleted bool
}
这样我们存入的数据或者取出的数据只要是 定义为了 GormList 格式,就会在存入时自动转为字符串,取出是自动转为 json
之后进行数据库创建操作
这里我们默认数据库自行创建完成了,利用 gorm 来建表:
goods_srv/model/main/main.go
package main
import (
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"mxshop_srvs/goods_srv/model"
)
// 这里的代码是用来在数据库中建表的
func main() {
dsn := "root:123456@tcp(192.168.202.140:3306)/mxshop_goods_srv?charset=utf8mb4&parseTime=True&loc=Local"
// 添加日志信息
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color,true is colorful, false to black and white
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 阻止向创建的数据库表后添加复数
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
// 将日添加到配置中
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 建表
_ = db.AutoMigrate(&model.Category{}, &model.Brands{}, &model.GoodsCategoryBrand{}, &model.Banner{}, &model.Goods{})
}
这里相当于是一个商品的单元测试,用来做一些一次性的事情
protobuf 数据定义、定义所有的接口和请求和返回信息
注意这里,一定是先确定好需要的所有的需要的接口信息,再进行后续的接口定义操作
下面是全量的详细proto信息:
syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;proto";
// 在这里定义一个个的接口
service Goods {
// 商品部分
// 获取商品的接口,包括条件获取
rpc GoodsList(GoodsFilterRequest) returns(GoodsListResponse);
// 批量查询商品信息的接口,避免查商品时发生一个一个调用服务、一条一条查的低效情况
rpc BatchGetGoods(BatchGoodsIdInfo) returns(GoodsListResponse);
// 添加商品
rpc CreateGoods(CreateGoodsInfo) returns(GoodsInfoResponse);
// 删除商品,没有明确需要返回的信息,返回一个占位符
rpc DeleteGoods(DeleteGoodsInfo) returns(google.protobuf.Empty);
// 更新商品信息
rpc UpdateGoods(CreateGoodsInfo) returns(google.protobuf.Empty);
// 获取商品信息(单独获取)
rpc GetGoodsDetail(GoodInfoRequest) returns(GoodsInfoResponse);
// 分类部分
// 获取所有商品分类
rpc GetAllCategorysList(google.protobuf.Empty) returns(CategoryListResponse);
// 获取子分类 todo 把这个补齐,文件在桌面上,视频已经看完了
rpc GetSubCategory(CategoryListRequest) returns(SubCategoryListResponse);
rpc CreateCategory(CategoryInfoRequest) returns(CategoryInfoResponse);
rpc DeleteCategory(DeleteCategoryRequest) returns(google.protobuf.Empty);
rpc UpdateCategory(CategoryInfoRequest) returns(google.protobuf.Empty);
// 品牌部分
rpc BrandList(BrandFilterRequest) returns(BrandListResponse);
rpc CreateBrand(BrandRequest) returns(BrandInfoResponse);
rpc DeleteBrand(BrandRequest) returns(google.protobuf.Empty);
rpc UpdateBrand(BrandRequest) returns(google.protobuf.Empty);
// 轮播图部分
rpc BannerList(google.protobuf.Empty) returns(BannerListResponse);
rpc CreateBanner(BannerRequest) returns(BannerResponse);
rpc DeleteBranner(BannerRequest) returns(google.protobuf.Empty);
rpc UpdateBanner(BannerRequest) returns(google.protobuf.Empty);
// 品牌分类信息
// 过滤需要的品牌、分类信息
rpc CategoryBrandList(CategoryBrandFilterRequest) returns(CategoryBrandListResponse);
// 获取某个分类下所有品牌的接口
rpc GetCategoryBrandList(CategoryInfoRequest) returns(BrandListResponse);
rpc CreateCategoryBrand(CategoryBrandRequest) returns(CategoryBrandResponse);
rpc DeleteCategoryBrand(CategoryBrandRequest) returns(google.protobuf.Empty);
rpc UpdateCategoryBrand(CategoryBrandRequest) returns(google.protobuf.Empty);
}
// 在过滤商品时传入的条件信息
message GoodsFilterRequest {
int32 priceMin = 1;
int32 priceMax = 2;
bool isHot = 3;
bool isNew = 4;
bool isTab = 5;
int32 topCategory = 6;
int32 pages = 7;
int32 pagePerNums = 8;
string keyWords = 9;
int32 brand = 10;
}
// 单独的一条关联信息
message CategoryBrandRequest{
int32 id = 1;
int32 categoryId = 2;
int32 brandId = 3;
}
// 返回的品牌、分类信息集合、也就是联系信息
message CategoryBrandListResponse {
int32 total = 1;
repeated CategoryBrandResponse data = 2;
}
// 返回一个品牌信息、一个分类信息
message CategoryBrandResponse{
int32 id = 1;
BrandInfoResponse brand = 2;
CategoryInfoResponse category = 3;
}
// 轮播图的返回结果
message BannerListResponse {
int32 total = 1;
repeated BannerResponse data = 2;
}
// 过滤品牌、分类信息请求
message CategoryBrandFilterRequest {
int32 pages = 1;
int32 pagePerNums = 2;
}
// 单个轮播图
message BannerResponse {
int32 id = 1;
int32 index = 2;
string image = 3;
string url = 4;
}
// 单个轮播图的请求
message BannerRequest {
int32 id = 1;
int32 index = 2;
string image = 3;
string url = 4;
}
// 过滤品牌请求的信息
message BrandFilterRequest {
int32 pages = 1;
int32 pagePerNums = 2;
}
// 品牌查询请求
message BrandRequest {
int32 id = 1;
string name = 2;
string logo = 3;
}
// 创建分类的请求信息
message CategoryInfoRequest {
int32 id = 1;
string name = 2;
int32 parentCategory = 3;
int32 level = 4;
bool isTab = 5;
}
// 传入删除信息的ID
message DeleteCategoryRequest {
int32 id = 1;
}
// 商品ID 列表,便于批量查询
message BatchGoodsIdInfo {
repeated int32 id = 1;
}
// 获取分类信息集合
message CategoryListResponse {
int32 total = 1;
repeated CategoryInfoResponse data = 2;
string jsonData = 3;
}
// 获取子分类集合(需要传入选中分类的id,level选传)
message CategoryListRequest {
int32 id = 1;
int32 level = 2;
}
// 子分类的返回
message SubCategoryListResponse {
int32 total = 1;
CategoryInfoResponse info = 2; // 将本分类的所有信息返回
repeated CategoryInfoResponse subCategorys = 3; // 将子分类的所有信息返回
}
// 分类信息
message CategoryInfoResponse {
int32 id = 1;
string name = 2;
int32 parentCategory = 3;
int32 level = 4;
bool isTab = 5;
}
// 获取单独商品详情
message GoodInfoRequest {
int32 id = 1;
}
// 商品列表的返回信息
message GoodsListResponse {
int32 total = 1;
repeated GoodsInfoResponse data = 2;
}
// 单个商品的信息
message GoodsInfoResponse {
int32 id = 1;
int32 categoryId = 2;
string name = 3;
string goodsSn = 4;
int32 clickNum = 5;
int32 soldNum = 6;
int32 favNum = 7;
float marketPrice = 9;
float shopPrice = 10;
string goodsBrief = 11;
string goodsDesc = 12;
bool shipFree = 13;
repeated string images = 14;
repeated string descImages = 15;
string goodsFrontImage = 16;
bool isNew = 17;
bool isHot = 18;
bool onSale = 19;
int64 addTime = 20;
CategoryBriefInfoResponse category = 21;
BrandInfoResponse brand = 22;
}
// 删除时传入一个ID
message DeleteGoodsInfo {
int32 id = 1;
}
// 创建商品去要传递的信息
message CreateGoodsInfo {
int32 id = 1;
string name = 2;
string goodsSn = 3;
int32 stocks = 7;
float marketPrice = 8;
float shopPrice = 9;
string goodsBrief = 10;
string goodsDesc = 11;
bool shipFree = 12;
repeated string images = 13;
repeated string descImages = 14;
string goodsFrontImage = 15;
bool isNew = 16;
bool isHot = 17;
bool onSale = 18;
int32 categoryId = 19;
int32 brandId = 20;
}
// 商品分类的简要信息
message CategoryBriefInfoResponse {
int32 id = 1;
string name = 2;
}
message CategoryFilterRequest {
int32 id = 1;
string name = 2;
}
// 品牌单个信息
message BrandInfoResponse {
int32 id = 1;
string name = 2;
string logo = 3;
}
// 品牌列表信息
message BrandListResponse {
int32 total = 1;
repeated BrandInfoResponse data = 2;
}
protobuf 文件的生成
在对应的文件夹目录下输入:
// 标准版
protoc --go_out=. xxxx.proto
// gprc 定制版
protoc --go_out=. --go-grpc_out=. *.proto
就可以在当前目录下创建好我们所需要的 xxxx.pb.go 文件,这个文件就是我们的以proto 作为传输协议的正式接口文件。
注意:此命令需要 protoc 环境完善,并配置好完整的环境变量(protoc 的环境变量)
protobuf 的构建
此时,我们发现,我们的接口过于多了,这就需要我们分开进行,我们在 .pb.go 文件中找到: GoodsServer,这里面定义的就是所有的接口,我们在 handler 文件中将所有的接口进行定义:
handler
banner.go
brands.go
goods.go
category.go
category_brand.go
示例定义(goods.go):
测试定义:若我们希望进行快速测试,就可以给 自己的 GoodsServer 添加一个属性,有这个属性存在,就可以进行服务器快速测试。
package handler
import (
"mxshop_srvs/goods_srv/proto"
)
type GoodsServer struct {
proto.UnimplementedGoodsServer
}
记得修改 main文件中的 注册:
proto.RegisterGoodsServer(server, &handler.GoodsServer{})
之后进行Nacos 相关配置:
创建一个新的命名空间、添加文件:goods-srv.json :
{
"name": "goods-srv",
"tags": ["imooc", "bobby", "goods", "srv"],
"web-host": "192.168.10.108",
"mysql": {
"host": "192.168.202.140",
"port": 3306,
"db": "mxshop_goods_srv",
"user": "root",
"password": "123456"
},
"consul": {
"host": "192.168.202.140",
"port": 8500
}
}
之后修改启动的配置文件的命名空间的ID
记得同步修改 config.go 中,新添加了一个 Tags 标签
type ServerConfig struct {
// 这里是为了配置服务端口使用的,后期会移植到
//Host string `mapstruce:"host" json:"host"`
//Port int `mapstruct:"port" json:"port"`
Name string `mapstructure:"name" json:"name"`
Tags []string `mapstructure:"tags" json:"tags"`
MysqlInfo MysqlConfig `mapstructure:"mysql" json:"mysql"`
ConsulInfo ConsulConfig `mapstructure:"consul" json:"consul"`
WebHost string `json:"web-host"`
}
然后在 main 中读取:
registration.Tags = global.ServerConfig.Tags
GRPC服务启动的相关信息
main.go:
package main
import (
"flag"
"fmt"
"go.uber.org/zap"
"mxshop_srvs/goods_srv/global"
"mxshop_srvs/goods_srv/initialize"
"mxshop_srvs/goods_srv/utils"
"net"
"os"
"os/signal"
"syscall"
"github.com/hashicorp/consul/api"
"github.com/satori/go.uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"mxshop_srvs/goods_srv/handler"
"mxshop_srvs/goods_srv/proto"
)
func main() {
// 由于ip和端口号有可能需要用户输入,所以这里摘出来
// flag 包是一个命令行工具包,允许从命令行中设置参数
IP := flag.String("ip", "0.0.0.0", "ip地址")
Port := flag.Int("port", 0, "端口号")
initialize.InitLogger()
initialize.InitConfig()
flag.Parse()
fmt.Println("ip: ", *IP)
// 设置端口号自动获取
if *Port == 0 {
*Port, _ = utils.GetFreePort()
}
fmt.Println("port: ", *Port)
// 创建新服务器
server := grpc.NewServer()
// 注册自己的已实现的方法进来
proto.RegisterGoodsServer(server, &handler.GoodsServer{})
//lis, err := net.Listen("tcp", fmt.Sprintf("192.168.202.140:8021"))
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
if err != nil {
panic("failed to listen" + err.Error())
}
// 绑定服务健康检查
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
// 服务注册
cfg := api.DefaultConfig()
cfg.Address = fmt.Sprintf("%s:%d", global.ServerConfig.ConsulInfo.Host, global.ServerConfig.ConsulInfo.Port)
client, err := api.NewClient(cfg)
if err != nil {
panic(err)
}
check := &api.AgentServiceCheck{
GRPC: fmt.Sprintf("%s:%d", global.ServerConfig.Host, *Port),
Interval: "5s",
//Timeout: "10s",
DeregisterCriticalServiceAfter: "30s",
}
registration := new(api.AgentServiceRegistration)
registration.Address = global.ServerConfig.Host
//registration.Address = "127.0.0.1"
//registration.ID = global.ServerConfig.Name // 此处修改为使用 UUID 生成
serviceID := fmt.Sprintf("%s", uuid.NewV4()) // 此处修改为使用 UUID 生成
registration.ID = serviceID
registration.Port = *Port
registration.Tags = global.ServerConfig.Tags
registration.Name = global.ServerConfig.Name
registration.Check = check
err = client.Agent().ServiceRegister(registration)
if err != nil {
panic(err)
}
//err = server.Serve(lis)
// 注意此处是阻塞式的所以需要一个 goroutine 来进行异步操作
// 将自己的服务绑定端口
go func() {
err = server.Serve(lis)
if err != nil {
panic("fail to start grpc" + err.Error())
}
}()
// 创建一个通道
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 阻塞住,若接到请求则放通,直接将服务注销
<-quit
if err = client.Agent().ServiceDeregister(serviceID); err != nil {
zap.S().Info("注销失败...")
}
zap.S().Info("注销成功")
}
InitConfig:(配置文件的相关信息)
intialize/config.go:
package initialize
import (
"encoding/json"
"fmt"
"github.com/nacos-group/nacos-sdk-go/clients"
"github.com/nacos-group/nacos-sdk-go/vo"
"github.com/nacos-group/nacos-sdk-go/common/constant"
"github.com/spf13/viper"
"go.uber.org/zap"
"mxshop_srvs/goods_srv/global"
)
func GetEnvInfo(env string) bool {
viper.AutomaticEnv()
var rs bool
rs = viper.GetBool(env)
return rs
return true
}
func InitConfig() {
debug := GetEnvInfo("MXSHOP-DEBUG")
zap.S().Info(fmt.Sprintf("------------", debug))
configFileNamePrefix := "config"
configFileName := fmt.Sprintf("goods_srv/%s-pro.yaml", configFileNamePrefix)
if debug {
configFileName = fmt.Sprintf("goods_srv/%s-debug.yaml", configFileNamePrefix)
}
v := viper.New()
v.SetConfigFile(configFileName)
if err := v.ReadInConfig(); err != nil {
panic(err)
}
// 将配置文件进行解析
if err := v.Unmarshal(&global.NacosConfig); err != nil {
panic(err)
}
sc := []constant.ServerConfig{
{
IpAddr: global.NacosConfig.Host,
Port: global.NacosConfig.Port,
},
}
cc := constant.ClientConfig{
TimeoutMs: 5000,
NamespaceId: global.NacosConfig.Namespace,
CacheDir: "tmp/nacos/cache",
NotLoadCacheAtStart: true,
LogDir: "tmp/nacos/log",
LogLevel: "debug",
}
configClient, err := clients.CreateConfigClient(map[string]interface{}{
"serverConfigs": sc,
"clientConfig": cc,
})
if err != nil {
zap.S().Fatalf("%s", err.Error())
}
content, err := configClient.GetConfig(vo.ConfigParam{
DataId: global.NacosConfig.Dataid,
Group: global.NacosConfig.Group,
})
if err != nil {
zap.S().Fatalf("%s", err.Error())
}
err = configClient.ListenConfig(vo.ConfigParam{
DataId: global.NacosConfig.Dataid,
Group: global.NacosConfig.Group,
OnChange: func(namespace, group, dataId, data string) {
fmt.Println("配置文件发生变化")
fmt.Println("namespace: " + namespace)
fmt.Println("group: " + group)
fmt.Println("dataId: " + dataId)
fmt.Println("data: " + data)
},
})
if err != nil {
zap.S().Fatalf("%s", err.Error())
}
err = json.Unmarshal([]byte(content), &global.ServerConfig)
if err != nil {
zap.S().Fatalf("%s", err.Error())
}
zap.S().Info(global.ServerConfig)
}
此处还需要注意配置文件和 Nacos 的配置文件:
config-debug.yml
host: '192.168.202.140'
port: 8848
namespace: '043d2547-bd1e-44df-b097-75f649848099'
user: 'nacos'
password: 'nacos'
dataid: 'goods-srv.json'
group: 'dev'
Nacos配置:
{
"name": "goods-srv",
"host": "192.168.10.107",
"mysql": {
"host": "192.168.202.140",
"port": 3306,
"db": "mxshop_user_srv",
"user": "root",
"password": "123456"
},
"consul": {
"host": "192.168.202.140",
"port": 8500
}
}