4

kratos 微服务框架商城实战初识 kratos

 2 years ago
source link: https://xie.infoq.cn/article/64641cdf2e6dcd91c1c43971c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

kratos 微服务框架商城实战初识 kratos

kratos 微服务框架商城实战初识 kratos

看文章之前推荐看一下Kratos 官方文档 以便能更加流畅观看此文章。同时强烈建议阅读 Go 工程化-依赖注入Project Layout 最佳实践 这两篇文章。

本机器这里已经安装好了go、kratos、proto、wire、make 等所需的工具。

初始化项目目录

进入自己电脑中存放 Go 项目的目录

新建 kratos-shop/service 目录,并进入到新建的目录中

执行 kratos new user 命令并进入 user 目录,执行命令 kratos proto add api/user/v1/user.proto

这时你在 /service/user/api/user/v1 目录下会看到新的 user.proto 文件已经创建好了

接下来执行 kratos proto server api/user/v1/user.proto -t internal/service 命令生成对应的 service 文件。

删除不需要的 proto 文件 rm -rf api/helloworld/

删除不需要的 service 文件 rm internal/service/greeter.go

  • 完整的命令代码如下

mkdir  -p kratos-shop/servicecd kratos-shop/servicekratos new usercd userkratos proto add api/user/v1/user.protokratos proto server api/user/v1/user.proto -t internal/servicerm -rf api/helloworld/rm internal/service/greeter.go
  • 修改 user.proto 文件,内容如下:

proto 基本的语法请自行学习,目前这里的只先提供了一个创建用户的 rpc 接口,后续会逐步添加其他 rpc 接口

syntax = "proto3";package user.v1;option go_package = "user/api/user/v1;v1";service User{  rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 创建用户}// 创建用户所需字段message  CreateUserInfo{  string nickName = 1;  string password = 2;  string mobile = 3;}// 返回用户信息message UserInfoResponse{  int64 id = 1;  string password = 2;  string mobile = 3;  string nickName = 4;  int64 birthday = 5;  string gender = 6;  int32 role = 7;}
  • 生成 user.proto 定义的接口信息

进入到 service/user 目录下,执行 make api 命令,

这时可以看到 user/api/user/v1/ 目录下多出了 proto 创建的文件

cd usermake api# 目录结构如下:├── api│   └── user│       └── v1│           ├── user.pb.go│           ├── user.proto│           └── user_grpc.pb.go

修改配置文件

  • 修改 user/configs/config.yaml 文件,代码如下:

具体链接 mysql、redis 的参数填写自己本机的,本项目用到的是 gorm 。trace 是以后要用到的链路追踪的参数,先定义了。

server:  http:    addr: 0.0.0.0:8000    timeout: 1s  grpc:    addr: 0.0.0.0:50051    timeout: 1sdata:  database:    driver: mysql    source: root:root@tcp(127.0.0.1:3306)/shop_user?charset=utf8mb4&parseTime=True&loc=Local  redis:    addr: 127.0.0.1:6379    dial_timeout: 1s    read_timeout: 0.2s    write_timeout: 0.2strace:  endpoint: http://127.0.0.1:14268/api/traces

新建 user/configs/registry.yaml 文件,代码如下:

# 这里引入了 consul 的服务注册与发现,先把配置加入进去consul:    address: 127.0.0.1:8500    scheme: http
  • 修改 user/internal/conf/conf.proto 配置文件

# 文件底部新增 consul 和 trace 的配置信息message Trace {  string endpoint = 1;}message Registry {  message Consul {    string address = 1;    string scheme = 2;  }  Consul consul = 1;}
  • 新生成 conf.pb.go 文件,执行 make config

# `service/user` 目录下,执行命令make config

安装 consul 服务工具

# 这里使用的是 docker 工具进行创建的docker run -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8600:8600/udp consul consul agent -dev -client=0.0.0.0# 浏览器访问 http://127.0.0.1:8500/ui/dc1/services 测试是否安装成功

修改 internal 服务目录

修改 user/internal/data/ 目录下的文件

  • 修改 data.go添加如下内容:

package dataimport (    "github.com/go-kratos/kratos/v2/log"    "github.com/go-redis/redis/extra/redisotel"    "github.com/go-redis/redis/v8"    "github.com/google/wire"    "gorm.io/driver/mysql"    "gorm.io/gorm"    "gorm.io/gorm/logger"    "gorm.io/gorm/schema"    slog "log"    "os"    "time"    "user/internal/conf")// ProviderSet is data providers.var ProviderSet = wire.NewSet(NewData, NewDB, NewRedis, NewUserRepo)type Data struct {    db  *gorm.DB    rdb *redis.Client}// NewData .func NewData(c *conf.Data, logger log.Logger, db *gorm.DB, rdb *redis.Client) (*Data, func(), error) {    cleanup := func() {        log.NewHelper(logger).Info("closing the data resources")    }    return &Data{db: db, rdb: rdb}, cleanup, nil}// NewDB .func NewDB(c *conf.Data) *gorm.DB {    // 终端打印输入 sql 执行记录    newLogger := logger.New(        slog.New(os.Stdout, "\r\n", slog.LstdFlags), // io writer        logger.Config{            SlowThreshold: time.Second, // 慢查询 SQL 阈值            Colorful:      true,        // 禁用彩色打印            //IgnoreRecordNotFoundError: false,            LogLevel: logger.Info, // Log lever        },    )    db, err := gorm.Open(mysql.Open(c.Database.Source), &gorm.Config{        Logger:                                   newLogger,        DisableForeignKeyConstraintWhenMigrating: true,        NamingStrategy:                           schema.NamingStrategy{            //SingularTable: true, // 表名是否加 s        },    })    if err != nil {        log.Errorf("failed opening connection to sqlite: %v", err)        panic("failed to connect database")    }    return db}func NewRedis(c *conf.Data) *redis.Client {    rdb := redis.NewClient(&redis.Options{        Addr:         c.Redis.Addr,        Password:     c.Redis.Password,        DB:           int(c.Redis.Db),        DialTimeout:  c.Redis.DialTimeout.AsDuration(),        WriteTimeout: c.Redis.WriteTimeout.AsDuration(),        ReadTimeout:  c.Redis.ReadTimeout.AsDuration(),    })    rdb.AddHook(redisotel.TracingHook{})    if err := rdb.Close(); err != nil {        log.Error(err)    }    return rdb}

这里的 wire 概念如果不熟悉的话,请参看Wire 依赖注入

修改 user/internal/service/ 目录下的文件

  • 修改或者删除 user/internal/service/greeter.gouser.go, 添加代码如下:

package serviceimport (    "context"    "github.com/go-kratos/kratos/v2/log"    v1 "user/api/user/v1"    "user/internal/biz")type UserService struct {    v1.UnimplementedUserServer    uc  *biz.UserUsecase    log *log.Helper}// NewUserService new a greeter service.func NewUserService(uc *biz.UserUsecase, logger log.Logger) *UserService {    return &UserService{uc: uc, log: log.NewHelper(logger)}}// CreateUser create a userfunc (u *UserService) CreateUser(ctx context.Context, req *v1.CreateUserInfo) (*v1.UserInfoResponse, error) {    user, err := u.uc.Create(ctx, &biz.User{        Mobile:   req.Mobile,        Password: req.Password,        NickName: req.NickName,    })    if err != nil {        return nil, err    }    userInfoRsp := v1.UserInfoResponse{        Id:       user.ID,        Mobile:   user.Mobile,        Password: user.Password,        NickName: user.NickName,        Gender:   user.Gender,        Role:     int32(user.Role),        Birthday: user.Birthday,    }    return &userInfoRsp, nil}
  • 修改 ser/internal/service/service.go 文件, 代码如下:

package serviceimport "github.com/google/wire"// ProviderSet is service providers.var ProviderSet = wire.NewSet(NewUserService)
  • 修改或删除 user/internal/biz/greeter.gouser.go添加如下内容:

package bizimport (    "context"    "github.com/go-kratos/kratos/v2/log")// 定义返回数据结构体type User struct {    ID       int64    Mobile   string    Password string    NickName string    Birthday int64    Gender   string    Role     int}type UserRepo interface {    CreateUser(context.Context, *User) (*User, error)}type UserUsecase struct {    repo UserRepo    log  *log.Helper}func NewUserUsecase(repo UserRepo, logger log.Logger) *UserUsecase {    return &UserUsecase{repo: repo, log: log.NewHelper(logger)}}func (uc *UserUsecase) Create(ctx context.Context, u *User) (*User, error) {    return uc.repo.CreateUser(ctx, u)}
  • 修改 user/internal/biz/biz.go 文件,内容如下:

package bizimport "github.com/google/wire"// ProviderSet is biz providers.var ProviderSet = wire.NewSet(NewUserUsecase)
  • 修改或删除 user/internal/data/greeter.gouser.go添加如下内容:

package dataimport (    "context"    "crypto/sha512"    "fmt"    "github.com/anaskhan96/go-password-encoder"    "github.com/go-kratos/kratos/v2/log"    "google.golang.org/grpc/codes"    "google.golang.org/grpc/status"    "gorm.io/gorm"    "time"    "user/internal/biz")// 定义数据表结构体type User struct {    ID          int64      `gorm:"primarykey"`    Mobile      string     `gorm:"index:idx_mobile;unique;type:varchar(11) comment '手机号码,用户唯一标识';not null"`    Password    string     `gorm:"type:varchar(100);not null "` // 用户密码的保存需要注意是否加密    NickName    string     `gorm:"type:varchar(25) comment '用户昵称'"`    Birthday    *time.Time `gorm:"type:datetime comment '出生日日期'"`    Gender      string     `gorm:"column:gender;default:male;type:varchar(16) comment 'female:女,male:男'"`    Role        int        `gorm:"column:role;default:1;type:int comment '1:普通用户,2:管理员'"`    CreatedAt   time.Time  `gorm:"column:add_time"`    UpdatedAt   time.Time  `gorm:"column:update_time"`    DeletedAt   gorm.DeletedAt    IsDeletedAt bool}type userRepo struct {    data *Data    log  *log.Helper}// NewUserRepo . 这里需要注意,上面 data 文件 wire 注入的是此方法,方法名不要写错了func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {    return &userRepo{        data: data,        log:  log.NewHelper(logger),    }}// CreateUser .func (r *userRepo) CreateUser(ctx context.Context, u *biz.User) (*biz.User, error) {    var user User    // 验证是否已经创建    result := r.data.db.Where(&biz.User{Mobile: u.Mobile}).First(&user)    if result.RowsAffected == 1 {        return nil, status.Errorf(codes.AlreadyExists, "用户已存在")    }    user.Mobile = u.Mobile    user.NickName = u.NickName    user.Password = encrypt(u.Password) // 密码加密    res := r.data.db.Create(&user)    if res.Error != nil {        return nil, status.Errorf(codes.Internal, res.Error.Error())    }    return &biz.User{        ID:       user.ID,        Mobile:   user.Mobile,        Password: user.Password,        NickName: user.NickName,        Gender:   user.Gender,        Role:     user.Role,    }, nil}// Password encryptionfunc encrypt(psd string) string {    options := &password.Options{SaltLen: 16, Iterations: 10000, KeyLen: 32, HashFunction: sha512.New}    salt, encodedPwd := password.Encode(psd, options)    return fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)}

修改 user/internal/server/ 目录下的文件

这里用不到 http 服务,所以删除了 http.go 文件,

  • 修改 grpc.go 文件内容如下:

 package serverimport (    "github.com/go-kratos/kratos/v2/log"    "github.com/go-kratos/kratos/v2/middleware/logging"    "github.com/go-kratos/kratos/v2/middleware/recovery"    "github.com/go-kratos/kratos/v2/transport/grpc"    v1 "user/api/user/v1"    "user/internal/conf"    "user/internal/service")// NewGRPCServer new a gRPC server.func NewGRPCServer(c *conf.Server, greeter *service.UserService, logger log.Logger) *grpc.Server {    var opts = []grpc.ServerOption{        grpc.Middleware(            recovery.Recovery(),            logging.Server(logger),        ),    }    if c.Grpc.Network != "" {        opts = append(opts, grpc.Network(c.Grpc.Network))    }    if c.Grpc.Addr != "" {        opts = append(opts, grpc.Address(c.Grpc.Addr))    }    if c.Grpc.Timeout != nil {        opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))    }    srv := grpc.NewServer(opts...)    v1.RegisterUserServer(srv, greeter)    return srv}
  • 修改 server.go 文件,这里加入了 consul 的服务,内容如下:

package serverimport (    "github.com/go-kratos/kratos/v2/registry"    "github.com/google/wire"    "user/internal/conf"    consul "github.com/go-kratos/kratos/contrib/registry/consul/v2"    consulAPI "github.com/hashicorp/consul/api")// ProviderSet is server providers.var ProviderSet = wire.NewSet(NewGRPCServer, NewRegistrar)// NewRegistrar 引入 consulfunc NewRegistrar(conf *conf.Registry) registry.Registrar {    c := consulAPI.DefaultConfig()    c.Address = conf.Consul.Address    c.Scheme = conf.Consul.Scheme    cli, err := consulAPI.NewClient(c)    if err != nil {        panic(err)    }    r := consul.New(cli, consul.WithHealthCheck(false))    return r}

修改启动程序

  • 修改 user/cmd/wire.go文件

这里注入了 consul 需要的配置,需要添加进来

func initApp(*conf.Server, *conf.Data, *conf.Registry, log.Logger) (*kratos.App, func(), error) {    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))}
  • 修改 user/cmd/user/main.go 文件

package mainimport (    "flag"    "os"    "github.com/go-kratos/kratos/v2"    "github.com/go-kratos/kratos/v2/config"    "github.com/go-kratos/kratos/v2/config/file"    "github.com/go-kratos/kratos/v2/log"    "github.com/go-kratos/kratos/v2/middleware/tracing"    "github.com/go-kratos/kratos/v2/registry"    "github.com/go-kratos/kratos/v2/transport/grpc"    "user/internal/conf")// go build -ldflags "-X main.Version=x.y.z"var (    // Name is the name of the compiled software.    Name = "shop.users.service"    // Version is the version of the compiled software.    Version = "v1"    // flagconf is the config flag.    flagconf string    id, _ = os.Hostname())func init() {    flag.StringVar(&flagconf, "conf", "../../configs", "config path, eg: -conf config.yaml")}func newApp(logger log.Logger, gs *grpc.Server, rr registry.Registrar) *kratos.App {    return kratos.New(        kratos.ID(id+"shop.user.service"),        kratos.Name(Name),        kratos.Version(Version),        kratos.Metadata(map[string]string{}),        kratos.Logger(logger),        kratos.Server(            gs,        ),        kratos.Registrar(rr), // consul 的引入    )}func main() {    flag.Parse()    logger := log.With(log.NewStdLogger(os.Stdout),        "ts", log.DefaultTimestamp,        "caller", log.DefaultCaller,        "service.id", id,        "service.name", Name,        "service.version", Version,        "trace_id", tracing.TraceID(),        "span_id", tracing.SpanID(),    )    c := config.New(        config.WithSource(            file.NewSource(flagconf),        ),    )    defer c.Close()    if err := c.Load(); err != nil {        panic(err)    }    var bc conf.Bootstrap    if err := c.Scan(&bc); err != nil {        panic(err)    }    // consul 的引入    var rc conf.Registry    if err := c.Scan(&rc); err != nil {        panic(err)    }    app, cleanup, err := initApp(bc.Server, bc.Data, &rc, logger)    if err != nil {        panic(err)    }    defer cleanup()    // start and wait for stop signal    if err := app.Run(); err != nil {        panic(err)    }}
  • 修改根目录 user/makefile 文件

    在 go generate ./... 下面添加代码    wire:        cd cmd/user/ && wire
  • 根目录执行 make wire 命令

# service/usermake wire

别忘记根据 data 里面的 user struct 创建对应的数据库表,这里也可以写一个 gorm 创建表的文件进行创建。

启动程序 kratos run

根目录 service/user 执行命令    kratos run

由于没写对外访问的 http 服务,这里还没有加入单元测试,所以先创建个文件链接启动过的 grpc 服务简单测试一下。

  • 根目录新建 user/test/user.go 文件,添加如下内容:

package mainimport (    "context"    "fmt"    "google.golang.org/grpc"    v1 "user/api/user/v1")var userClient v1.UserClientvar conn *grpc.ClientConnfunc main() {    Init()    TestCreateUser() // 创建用户    conn.Close()}// Init 初始化 grpc 链接 注意这里链接的 端口func Init() {    var err error    conn, err = grpc.Dial("127.0.0.1:50051", grpc.WithInsecure())    if err != nil {        panic("grpc link err" + err.Error())    }    userClient = v1.NewUserClient(conn)}func TestCreateUser() {    rsp, err := userClient.CreateUser(context.Background(), &v1.CreateUserInfo{        Mobile:   fmt.Sprintf("1388888888%d", 1),        Password: "admin123",        NickName: fmt.Sprintf("YWWW%d", 1),    })    if err != nil {        panic("grpc 创建用户失败" + err.Error())    }    fmt.Println(rsp.Id)}

这里别忘记启动 kratos user 服务之后,再执行 test/user.go 文件,查询执行结果,是否有个 ID 输出查询自己的数据库,看看是否有插入的数据了。

Kratos-shop 源码 已经上传到 GitHub 上了,如果有疑问,请点击源码进行查看。也感谢您指出错误。

感谢您的耐心阅读,动动手指点个赞吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK