Go项目(三)

news2024/9/23 1:30:50

文章目录

  • 用户微服务
    • 表结构
    • 查表
  • web 服务
    • 跨域问题
    • 图形验证码
    • 短信
    • 用户注册
  • 服务中心
    • 注册 grpc 服务
    • 动态获取端口
  • 负载均衡
  • 配置中心
  • 启动项目
  • 小结

用户微服务

  • 作为系统的第一个微服务,开发的技术点前面已经了解了一遍,虽有待补充,但急需实战
  • 这里主要梳理架构和开发步骤,为整个开发定下基调
  • go version 升到 1.20

表结构

  • 在 model 定义用户表结构;技术点:gorm
    • 密文保存密码,使用信息摘要算法 md5 配合盐值
    • 使用工具,可指定使用的算法,验证密码
    • 采用比较普遍的做法:将算法、盐值、密码拼接存入数据库
  • 将表创建:_ = db.AutoMigrate(&model.User{})

查表

  • 在 proto 文件定义需要的方法,请求参数,返回值;技术点:protobuf
    • 主要是数据库相关的操作
  • 生成代码,handler 中实现接口中定义的方法,即 server 端逻辑
    • protoc -I . user.proto --go_out=plugins=grpc:.
    • 返回值一定要严格按照 message 定义
      type UserServer interface {
      	GetUserList(context.Context, *PageInfo) (*UserListResponse, error)
      	GetUserByMobile(context.Context, *MobileRequest) (*UserInfoResponse, error)
      	GetUserById(context.Context, *IdRequest) (*UserInfoResponse, error)
      	CreateUser(context.Context, *CreateUserInfo) (*UserInfoResponse, error)
      	UpdateUser(context.Context, *UpdateUserInfo) (*emptypb.Empty, error)
      	CheckPassWord(context.Context, *PasswordCheckInfo) (*CheckResponse, error)
      }
      
  • GetUserList/GetUserById
    • 使用 gorm 的 DB struct 和分页方法
    • 定义将 model struct 转换为 message response 的函数
  • GetUserByMobile
    • 使用 config + viper 配合 yaml 文件做全局配置,可以回顾前面的文章
    • 使用 grpc 的 status 返回状态信息,日志和错误处理很重要
  • UpdateUser
    • 处理时间:time.Unix(int64(req.BirthDay)
  • 验证密码
    • 使用工具的 Verify 函数

web 服务

  • 上面的用户微服务是 service 层,接下来通过 gin 将 service 层的 api 暴露出来,也就是 client 端 API
    • 当然,先从 user-web 开始,api/user.go
    • 也可以通过 grpc 的 gateway 暴露
  • 日志配置,使用 zap 将日志输出到文件,可以回顾前面的文章,或者看官方教程
    • 用 Sugar 会损失一些性能(反射判断类型),但是好用
    • 配置 zap.S()
  • 初始化放在 initialize
    • 初始化 Router := gin.Default(),只能有一个,通过 Group 添加分组
    • 初始化日志
    • 把server 端的 proto 文件拿过来,生成 stub,开始编写客户端调用;client 端和页面在这里是前端
    • 转换 grpc status 为 HTTP status code
    • 在 global/user.go 定义格式
      • 得到 server 端的 response 可以直接用 map[sting]interface{} 存用户信息,但是切片不够优雅
      • 为前端定义 UserResponse struct,实例化 user 再加入 []interface{}
      • 为 birthday 重写 MarshalJSON 方法,返回指定的时间格式
  • 配置文件管理工具;技术点:viper
    • 将文件解析成内部的 struct 是 go 语言中非常常见的操作,上面为返回值定义 struct 也是一样的道理,在 go 环境中操作,自然是 struct 更方便;定义 struct 时打 tag 也是这个作用,把文件/json体中的数据映射成 struct 字段值
    • viper 可以自动将 yaml 文件解析成 struct
    • 将线上和线下配置文件隔离:在本地设置环境变量,比如 SHOP_DEBUG=true,代表开发环境
      func GetEnvInfo(env string) bool {
      	viper.AutomaticEnv()
      	return viper.GetBool(env)
      	//刚才设置的环境变量 想要生效 我们必须得重启goland
      }
      
    • 服务器启动后,使用 v.WatchConfig() 配合 fsnotify 监听配置文件变化
      v.AddConfigPath(configFileName)
      viper.WatchConfig()		// 要放在 AddConfigPath 之后
      viper.OnConfigChange(func(e fsnotify.Event) {
      	fmt.Println("Config changed.", e.Name)
      })
      err := viper.ReadConfig()
      err := viper.Unmarshal()
      
    • 后面会引入分布式服务的配置中心
    • 接口(API)和前端页面都可以通过 yapi 做 mock 测试,有的接口也可以写 UT 测试,可以回顾前面的文章
    • 一些组件的使用方法可以临时新建目录写 demo 测试
    • 注:初始化操作的调用和配置文件的加载都是在 main.go入口
  • 密码登录 PassWordLogin
    • 表单验证,可以回顾前面的文章,这部分都是用验证器实现
    • 初始化 validator 中实现本地化(多语言),通过修改 binding.Validator 引擎的属性
    • 自定义手机号验证器并注册到 gin/binding(注册个 tag),表单字段的验证也是用这个;也要给自定义的验证器注册翻译器,binding 中定义好的验证器(比如 required)已经通过修改引擎实现了翻译
      import (
      	"regexp"
      	// 用 v10
      	"github.com/go-playground/validator/v10"
      )
      
      func ValidateMobile(fl validator.FieldLevel) bool {
      	mobile := fl.Field().String()
      	//使用正则表达式判断是否合法
      	ok, _ := regexp.MatchString(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`, mobile)
      	if !ok{
      		return false
      	}
      	return true
      }
      
      // main.go
      //注册validator/中的验证器,验证手机号
      if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
      	_ = v.RegisterValidation("mobile", myvalidator.ValidateMobile)
      	_ = v.RegisterTranslation("mobile", global.Trans, func(ut ut.Translator) error {
      		return ut.Add("mobile", "{0} 非法的手机号码!", true) // see universal-translator for details
      	}, func(ut ut.Translator, fe validator.FieldError) string {
      		t, _ := ut.T("mobile", fe.Field())
      		return t
      	})
      }
      
    • 验证密码的逻辑就是先根据手机号查询,再比对密码;使用 proto 中的方法(已经在server端定义好),注意参数形式,对照 proto 文件写
    • 关键在于验证通过后的处理,使用 sessionId 还是 token?
  • session 机制
    • 在单体应用中,登录成功会返回 sessionId 存在 cookie,表明身份
      1
    • 在微服务中,由于服务之间隔离,在用户服务保存的 sessionid 不能在商品服务验证,需要有个公用的数据库
      2
    • 但我们不用上面的方案,而是采用更方便的 JWT(json web token)
  • JWT
    • 基础知识
    • 一般放在浏览器的 Headers 中,可以自定义名称,比如 x-token
    • JWT 相关代码是通用的,放在 middlewares下面;models 下定义 payload struct
      • payload 不能放敏感信息,因为是没有加密的,我们只是通过 Signature 部分判断 token 有效性(secret 只存在服务器,一定不能泄露;类似对称加密,私钥签私钥解)
      • 如何保证JWT的安全呢?一般会把过期时间设置短一些
      • 可以在官网测试一下生成的 token
    • 然后给 URL 添加 token 验证
      • 使用鉴权函数 JWTAuth(),因为是作为中间件要注册的,所以写法上是返回函数 return func(c *gin.Context)
      • 鉴权后使用c.Set()将登录用户的信息保存在 context,方便后端获取
      • URL 的鉴权在 router 中添加 middlewares.JWTAuth() 即可
      • 如果是个整个 Group 添加,直接用 .Use(middlewares.JWTAuth())
    • 验证是否为管理员
      • 还是在 middlewares 定义鉴权函数,用刚保存的 context 获取 AuthorityId,如果为 2 则是管理员
      • Role Id 是在登录时就从数据库获取并保存在 token
      • 可以做基于 Role 的访问控制(RBAC),对访问 API 做控制
    • 更多注释在代码中,可以 clone 代码研究

跨域问题

  • 跨域(protocol/ip/port 不一致)一般会报 404
  • 在发起复杂请求且跨域时(这种情况很常见),浏览器出于安全性考虑,会发起 OPTIONS 预检请求
    • 预检是 CORS(跨域资源共享) 中一种透明服务器验证机制。预检请求首先需要向另外一个域名的资源发送一个 HTTP OPTIONS 请求头,其目的是为了判断实际发送的请求是否是安全的
    • 测试代码,本地请求用户列表,会跨域
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>Title</title>
          <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
      </head>
      <body>
          <button type="button" id="query">请求数据</button>
          <div id="content" style="background-color: aquamarine; width: 300px;height: 500px;"></div>
      </body>
      <script type="text/javascript">
          $("#query").click(function () {
          $.ajax(
              {
                  url:"http://127.0.0.1:8021/u/v1/user/list",
                  dataType: "json",
                  type: "get",
                  beforeSend: function(request) {
                      request.setRequestHeader("x-token", "eyJhbGciOiJIUzI1NiIsInR5cC")
                  },
                  success: function (result) {
                      console.log(result.data);
                      $("#content").text(result.data);
                  },
                  error: function (data) {
                      alert("请求出错")
                  }
              );
          });
      </script>
      </html>
      
    • 跨域可以在前端或者后端解决
  • 这里我们在后端解决,还是使用中间件 cors.go,设置 Header 字段 Access-Control-Allow-Methods
    package middlewares
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func Cors() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		method := c.Request.Method
    
    		c.Header("Access-Control-Allow-Origin", "*")
    		c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")
    		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
    		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
    		c.Header("Access-Control-Allow-Credentials", "true")
    		
    		// 不需要预检,直接 abort,再请求原地址即可
    		if method == "OPTIONS" {
    			c.AbortWithStatus(http.StatusNoContent)
    		}
    	}
    }
    
  • 在初始化时配置跨域:Router.Use(middlewares.Cors())

图形验证码

  • 参考文档,相关逻辑放在 api/captcha.go
  • 这块路由我们新建 BaseRouter
  • 全局变量 store,is a shared storage for captchas,同一个包(目录)内的 go 文件都可以用
    • 提供了 Verify 方法,验证通过 form 传过来的验证码
  • 在登录表单中加上 Captcha 和 CaptchaId
    • 验证之后当前验证码就会失效,已经从变量中删除

短信

  • 用户注册或动态登录需要短信验证码
  • 使用阿里云的服务,去控制台申请 code,代码是固定的
  • 添加配置
    type RedisConfig struct {
    	Host   string `mapstructure:"host" json:"host"`
    	Port   int    `mapstructure:"port" json:"port"`
    	Expire int    `mapstructure:"expire" json:"expire"`	// 短信过期时间
    }
    type AliSmsConfig struct {
    	ApiKey     string `mapstructure:"key" json:"key"`
    	ApiSecrect string `mapstructure:"secrect" json:"secrect"`
    }
    
  • 把发出去的验证码存在 Redis,也能设置过期时间
    rdb.Set(context.Background(), sendSmsForm.Mobile, smsCode, time.Duration(global.ServerConfig.RedisInfo.Expire)*time.Second)
    
  • 给前端返回 “发送成功” 即可
  • 前端部分,用户输入验证码,自然需要定义表单了
    • 一般咱们写 API 的逻辑是从前端到后端,便于明确需求和参数
    • 当然,前端页面也要结合后端的设计,但总的来说应该先有前端的需求和项目的架构,绘制 Draft 界面
  • 以上是 SendSms 接口,给前端调的,但依托于两处验证,才会用到:动态登录,用户注册

用户注册

  • 最后一个 API
  • 前端需要接收用户的注册参数,client 端调用后端的 CreateUser 即可,我们从定义表单开始
  • 采用注册成功自动登录的逻辑,就不跳转到登录页面让用户再次输入了
    • 在这里创建 JWT,并返回和密码登录逻辑一样的数据即可
      // 返回给这些值就代表登录成功
      c.JSON(http.StatusOK, gin.H{
      	"id":         user.Id,
      	"nick_name":  user.NickName,
      	"token":      token,
      	"expired_at": (time.Now().Unix() + 60*60*24*30) * 1000,
      })
      
    • 用户发起 ajax 请求并根据返回字段判断是否登录成功

服务中心

  • 有很多服务注册和发现的工具,这里我们选择 consul,支持健康检查和 DNS
  • 前面的文章中整理过,可以写 python 代码测试 consul 的接口,也可以使用 postman
    • 这篇文章底层使用 python 编写 grpc 服务,但道理是相通的
  • 用 go 调用一下 consul 的各接口,方便后续接入
    package main
    
    import (
    	"fmt"
    
    	"github.com/hashicorp/consul/api"
    )
    
    func Register(address string, port int, name string, tags []string, id string) error {
    	cfg := api.DefaultConfig()
    	cfg.Address = "192.168.1.103:8500"
    
    	client, err := api.NewClient(cfg)
    	if err != nil {
    		fmt.Println(err)
    	}
    	//生成对应的检查对象
    	check := &api.AgentServiceCheck{
    		HTTP:                           "http://192.168.1.102:8021/health",
    		Timeout:                        "5s",
    		Interval:                       "5s",
    		DeregisterCriticalServiceAfter: "10s",
    	}
    	
    	//生成注册对象
    	// 两种方式
    	//registration := api.AgentServiceRegistration{
    	//	Kind:              "",
    	//	ID:                "",
    	//	Name:              "",
    	//	Tags:              nil,
    	//	Port:              0,
    	//	Address:           "",
    	//	SocketPath:        "",
    	//	TaggedAddresses:   nil,
    	//	EnableTagOverride: false,
    	//	Meta:              nil,
    	//	Weights:           nil,
    	//	Check:             nil,
    	//	Checks:            nil,
    	//	Proxy:             nil,
    	//	Connect:           nil,
    	//	Namespace:         "",
    	//	Partition:         "",
    	//}
    	// 我们使用 new 实例化struct
    	registration := new(api.AgentServiceRegistration)
    	registration.Name = name
    	registration.ID = id
    	registration.Port = port
    	registration.Tags = tags
    	registration.Address = address
    	registration.Check = check
    
    	err = client.Agent().ServiceRegister(registration)
    	// client.Agent().ServiceDeregister()
    	if err != nil {
    		fmt.Println(err)
    	}
    	return nil
    }
    
    func AllServices() {
    	cfg := api.DefaultConfig()
    	cfg.Address = "192.168.1.103:8500"
    
    	client, err := api.NewClient(cfg)
    	if err != nil {
    		panic(err)
    	}
    
    	data, err := client.Agent().Services()
    	if err != nil {
    		panic(err)
    	}
    	for key, _ := range data {
    		fmt.Println(key)
    	}
    }
    func FilterSerivice() {
    	cfg := api.DefaultConfig()
    	cfg.Address = "192.168.1.103:8500"
    
    	client, err := api.NewClient(cfg)
    	if err != nil {
    		panic(err)
    	}
    
    	data, err := client.Agent().ServicesWithFilter(`Service == "user-web"`)
    	if err != nil {
    		panic(err) // 如果报错不用管,IDE 的原因
    	}
    	for key, _ := range data {
    		fmt.Println(key)
    	}
    }
    
    func main() {
    	_ = Register("192.168.1.102", 8021, "user-web", []string{"vshop", "Roy"}, "user-web")
    	//AllServices()
    	//FilterSerivice()
    	// fmt.Println(fmt.Sprintf(`Service == "%s"`, "user-srv"))
    }
    
  • 服务没启动时,会显示这个检查结果
    1

注册 grpc 服务

  • 之前 user-web 通过 grpc.Dial 直接拨号连接 service 层的服务,现在用服务发现注册中心代替
    • 当然,Dial 是必须的,这里只不过是将 IP 和 Port 集中管理,查找获取,不再是写死的
  • 服务注册,将 grpc 服务(service层)注册到 consul 中
    • GRPC 或 HTTP 都是在 AgentServiceCheck 里指明;其实主要区别就是 consul 对它们健康检查的方式不一样,获取服务不都是IP/Port嘛
      4
      // 这是HTTP的检查方式,很简单,请求 /health 能返回即可
      // 生成对应的检查对象
      check := &api.AgentServiceCheck{
      	HTTP:                           fmt.Sprintf("http://%s:%d/health", address, port),
      	Timeout:                        "10s",
      	Interval:                       "30s",
      	DeregisterCriticalServiceAfter: "10s",
      }
      
      // 当然,要在路由初始化时定义 /health
      Router.GET("/health", func(c *gin.Context) {
      	c.JSON(http.StatusOK, gin.H{
      		"code":    http.StatusOK,
      		"success": true,
      		"msg":	"very healthy",
      	})
      })
      
    • 先把配置文件和日志工具集成进来:
    • 同样,新建 config/config.go,打上 mapstructure tag,为映射成 struct 做准备
      • 加上 consul 的配置(IP:Port)
    • main.go 调用 initialize 读取配置文件映射成全局 struct(放在 global 下)就可以开用了
      • 数据库初始化,包括数据库日志配置
      • zap.S() 日志初始化
    • 不同于 client(API)层,这里可以新建 test 目录使用 var userClient proto.UserClient 测试,不必等到 yapi 统一测试 client 端 API,不好定位问题
    • 给 grpc 服务配置健康检查,grpc 本身提供了接口,前面的文章也说过
      • 实现 CheckWatch 方法
      • 这里只需要加一句 grpc_health_v1.RegisterHealthServer(server, health.NewServer()),注册一个用于检查的 server
      • 这个 Server 还是一个 struct,且实现了上述两个方法,grpc/health 这个库帮我们写好了(可以看源码了解是怎么检查的)
      • consul 相当于 client 端,注册到 consul 之后,健康检查时就调用这两个方法
      • 为啥consul健康检查时会调用注册的grpc的两个方法呢?因为 consul 对 grpc 的健康检查就是这么定义的,从 consul 文档中也能找到
    • 按照上面的 demo 注册即可,注意 IP 和 port
  • user-web 使用 consul 进行服务发现(调用)
    • 传统做法,就是连上 consul,过滤得到需要的 user_srv,配置文件和相关代码如下:
      1
      2
    • 配置文件和 config 的 struct 一致,但这里我们 service 的 host 和 port 不再是从文件获取(不需要),而是拿着 Name/Id 从 consul 发现,并直接拨号
    • 问题也很明显,就是代码很长,比业务逻辑都长;怎么办?我们将连接 consul 获取用户服务这部分抽出来,放在 initialize/srv_conn.go,并返回给 global/UserSrvClient 全局使用
    • 这样做还有个好处,已经事先创立好了连接,这样后续就不用进行多次 tcp 的握手
    • 但也有问题,如何均衡发现的多个服务呢?这个问题在负载均衡部分解决

动态获取端口

  • 本地开发环境端口号固定,但线上环境需要自动获取端口号
    func GetFreePort() (int, error) {
    	addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
    	if err != nil {
    		return 0, err
    	}
    
    	l, err := net.ListenTCP("tcp", addr)
    	if err != nil {
    		return 0, err
    	}
    	defer l.Close()
    	return l.Addr().(*net.TCPAddr).Port,  nil
    }
    
  • 网络这块后面会研究,需要用 docker 部署,NGINX 转发

负载均衡

  • 什么是负载均衡,前面的文章解释过
    • 简而言之,用户到网关,网关到 API,API 到 Service之间,都需要负载均衡,为了抗住并发
    • 但是 API 和 Service 之间是 grpc 调用,如何做负载均衡呢?
  • 负载均衡策略(架构)
    • 进程内的负载均衡策略用的比较广泛,自定义 SDK 不是问题,要尽量避免使用第三方插件
  • 负载均衡算法(核心)
    • 使用轮询法(round_robin)
  • grpc 实现负载均衡
    • 先到 srv 部分,demo 还是看前面的文章
    • 启动多个 Server 测试,需要将 Serve 监听(请求)放在协程,避免阻塞,影响后续终止信号的监听
    • 因为负载均衡算法作用在 consul 和 grpc 上,所以不必在一次进程中连续请求,采用多次启动也能查看负载均衡的效果
  • 集成到 user-web,使用负载均衡连接 Server 端的服务
    import _ "github.com/mbobakov/grpc-consul-resolver"
    
    // 使用这个工具:grpc-consul-resolver,服务发现+负载均衡一并实现
    func InitSrvConn() {
    	consulInfo := global.ServerConfig.ConsulInfo
    	userConn, err := grpc.Dial(
    		fmt.Sprintf("consul://%s:%d/%s?wait=14s", consulInfo.Host, consulInfo.Port, global.ServerConfig.UserSrvInfo.Name),
    		grpc.WithInsecure(),
    		grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),
    	)
    	if err != nil {
    		zap.S().Fatal("[InitSrvConn] 连接 【用户服务失败】")
    	}
    
    	userSrvClient := proto.NewUserClient(userConn)
    	global.UserSrvClient = userSrvClient
    }
    
  • 负载均衡的代码目前只体现在 user-web 部分,因为它调用 user-srv,服务发现(不是srv的服务注册)+负载均衡一并使用 grpc-consul-resolver实现;按道理只要请求其他服务都应该配置负载均衡
  • 目前 user-srv 启动多个server做算法验证即可
    2
  • 注:user-web 也需要注册到 consul,因为后面也会被调用(发现)
    1

配置中心

  • 为什么使用配置中心,这部分在前面的文章分析过,Client(API)端和 Server 端都要用(各自启动时拉取配置)
  • 再梳理一下流程
    • 技术选型:nacos
    • 安装:由于nacos是基于Java的,所以选择使用docker安装,避免手动配置Java环境
      docker run --name nacos-standalone -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m -p 8848:8848 -d nacos/nacos-server:latest
      
    • 主要关注:配置管理,命名空间
      3
    • go nacos,是 go 语言操作 nacos 的 SDK,参考手册使用
      1
    • 写一个 go 的 demo,先搭建好上面环境,新建好配置
      2
      package main
      
      import (
      	//"encoding/json"
      	"fmt"
      	"github.com/nacos-group/nacos-sdk-go/clients"
      	"github.com/nacos-group/nacos-sdk-go/common/constant"
      	"github.com/nacos-group/nacos-sdk-go/vo"
      	//"shop_srvs/temp/config"
      )
      
      func main() {
      	// nacos的地址
      	sc := []constant.ServerConfig{
      		{
      			IpAddr: "192.168.109.128",
      			Port:   8848,
      		},
      	}
      
      	// nacos的client地址
      	cc := constant.ClientConfig{
      		NamespaceId:         "26195427-54f4-4a0b-8b25-29e7b654686b", // 如果需要支持多namespace,我们可以创建多个client,它们有不同的NamespaceId
      		TimeoutMs:           5000,
      		NotLoadCacheAtStart: true,
      		LogDir:              "tmp/nacos/log", // 日志和缓存目录,要配置
      		CacheDir:            "tmp/nacos/cache",
      		LogLevel:            "debug",
      	}
      	
      	// 动态配置客户端;到这一步相当于定位到了nacos的一个命名空间里,接下来只需传入ID和Group
      	configClient, err := clients.CreateConfigClient(map[string]interface{}{
      		"serverConfigs": sc,
      		"clientConfig":  cc,
      	})
      	// 创建动态配置客户端的另一种方式 (推荐)
      	//configClient, err := clients.NewConfigClient(
      	//	vo.NacosClientParam{
      	//		ClientConfig:  &clientConfig,
      	//		ServerConfigs: serverConfigs,
      	//	},
      	//)
      	if err != nil {
      		panic(err)
      	}
      
      	// 命名空间,group,ID 唯一定位
      	content, err := configClient.GetConfig(vo.ConfigParam{
      		DataId: "user-web",
      		Group:  "dev"})
      
      	if err != nil {
      		panic(err)
      	}
      	fmt.Println(content) //字符串 - json
      
      	//serverConfig := config.ServerConfig{}
      	//想要将一个json字符串转换成struct,需要去设置这个struct的tag
      	//json.Unmarshal([]byte(content), &serverConfig)
      	//fmt.Println(serverConfig)
      
      	//err = configClient.ListenConfig(vo.ConfigParam{
      	//	DataId: "user-web.json",
      	//	Group:  "dev",
      	//	OnChange: func(namespace, group, dataId, data string) {
      	//		fmt.Println("配置文件变化")
      	//		fmt.Println("group:" + group + ", dataId:" + dataId + ", data:" + data)
      	//	},
      	//})
      	//time.Sleep(3000 * time.Second)
      }
      
    • 代码包括了将获取到的配置映射成 struct,如果我们的配置文件是yaml格式,可以换成 json(go内置支持的映射方式)
    • 这是目前我们项目 user-web 部分的所有配置字段
      package config
      
      type UserSrvConfig struct {
      	Host string `mapstructure:"host" json:"host"`
      	Port int    `mapstructure:"port" json:"port"`
      	Name string `mapstructure:"name" json:"name"`
      }
      
      type JWTConfig struct {
      	SigningKey string `mapstructure:"key" json:"key"`
      }
      
      type AliSmsConfig struct {
      	ApiKey     string `mapstructure:"key" json:"key"`
      	ApiSecrect string `mapstructure:"secrect" json:"secrect"`
      }
      
      type ConsulConfig struct {
      	Host string `mapstructure:"host" json:"host"`
      	Port int    `mapstructure:"port" json:"port"`
      }
      
      type RedisConfig struct {
      	Host   string `mapstructure:"host" json:"host"`
      	Port   int    `mapstructure:"port" json:"port"`
      	Expire int    `mapstructure:"expire" json:"expire"`
      }
      
      type ServerConfig struct {
      	Name        string        `mapstructure:"name" json:"name"`
      	Port        int           `mapstructure:"port" json:"port"`
      	UserSrvInfo UserSrvConfig `mapstructure:"user_srv" json:"user_srv"`
      	JWTInfo     JWTConfig     `mapstructure:"jwt" json:"jwt"`
      	AliSmsInfo  AliSmsConfig  `mapstructure:"sms" json:"sms"`
      	RedisInfo   RedisConfig   `mapstructure:"redis" json:"redis"`
      	ConsulInfo  ConsulConfig  `mapstructure:"consul" json:"consul"`
      }
      
  • 集成到 user-web
    • 总配置文件 config-dev.yaml
      host: '192.168.109.128'
      port: 8848
      namespace: '26195427-54f4-4a0b-8b25-29e7b654686b'
      user: 'nacos'
      password: 'nacos'
      dataid: 'user-web'
      group: 'dev'
      
    • config/config.go 新增 nacos 的配置项,在初始化 InitConfig 里用 viper 读取并映射成 struct 操作
      type NacosConfig struct {
      	Host      string `mapstructure:"host"`
      	Port      uint64 `mapstructure:"port"`
      	Namespace string `mapstructure:"namespace"`
      	User      string `mapstructure:"user"`
      	Password  string `mapstructure:"password"`
      	DataId    string `mapstructure:"dataid"`
      	Group     string `mapstructure:"group"`
      }
      
  • 集成到 user_srv
    • 和上面的过程类似
    • 如何监听 nacos 的配置文件(总配置)的变化呢?使用 viper
    • 如何监听远程配置文件的变化呢?

启动项目

  • 环境准备
    • MySQL;initialize 里只做了初始化连接数据库,需要先手动创建表?handler 里如何确定表名(model.User作为参数即可吗)?
      docker run \
      --name mysql \
      -d \
      -p 3306:3306 \
      --restart unless-stopped \
      -v /mydata/mysql/log:/var/log/mysql \
      -v /mydata/mysql/data:/var/lib/mysql \
      -v /mydata/mysql/conf:/etc/mysql \
      -e MYSQL_ROOT_PASSWORD=123456 \
      mysql:5.7
      
      -- 按道理,容器暴露出3306端口远程即可连接,但要检查网络是否通畅
      -- https://blog.csdn.net/cljdsc/article/details/115207336
      
      -- 手动创建数据库,user-srv 服务使用
      create database shop_user_srv default charset utf8 collate utf8_general_ci;
      
    • 手动创建表
      func main() {
      	// 手动创建数据库 vshop_user_srv
      	dsn := "root:123456@tcp(192.168.109.128:3306)/shop_user_srv?charset=utf8mb4&parseTime=True&loc=Local"
      	
      	// 设置全局的logger,这个logger在我们执行每个sql语句的时候会打印每一行sql
      	newLogger := logger.New(
      		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
      		logger.Config{
      			SlowThreshold: time.Second, // 慢 SQL 阈值
      			LogLevel:      logger.Info, // Log level
      			Colorful:      true,        // 禁用彩色打印
      		},
      	)
      
      	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
      		// 表名
      		NamingStrategy: schema.NamingStrategy{
      			// 这里直接使用 model 名
      			SingularTable: true,
      		},
      		Logger: newLogger,
      	})
      	if err != nil {
      		fmt.Println(err)
      	}
      	
      	//定义一个表结构, 将表结构直接生成对应的表 - migrations
      	// 迁移 schema
      	_ = db.AutoMigrate(&model.User{})
      
      	// 写入一些数据
      	options := &password.Options{16, 100, 32, sha512.New}
      	salt, encodedPwd := password.Encode("admin123", options)
      	newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
      	fmt.Println(newPassword)
      
      	for i := 0; i < 10; i++ {
      		user := model.User{
      			NickName: fmt.Sprintf("Roy%d", i),
      			Mobile:   fmt.Sprintf("1878222222%d", i),
      			Password: newPassword,
      		}
      		db.Save(&user)
      	}
      }
      
    • nacos 上定义配置;本地配置文件定义 nacos 地址及需要的配置集信息即可
      // user-srv(dev)
      {
          "name": "user-srv",
          "host": "192.168.0.102",
          "port": 64924,	// 用于开发环境
          "tags": ["Roy", "shop", "go"],
          "mysql": {
              "db": "shop_user_srv",
              "host": "192.168.109.128",
              "port": 3306,
              "user": "root",
              "password": "123456"
          },
          "consul": {
              "host": "192.168.109.128",
              "port": 8500
          },
          "env": "SHOP_DEBUG"
      }
      
      // user-web(dev)
      {
        "name": "user-web",
        "host": "192.168.0.102",	// 在我的Windows上启动
        "tags":["shop", "Roy", "user", "web"],
        "port": 8021,
        // 服务发现
        "user_srv": {
        	// 只需保留name
          "name": "user-srv"
        },
        "jwt": {
          "key": "5$!UEmvB#nRB@Iwab#Sy!zofKEOGLRtE"
        },
        "sms": {
          "key": "",
          "secrect": "",
          "expire": 300
        },
        "redis": {
          "host": "192.168.109.128",
          "port": 6379
        },
        "consul": {
          "host": "192.168.109.128",
          "port": 8500
        },
        "env": "SHOP_DEBUG"
      }
      
    • sms 相关的 key 登录自己的阿里云控制台获取
    • redis 安装,默认在 6379 端口
    • 新建环境变量:SHOP_DEBUG=true,重启 GoLand
      • 开发环境端口号固定,生产环境随机获取
    • consul 配置
      • 项目启动会自动注册的
    • 我这里项目启动在 Windows 机器(0.102)
      • user-web 和 user-srv 项目都通过配置中心获取 ip/port,定义 ip 主要为了 consul 注册,定义端口主要是为了开发环境使用
      • 因为注册在 consul 的端口要看是 dev/pro,生产环境中是动态的,和这里配置的就不一样了
      • 目前,新加service或者client服务器就要新加一份配置(涉及到后期的集群管理和容灾)
      • MySQL、consul、nacos、redis、yapi 均启动在虚拟机(109.128)
      • 最终上线都要放在 Linux 服务器启动,目前为了方便写代码采用这种方式
  • yapi 测试
    • 这里是测试 API(user-web)部分
    • 要按照这个配置初始化,部署成功后访问:http://ip:3000/,邮箱登录,密码:ymfe.org;yapi 底层用了 MongoDB,nacos 底层用了 MySQL
      5
    • 导入定义了测试接口的 json 文件;安装扩展(解决跨域问题)
      6
    • 设置请求参数并发送
      7
    • yapi 可以严格规定请求参数和返回数据的格式,也就是预览中的那份文档,前后端必须严格遵从;可以mock,可以指定值
      8
    • 对于前端,要严格传参带header,yapi相当于后端;对于后端,一定要返回指定格式(类型就是数字字符串,但格式很多)的数据,yapi相当于前端
    • mock 地址固定,不能换 IP,因为是作为服务器的;发起请求可以换 IP,请求谁都可以
  • Q&A
    • user-web 中能监听配置文件变化,但好像没起作用?
    • 如何监听 nacos 中心的配置变化呢?
    • 换了热点,服务端 IP 变了,注册到 consul 异常?

小结

  • 这几篇和《Python+Go实践》的内容基本一致,只是 Server 层用了 Go 语言实现而非 Python
  • 对一些细节和流程也做了补充强调,有助于理解(也显得废话较多);基本定下了开发基调,后续会快速开发完所有的微服务,实现电商系统
  • 最后将重点放在 k8s 部署、单元测试、重构、调优

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/355793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pytorch离线快速安装

1.pytorch官网查看cuda版本对应的torch和torchvisionde 版本(ncvv -V&#xff0c;nvidia-sim查看cuda对应的版本) 2.离线下载对应版本&#xff0c;网址https://download.pytorch.org/whl/torch_stable.html 我下载的&#xff1a; cu113/torch-1.12.0%2Bcu113-cp37-cp37m-win_…

基于python的一款数据处理工具pandas

在python处理数据的时候&#xff0c;都免不了用pandas做数据处理。在数据处理时&#xff0c;都免不了用数据筛选来提取自己想要的数据&#xff0c;咱们今天就讲讲pandas的条件筛选。安装库建议做数据分析的酱友们安装anaconda3&#xff0c;这个包几乎包括了数据分析用的所需要的…

【博客623】Prometheus一条告警的触发流程与等待时间

Prometheus一条告警的触发流程与等待时间 1、与告警等待时间相关的参数 prometheus.yml global:# 数据采集间隔scrape_interval: 15s # 评估告警周期evaluation_interval: 15s # 数据采集超时时间默认10s# scrape_timeoutalertmanager.yml # route标记&#xff1a;告警…

Python urllib

Python urllib Python urllib 库用于操作网页 URL&#xff0c;并对网页的内容进行抓取处理。 本文主要介绍 Python3 的 urllib。 urllib 包 包含以下几个模块&#xff1a; urllib.request - 打开和读取 URL。urllib.error - 包含 urllib.request 抛出的异常。urllib.parse …

剑指Offer专项突击版题解八

71.按权重生成随机数 思考&#xff1a;说到平均的生成随机数&#xff0c;想到了水塘抽样法和彩票调度法。 水塘抽样算法适合于样本不确定&#xff0c;乃至于是变化的&#xff0c;每个样本的概率是一样的。 // 样本nums[],每个元素的被抽到的概率是一样的 index : 0 for i : 1;…

Kubernetes03:kubernetes 功能和架构

2.1 概述 Kubernetes 是一个轻便的和可扩展的开源平台&#xff0c;用于管理容器化应用和服务。通过 Kubernetes 能够进行应用的自动化部署和扩缩容。在 Kubernetes 中&#xff0c;会将组成应用的容 器组合成一个逻辑单元以更易管理和发现。Kubernetes 积累了作为 Google 生产环…

时序预测 | Python实现TCN时间卷积神经网络时间序列预测

时序预测 | Python实现TCN时间卷积神经网络时间序列预测 目录 时序预测 | Python实现TCN时间卷积神经网络时间序列预测预测效果基本介绍环境准备模型描述程序设计学习小结参考资料预测效果 基本介绍 递归神经网络 (RNN),尤其是 LSTM,非常适合时间序列处理。 作为研究相关技术…

生成模型技术发展过程

生成模型生成模型和判别模型的差异生成模型的目标是在给定了数据集D&#xff0c;并且假设这个数据集的底层分布(underlying distribution)是Pdata&#xff0c;我们希望够近似出这个数据分布。如果我们能够学习到一个好的生成模型&#xff0c;我们就能用这个生成模型为下游任务做…

【项目立项管理】

项目立项管理 很杂&#xff0c;可以根据左边的列表查看自己不会的 。。。 立项管理主要是解决项目的组织战略符合性问题 开发所需的成本和资源属于经济可行性 承建方组织资源和项目的匹配程度 内部立项目的&#xff1a; 为项目进行资源分配&#xff0c;确定项目绩效目标&am…

字节二面:10Wqps超高流量系统,如何设计?

超高流量系统设计思路 前言 在40岁老架构师 尼恩的**读者交流群(50)**中&#xff0c;大流量、高并发的面试题是一个非常、非常高频的交流话题。最近&#xff0c;有小伙伴面试字节时&#xff0c;遇到一个面试题&#xff1a; 10Wqps超高流量系统&#xff0c;该如何设计&#xf…

湿度敏感性等级(MSL)

趁着周末得空&#xff0c;也有意愿&#xff0c;赶紧把之前一直想写的这个主题完成了。 湿度敏感性等级&#xff0c;相信大部分人还是比较陌生的。 湿度敏感性等级&#xff1a;MSL&#xff0c;Moisture sensitivity level 之所以有这个等级&#xff0c;大概是因为以下原因&a…

软考高级-信息系统管理师之进度管理(最新版)

项目进度管理 项目进度管理1概述2项目进度管理过程2.1规划进度管理1、规划项目进度管理目的2、规划进度管理:输入3、规划进度管理:输出2.2定义活动1、为了更好地规划项目2、定义活动过程就是3、定义活动:输入4、定义活动:输出2.3排列活动顺序1、排列活动顺序2、排列活动顺序:输…

19 顺序存储二叉树

文章目录1 顺序存储二叉树的概念1 顺序存储二叉树的概念 从数据存储来看&#xff0c;数组存储方式和树的存储方式可以相互转换&#xff0c;即数组可以转换成树&#xff0c;树也可以转换成数组&#xff0c;看右面的示意图。 要求: 1) 右图的二叉树的结点&#xff0c;要求以数…

Git常用命令以及如何在IDEA中使用Git

前言Git是一个分布式版本控制工具&#xff0c;主要用于管理开发过程中的源代码文件&#xff08;Java类、xml文件、html页面等&#xff09;。Git在管理文件过程中会记录日志&#xff0c;方便回退到历史版本&#xff1b;Git存在分支的概念&#xff0c;一个项目可以有多个分支&…

linux014之进程和服务管理

linux中的进程管理&#xff1a; 查看进程命令&#xff1a; ps &#xff1a;查看应用级别的进程 ps -e&#xff1a; 查看系统应用级的进程 ps -ef &#xff1a;显示进程的全部信息(这个命令经常用) ps -ef|grep 关键字&#xff1a; 查看带有关键字的进程 关闭进程命令&#xff1…

win11+pytorch1.7.0+python3.8(也可以是python3.7)+cuda11.0

win11pytorch1.7.0python3.8cuda11.0写在最前面一. 软件配备二. 创建虚拟环境2.1 创建环境名称&#xff0c;并选择‘y’2.2.进入虚拟环境三. 安装pytorch四. 安装虚拟环境下的notebook4.1 安装ipykernel&#xff0c;并选择‘y’&#xff0c;直至安装完成4.2 将环境名注册到note…

安全—09day

XSS1. XSS的原理2. Xss漏洞分类2.1 反射性xss2.2 存储型XSS2.3 基于DOM的 XSS2.4 XSS漏洞的危害3. XSS的各种bypass技巧4. 从 XSS Payload 学习浏览器解码5. 浏览器解析机制5.1 HTML中有五类元素&#xff1a;5.2 五类元素的区别1. XSS的原理 恶意攻击者往Web页面里插入恶意Scr…

AcWing:4861. 构造数列、4862. 浇花(C++)

目录 4861. 构造数列 问题描述&#xff1a; 实现代码&#xff1a; 4862. 浇花 问题描述&#xff1a; 实现代码&#xff1a; 4861. 构造数列 问题描述&#xff1a; 我们规定如果一个正整数满足除最高位外其它所有数位均为 00&#xff0c;则称该正整数为圆数。 例如&…

Redis基础操作以及数据类型

目录 Redis基础操作 java中的i是不是原子操作&#xff1f;不是 数据类型 1. list 2. set 3. Hash哈希 4. Zset有序集合 Redis基础操作 set [key] [value] 设置值 &#xff08;设置相同的会将原先的覆盖&#xff09; get [key] 获取值 不能覆盖和替换 ttl [key] 以秒为单…

【uniapp】开发一款手机端go语言教程软件

文章目录介绍数据源体验优化总结介绍 闲来无事&#xff0c;折腾一款 go语言教程软件&#xff0c;软件使用uniapp-cli&#xff0c;vue2编写。 界面很简单&#xff0c;侧边栏为目录&#xff0c;主页是由markdown渲染的正文。 单击悬浮按钮打开目录&#xff0c;双击回到正文顶部。…