Go-kratos 框架商城微服務實戰之使用者服務API (五)

Aliliin發表於2022-02-17

Go-kratos 框架商城微服務實戰之使用者服務 (五)

這篇主要編寫對 web 端提供 http 介面的服務,主要還是一些準備工作。寫的不清晰的地方可看原始碼 , 歡迎大佬指教。

注:豎排 … 程式碼省略,為了保持文章的篇幅簡潔,我會將一些不必要的程式碼使用豎排的 . 來代替,你在複製本文程式碼塊的時候,切記不要將 . 也一同複製進去。

  1. shop http 服務專案的建立
  • new 一個新的 kratos 專案程式碼
// 在 user 服務的上一級目錄中,新建一個 shop 專案
// 整體的專案 目錄結構如下

|-- kratos-shop
    |-- service
        |-- user // 原先的使用者服務 grpc
    |-- shop // 通過 kratos new shop 新增的目錄程式碼
  • 刪除 shop/api/helloworld/v1 目錄下的所有檔案
  • 修改或刪除 shop 專案中的 shop/api/helloworld/v1 目錄為 shop/api/shop/v1
  • 複製 service/user/api/user/v1/user.proto 檔案到新建的 shop/api/service/user/v1 目錄下
  • 新建 shop/api/shop/v1/shop.proto 檔案

這裡提供對外訪問的介面,會聚合從不同的服務之間獲取資料,介面路由通過 Protobuf IDL 定義對應的 REST API 和 gRPC API,

引數校驗使用 Validate 中介軟體,使用 proto-gen-validate 生成

在使用 validate 之前首先需要安裝 proto-gen-validate

syntax = "proto3";

package shop.shop.v1;

import "google/api/annotations.proto";
import "validate/validate.proto";

option go_package = "shop/api/shop/v1;v1";

// The Shop service definition.
service Shop {
  rpc Register (RegisterReq) returns (RegisterReply) {
    option (google.api.http) = {
      post: "/api/users/register",
      body: "*",
    };
  }
  rpc Login (LoginReq) returns (RegisterReply) {
    option (google.api.http) = {
      post: "/api/users/login",
      body: "*",
    };
  }
  rpc Detail (DetailReq) returns (UserDetailResponse) {
    option (google.api.http) = {
      get: "/api/users/detail/{id}",
    };
  }
}

// Data returned by registration and login
message RegisterReply {
  int64 id = 1;
  string mobile = 3;
  string username = 4;
  string token = 5;
  int64 expiredAt = 6;
}

message RegisterReq {
  string mobile = 1 [(validate.rules).string.len = 11];
  string username = 2 [(validate.rules).string = {min_len: 3, max_len: 15}];
  string password = 3 [(validate.rules).string = {min_len: 8}];
}

message LoginReq {
  string mobile = 1 [(validate.rules).string.len = 11];
  string password = 3 [(validate.rules).string = {min_len: 8}];
}

// select user details
message  DetailReq{
  int64 id = 1 [(validate.rules).int64 = {gt: 0}];
}
// user Detail returned
message UserDetailResponse{
  int64 id = 1;
  string mobile = 2;
  string nickName = 3;
  int64 birthday = 4;
  string gender = 5;
  int32 role = 6;
}
  • shop 根目錄執行 make api ,生成 *pb.go 檔案

如果你覺得這樣子設計專案目錄不合理的話,你可以自己設計目錄。只要能把服務之間關聯起來可以了。

  • 修改 shop/cmd/shop/main.go 檔案裡面的 name 定義 name = shop.api

注: 這裡的 name,consul 的服務註冊與發現要用到

  • 刪除或修改 shop/configs/config.yaml 配置檔案

專案中引入了 consul、trace,配置檔案的時候,需要把相關的配置設定好,service 的配置是 consul 用來服務發現的

name: shop.api
server:
  http:
    addr: 0.0.0.0:8097
    timeout: 1s
  grpc:
    addr: 0.0.0.0:9001
    timeout: 1s
data:
  database:
    driver: mysql
    source: root:root@tcp(127.0.0.1:3306)/test
  redis:
    addr: 127.0.0.1:6379
    read_timeout: 0.2s
    write_timeout: 0.2s
trace:
  endpoint: http://127.0.0.1:14268/api/traces
auth:
  jwt_key: hqFr%3ddt32DGlSTOI5cO6@TH#fFwYnP$S
service:
  user:
    endpoint: discovery:///shop.user.service
  goods:
    endpoint: discovery:///shop.goods.service
  • 新增 shop/configs/registry.yaml 檔案,並加入:
consul:
  address: 127.0.0.1:8500
  scheme: http
  • 修改 shop/internal/conf/conf.proto 檔案:
syntax = "proto3";
package shop.api;

option go_package = "shop/internal/conf;conf";

import "google/protobuf/duration.proto";

message Bootstrap {
  Server server = 1;
  Data data = 2;
  Trace trace = 3; // 鏈路追蹤
  Auth auth = 4; // 認證鑑權
  Service service = 5; // 服務註冊與發現
}

message Server {
  message HTTP {
    string network = 1;
    string addr = 2;
    google.protobuf.Duration timeout = 3;
  }
  message GRPC {
    string network = 1;
    string addr = 2;
    google.protobuf.Duration timeout = 3;
  }
  HTTP http = 1;
  GRPC grpc = 2;
}

message Data {
  message Database {
    string driver = 1;
    string source = 2;
  }
  message Redis {
    string network = 1;
    string addr = 2;
    google.protobuf.Duration read_timeout = 3;
    google.protobuf.Duration write_timeout = 4;
  }
  Database database = 1;
  Redis redis = 2;
}

message Service {
  message User { // 使用者服務
    string endpoint = 1;
  }
  message Goods { // 商品服務
    string endpoint = 1;
  }
  User user = 1;
  Goods goods = 2;
}

message Trace {
  string endpoint = 1;
}

message Registry {
  message Consul {
    string address = 1;
    string scheme = 2;
  }
  Consul consul = 1;
}

message Auth {
  string jwt_key = 1;
}
  • 生成新的配置檔案 conf.proto
shop 根目錄執行 
make config
生成 shop/internal/conf/conf.pb.go  檔案
  • shop 根目錄執行 make config ,生成 conf.pb.go 檔案

  • 修改 shop/internal/server/http.go 檔案:

    這裡用到的一些中介軟體都是 kratos 官方支援的,jwt、validate、tracing,具體使用方式可參考文件

package server

import (
    "context"
    "github.com/go-kratos/kratos/v2/log"
    "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
    "github.com/go-kratos/kratos/v2/middleware/logging"
    "github.com/go-kratos/kratos/v2/middleware/recovery"
    "github.com/go-kratos/kratos/v2/middleware/selector"
    "github.com/go-kratos/kratos/v2/middleware/tracing"
    "github.com/go-kratos/kratos/v2/middleware/validate"
    "github.com/go-kratos/kratos/v2/transport/http"
    jwt2 "github.com/golang-jwt/jwt/v4"
    "github.com/gorilla/handlers"
    v1 "shop/api/shop/v1"
    "shop/internal/conf"
    "shop/internal/service"
)

// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, ac *conf.Auth, s *service.ShopService, logger log.Logger) *http.Server {
    var opts = []http.ServerOption{
        http.Middleware(
            recovery.Recovery(),
            logging.Server(logger),
            validate.Validator(), // 介面訪問的引數校驗
            tracing.Server(), // 鏈路追蹤
            selector.Server( // jwt 驗證
                jwt.Server(func(token *jwt2.Token) (interface{}, error) {
                    return []byte(ac.JwtKey), nil
                }, jwt.WithSigningMethod(jwt2.SigningMethodHS256)),
            ).Match(NewWhiteListMatcher()).Build(),
        ),
        http.Filter(handlers.CORS( // 瀏覽器跨域
            handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
            handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}),
            handlers.AllowedOrigins([]string{"*"}),
        )),
    }
    if c.Http.Network != "" {
        opts = append(opts, http.Network(c.Http.Network))
    }
    if c.Http.Addr != "" {
        opts = append(opts, http.Address(c.Http.Addr))
    }
    if c.Http.Timeout != nil {
        opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration()))
    }
    srv := http.NewServer(opts...)
    v1.RegisterShopHTTPServer(srv, s)
    return srv
}

// NewWhiteListMatcher 白名單不需要 token 驗證的介面
func NewWhiteListMatcher() selector.MatchFunc {
    whiteList := make(map[string]struct{})
    whiteList["/shop.shop.v1.Shop/Login"] = struct{}{}
    whiteList["/shop.shop.v1.Shop/Register"] = struct{}{}
    return func(ctx context.Context, operation string) bool {
        if _, ok := whiteList[operation]; ok {
            return false
        }
        return true
    }
}
  • 修改 shop/internal/server/server.go
package server

import (
    "github.com/google/wire"
)

// ProviderSet is server providers.
var ProviderSet = wire.NewSet(NewHTTPServer)

​ 由於此服務只對外提供 http 服務,所以同目錄下 grpc 檔案可以刪除,這樣子註冊服務的時候,把 grpc 服務去掉。

  • 修改或刪除 shop/internal/service/greeter.goshop/internal/service/user.go

這裡的 ShopService 的 usecase 還沒實現,編輯器可能會有錯誤提示,先忽略

package service

import (
    "context"
    v1 "shop/api/shop/v1" // 注意這個位置引入的 proto 生成的檔案,不然會找不到對應接收和返回
)

// 使用者註冊介面
func (s *ShopService) Register(ctx context.Context, req *v1.RegisterReq) (*v1.RegisterReply, error) {
    return s.uc.CreateUser(ctx, req) 
}
// 使用者登入介面
func (s *ShopService) Login(ctx context.Context, req *v1.LoginReq) (*v1.RegisterReply, error) {
    return s.uc.PassWordLogin(ctx, req)
}
// 當前登入使用者詳情
func (s *ShopService) Detail(ctx context.Context, req *v1.DetailReq) (*v1.UserDetailResponse, error) {
    return s.uc.UserDetailByID(ctx, req)
}
  • 修改 shop/internal/service/service.go 檔案
package service

import (
    "github.com/go-kratos/kratos/v2/log"
    "github.com/google/wire"
    v1 "shop/api/shop/v1"
    "shop/internal/biz"
)

// ProviderSet is service providers.
var ProviderSet = wire.NewSet(NewShopService)

// ShopService is a shop service.
type ShopService struct {
    v1.UnimplementedShopServer // 這個位置引入的是 shop.proto 生成的服務,不要引入 user

    uc  *biz.UserUsecase
    log *log.Helper
}

// NewShopService new a shop service.
func NewShopService(uc *biz.UserUsecase, logger log.Logger) *ShopService {
    return &ShopService{
        uc:  uc,
        log: log.NewHelper(log.With(logger, "module", "service/shop")),
    }
}
  • 修改 shop/internal/biz/greeter.goshop/internal/biz/user.go

注: 這裡還沒有實現 service 目錄下 user.go 檔案呼叫的方法

package biz

.
.
.
// 定義返回需要用的結構體
type User struct {
    ID        int64
    Mobile    string
    Password  string
    NickName  string
    Birthday  int64
    Gender    string
    Role      int
    CreatedAt time.Time
}

type UserRepo interface {
    CreateUser(c context.Context, u *User) (*User, error)
    UserByMobile(ctx context.Context, mobile string) (*User, error)
    UserById(ctx context.Context, Id int64) (*User, error)
    CheckPassword(ctx context.Context, password, encryptedPassword string) (bool, error)
}

type UserUsecase struct {
    uRepo      UserRepo
    log        *log.Helper
    signingKey string // 這裡是 jwt 生成 token 所需的 key config 傳入的
}

func NewUserUsecase(repo UserRepo, logger log.Logger, conf *conf.Auth) *UserUsecase {
    helper := log.NewHelper(log.With(logger, "module", "usecase/shop"))
    return &UserUsecase{uRepo: repo, log: helper, signingKey: conf.JwtKey}
}
  • 修改 shop/internal/biz/biz.go 檔案
package biz

import "github.com/google/wire"

// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewUserUsecase) // 註冊 usercase
  • 修改或刪除 shop/internal/data/greeter.goshop/internal/data/user.go
package data
.
.
.

type userRepo struct {
    data *Data
    log  *log.Helper
}

// NewUserRepo .
func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo {
    return &userRepo{
        data: data,
        log:  log.NewHelper(log.With(logger, "module", "repo/user")),
    }
}

func (u *userRepo) CreateUser(c context.Context, user *biz.User) (*biz.User, error) {
        return nil,nil
}

func (u *userRepo) UserByMobile(c context.Context, mobile string) (*biz.User, error) {
    return nil,nil
}

func (u *userRepo) CheckPassword(c context.Context, password, encryptedPassword string) (bool, error) {
        return  nil,nil
}

func (u *userRepo) UserById(c context.Context, id int64) (*biz.User, error) {

    return  nil,nil
}
  • 修改 shop/internal/data/data.go

    這裡比較重要,data 是不直接連結資料庫的,而是連結服務

package data

import (
    "context"
    consul "github.com/go-kratos/kratos/contrib/registry/consul/v2"
    "github.com/go-kratos/kratos/v2/log"
    "github.com/go-kratos/kratos/v2/middleware/recovery"
    "github.com/go-kratos/kratos/v2/middleware/tracing"
    "github.com/go-kratos/kratos/v2/registry"
    "github.com/go-kratos/kratos/v2/transport/grpc"
    "github.com/google/wire"
    consulAPI "github.com/hashicorp/consul/api"
    grpcx "google.golang.org/grpc"
    userV1 "shop/api/service/user/v1"
    "shop/internal/conf"
    "time"
)

// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewUserRepo, NewUserServiceClient, NewRegistrar, NewDiscovery)

// Data .
type Data struct {
    log *log.Helper
    uc  userV1.UserClient
}

// NewData .
func NewData(c *conf.Data, uc userV1.UserClient, logger log.Logger) (*Data, error) {
    l := log.NewHelper(log.With(logger, "module", "data"))
    return &Data{log: l, uc: uc}, nil
}

// NewUserServiceClient 連結使用者服務 grpc
// 這裡的 ac 暫時沒有用到,它可以用來驗證服務之間認證鑑權呼叫的 也就是 middleware jwt
func NewUserServiceClient(ac *conf.Auth, sr *conf.Service, rr registry.Discovery) userV1.UserClient {
    conn, err := grpc.DialInsecure(
        context.Background(),
        grpc.WithEndpoint(sr.User.Endpoint), // 這裡是服務名稱
        grpc.WithDiscovery(rr),// consul 的服務註冊與發現
        grpc.WithMiddleware(
            tracing.Client(), // 鏈路追蹤,完整的請求鏈路
            recovery.Recovery(),
        ),
        grpc.WithTimeout(2*time.Second),
        grpc.WithOptions(grpcx.WithStatsHandler(&tracing.ClientHandler{})),// 完整的請求鏈路
    )
    if err != nil {
        panic(err)
    }
    c := userV1.NewUserClient(conn)
    return c
}

// NewRegistrar add consul
func 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
}

func NewDiscovery(conf *conf.Registry) registry.Discovery {
    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
}
  • 修改入口檔案 main.go
package main

import (
    "flag"
    "github.com/go-kratos/kratos/v2/middleware/tracing"
    "github.com/go-kratos/kratos/v2/registry"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    tracesdk "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
    "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/transport/grpc"
    "github.com/go-kratos/kratos/v2/transport/http"
    "shop/internal/conf"
)

// go build -ldflags "-X main.Version=x.y.z"
var (
    // Name is the name of the compiled software.
    Name = "shop.api"
    // Version is the version of the compiled software.
    Version string
    // 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, hs *http.Server, rr registry.Registrar) *kratos.App {
    return kratos.New(
        kratos.ID(id+"shop.api"), // 這裡注意如果你是同一臺電腦,同時啟動服務的話,需要修改一下 ID
        kratos.Name(Name),
        kratos.Version(Version),
        kratos.Metadata(map[string]string{}),
        kratos.Logger(logger),
        kratos.Server(
            hs,
        ),
        kratos.Registrar(rr),
    )
}

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)
    }

    var rc conf.Registry // consul 初始化
    if err := c.Scan(&rc); err != nil {
        panic(err)
    }
    // 鏈路追蹤 初始化    
    err := setTracerProvider(bc.Trace.Endpoint)
    if err != nil {
        panic(err)
    }

    app, cleanup, err := initApp(bc.Server, bc.Data, bc.Auth, bc.Service, &rc, logger)
    if err != nil {
        panic(err)
    }
    defer cleanup()

    // start and wait for stop signal
    if err := app.Run(); err != nil {
        panic(err)
    }
}

// 鏈路追蹤
func setTracerProvider(url string) error {
    // Create the Jaeger exporter
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
    if err != nil {
        return err
    }
    tp := tracesdk.NewTracerProvider(
        // Set the sampling rate based on the parent span to 100%
        tracesdk.WithSampler(tracesdk.ParentBased(tracesdk.TraceIDRatioBased(1.0))),
        // Always be sure to batch in production.
        tracesdk.WithBatcher(exp),
        // Record information about this application in an Resource.
        tracesdk.WithResource(resource.NewSchemaless(
            semconv.ServiceNameKey.String(Name),
            attribute.String("env", "dev"),
        )),
    )
    otel.SetTracerProvider(tp)
    return nil
}
  • 修改 shop/cmd/shop/wire.go

注意這裡的注入的引數

// initApp init shop application.
func initApp(*conf.Server, *conf.Data, *conf.Auth, *conf.Service, *conf.Registry, log.Logger) (*kratos.App, func(), error) {
    panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}

這一篇主要是準備工作,下一篇開始實現介面

本作品採用《CC 協議》,轉載必須註明作者和本文連結
高永立

相關文章