文章目录
- 用户微服务
- 表结构
- 查表
- 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
方法,返回指定的时间格式
- 得到 server 端的 response 可以直接用
- 初始化
- 配置文件管理工具;技术点: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,表明身份
- 在微服务中,由于服务之间隔离,在用户服务保存的 sessionid 不能在商品服务验证,需要有个公用的数据库
- 但我们不用上面的方案,而是采用更方便的 JWT(json web token)
- 在单体应用中,登录成功会返回 sessionId 存在 cookie,表明身份
- 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 做控制
- 还是在 middlewares 定义鉴权函数,用刚保存的 context 获取
- 更多注释在代码中,可以 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 请求并根据返回字段判断是否登录成功
- 在这里创建 JWT,并返回和密码登录逻辑一样的数据即可
服务中心
- 有很多服务注册和发现的工具,这里我们选择 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")) }
- 服务没启动时,会显示这个检查结果
注册 grpc 服务
- 之前 user-web 通过
grpc.Dial
直接拨号连接 service 层的服务,现在用服务发现注册中心代替- 当然,Dial 是必须的,这里只不过是将 IP 和 Port 集中管理,查找获取,不再是写死的
- 先服务注册,将 grpc 服务(service层)注册到 consul 中
- GRPC 或 HTTP 都是在
AgentServiceCheck
里指明;其实主要区别就是 consul 对它们健康检查的方式不一样,获取服务不都是IP/Port嘛
// 这是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 本身提供了接口,前面的文章也说过
- 实现
Check
和Watch
方法 - 这里只需要加一句
grpc_health_v1.RegisterHealthServer(server, health.NewServer())
,注册一个用于检查的 server - 这个 Server 还是一个 struct,且实现了上述两个方法,grpc/health 这个库帮我们写好了(可以看源码了解是怎么检查的)
- consul 相当于 client 端,注册到 consul 之后,健康检查时就调用这两个方法
- 为啥consul健康检查时会调用注册的grpc的两个方法呢?因为 consul 对 grpc 的健康检查就是这么定义的,从 consul 文档中也能找到
- 实现
- 按照上面的 demo 注册即可,注意 IP 和 port
- GRPC 或 HTTP 都是在
- user-web 使用 consul 进行服务发现(调用)
- 传统做法,就是连上 consul,过滤得到需要的 user_srv,配置文件和相关代码如下:
- 配置文件和 config 的 struct 一致,但这里我们 service 的 host 和 port 不再是从文件获取(不需要),而是拿着 Name/Id 从 consul 发现,并直接拨号
- 问题也很明显,就是代码很长,比业务逻辑都长;怎么办?我们将连接 consul 获取用户服务这部分抽出来,放在 initialize/srv_conn.go,并返回给 global/UserSrvClient 全局使用
- 这样做还有个好处,已经事先创立好了连接,这样后续就不用进行多次 tcp 的握手
- 但也有问题,如何均衡发现的多个服务呢?这个问题在负载均衡部分解决
- 传统做法,就是连上 consul,过滤得到需要的 user_srv,配置文件和相关代码如下:
动态获取端口
- 本地开发环境端口号固定,但线上环境需要自动获取端口号
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做算法验证即可
- 注:user-web 也需要注册到 consul,因为后面也会被调用(发现)
配置中心
- 为什么使用配置中心,这部分在前面的文章分析过,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
- 主要关注:配置管理,命名空间
- go nacos,是 go 语言操作 nacos 的 SDK,参考手册使用
- 写一个 go 的 demo,先搭建好上面环境,新建好配置
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"` }
- 总配置文件 config-dev.yaml
- 集成到 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 服务器启动,目前为了方便写代码采用这种方式
- MySQL;initialize 里只做了初始化连接数据库,需要先手动创建表?handler 里如何确定表名(model.User作为参数即可吗)?
- yapi 测试
- 这里是测试 API(user-web)部分
- 要按照这个配置初始化,部署成功后访问:http://ip:3000/,邮箱登录,密码:
ymfe.org
;yapi 底层用了 MongoDB,nacos 底层用了 MySQL
- 导入定义了测试接口的 json 文件;安装扩展(解决跨域问题)
- 设置请求参数并发送
- yapi 可以严格规定请求参数和返回数据的格式,也就是预览中的那份文档,前后端必须严格遵从;可以mock,可以指定值
- 对于前端,要严格传参带header,yapi相当于后端;对于后端,一定要返回指定格式(类型就是数字字符串,但格式很多)的数据,yapi相当于前端
- mock 地址固定,不能换 IP,因为是作为服务器的;发起请求可以换 IP,请求谁都可以
- Q&A
- user-web 中能监听配置文件变化,但好像没起作用?
- 如何监听 nacos 中心的配置变化呢?
- 换了热点,服务端 IP 变了,注册到 consul 异常?
小结
- 这几篇和《Python+Go实践》的内容基本一致,只是 Server 层用了 Go 语言实现而非 Python
- 对一些细节和流程也做了补充强调,有助于理解(也显得废话较多);基本定下了开发基调,后续会快速开发完所有的微服务,实现电商系统
- 最后将重点放在 k8s 部署、单元测试、重构、调优