写在前面
最近稍微重构了之前写的 grpc-todolist 模块
项目地址:https://github.com/CocaineCong/grpc-todoList
1. 项目结构改变
与之前的目录有很大的区别
1.1 grpc_todolist 项目总体
1.1.1 改变前
grpc-todolist/
├── api-gatway // 网关模块
├── task // task模块
└── user // user模块
v1版本的项目结构是分成了三个模块,网关,task,user模块。每个模块下各自读取配置文件,各自进行服务注册。
http 请求进入网关之后,网关开始转发并调用 rpc 请求,user、task模块接受到 rpc 调用之后,开始进行操作业务逻辑处理,结果返回给网关,网关再次对客户端进行 resp 响应。
基本思想没有问题,但是这种结构的问题是重复代码过多,很多代码是可以复用的,就没必要都写。
1.1.2 改变后
我们抽离出各个微服务模块下相同的内容,比如config,pkg,proto
的内容,并且新建一个app文件夹,放置所有的gateway、task、user模块中所独有的,并且启动文件都放到各个模块下的cmd中。
grpc-todolist/
├── app // 各个微服务
│ ├── gateway // 网关
│ ├── task // 任务模块微服务
│ └── user // 用户模块微服务
├── bin // 编译后的二进制文件模块
├── config // 配置文件
├── consts // 定义的常量
├── doc // 接口文档
├── idl // protoc文件
│ └── pb // 放置生成的pb文件
├── logs // 放置打印日志模块
├── pkg // 各种包
│ ├── e // 统一错误状态码
│ ├── discovery // etcd服务注册、keep-alive、获取服务信息等等
│ ├── res // 统一response接口返回
│ └── util // 各种工具、JWT、Logger等等..
└── types // 定义各种结构体
1.2 gateway网关模块
网关模块单单只是处理http请求,没有任何的业务逻辑,所以基本都是大量的中间件的使用。比如jwt鉴权,限流,etcd的服务发现等等…
gateway/
├── cmd // 启动入口
├── internal // 业务逻辑(不对外暴露)
│ ├── handler // 视图层
│ └── service // 服务层
│ └── pb // 放置生成的pb文件
├── logs // 放置打印日志模块
├── middleware // 中间件
├── routes // http 路由模块
└── rpc // rpc 调用
1.3. 各微服务模块
各个微服务的结构比较简单,因为各微服务模块都是处于一种被调用的状态,在该模块下聚焦好业务即可。
user/
├── cmd // 启动入口
└── internal // 业务逻辑(不对外暴露)
├── service // 业务服务
└── repository // 持久层
└── db // 视图层
├── dao // 对数据库进行操作
└── model // 定义数据库的模型
1.4 项目的总结模块
- 抽离出 proto 成 idl ,抽离pkg,config等公共模块。
- 简化各个微服务模块的结构。
2. 代码层级的改变
2.1 RPC的调用方式
2.1.1 改变前
在v1版本中,我们是将我们的微服务的服务实例放到 gin.Key
中
func InitMiddleware(service []interface{}) gin.HandlerFunc {
return func(context *gin.Context) {
// 将实例存在gin.Keys中
context.Keys = make(map[string]interface{})
context.Keys["user"] = service[0]
context.Keys["task"] = service[1]
context.Next()
}
}
通过断言的方式取出这个服务实例
func UserRegister(ginCtx *gin.Context) {
var userReq service.UserRequest
PanicIfUserError(ginCtx.Bind(&userReq))
// 从gin.Key中取出服务实例
userService := ginCtx.Keys["user"].(service.UserServiceClient)
userResp, err := userService.UserRegister(context.Background(), &userReq)
PanicIfUserError(err)
r := res.Response{
Data: userResp,
Status: uint(userResp.Code),
Msg: e.GetMsg(uint(userResp.Code)),
}
ginCtx.JSON(http.StatusOK, r)
}
这种结构是没有什么问题,但是就是缺少 rpc调度 那个味道。
2.1.2 改变后
我们可以新建一个rpc的文件来存储rpc相关,例如下面这个代码,是对下游的 UserRegister 进行调用。
func UserRegister(ctx context.Context, req *userPb.UserRequest) (resp *userPb.UserCommonResponse, err error) {
r, err := UserClient.UserRegister(ctx, req)
if err != nil {
return
}
if r.Code != e.SUCCESS {
err = errors.New(r.Msg)
return
}
return
}
然后在 handler 这里调用 rpc 进行操作。而不需要把服务实例放到ctx中,去取服务实例来进行调用。
func UserRegister(ctx *gin.Context) {
var userReq pb.UserRequest
if err := ctx.Bind(&userReq); err != nil {
ctx.JSON(http.StatusBadRequest, ctl.RespError(ctx, err, "绑定参数错误"))
return
}
r, err := rpc.UserRegister(ctx, &userReq)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ctl.RespError(ctx, err, "UserRegister RPC服务调用错误"))
return
}
ctx.JSON(http.StatusOK, ctl.RespSuccess(ctx, r))
}
2.2 Makefile文件编写
makefile文件是起到项目的快速启动和关闭的作用
2.2.1 proto文件的快速生成
这里使用protoc
和protoc-go-inject-tag
的命令来生成.pb.go
文件。
.PHONY: proto
proto:
@for file in $(IDL_PATH)/*.proto; do \
protoc -I $(IDL_PATH) $$file --go-grpc_out=$(IDL_PATH)/pb --go_out=$(IDL_PATH)/pb; \
done
@for file in $(shell find $(IDL_PATH)/pb/* -type f); do \
protoc-go-inject-tag -input=$$file; \
done
protoc命令:用于生成pb.go,grpc.go文件。
protoc-go-inject-tag 命令:用于重写pb.go文件中的tag,使得能加入json:“xxx” form:"xxx"等tag来进行操作。
例如如下proto文件,如果没有 protoc-go-inject-tag 那么我们生成的 NickName 中就是大写,也就是接受参数是大写,但我们一般是使用小写来接受参数。
message UserRequest{
// @inject_tag: json:"nick_name" form:"nick_name" uri:"nick_name"
string NickName=1;
// @inject_tag: json:"user_name" form:"user_name" uri:"user_name"
string UserName=2;
// @inject_tag: json:"password" form:"password" uri:"password"
string Password=3;
// @inject_tag: json:"password_confirm" form:"password_confirm" uri:"password_confirm"
string PasswordConfirm=4;
}
当然,除了这个解决方法我们可以将NickName写成nickname小写
,也是可以的,protoc会自动在代码层面,将nickname变成Nickname,代码层面还是可以调用的,并且接受参数还可以是小写。例如这样
message UserRequest{
string nick_name=1;
string user_name=2;
string password=3;
string password_confirm=4;
}
看个人喜好吧…
2.2.2 环境的快速启动
快速启动环境
.PHONY: env-up
env-up:
docker-compose up -d
这里会根据compose文件,来进行环境的快速启动。
创建的时候,已经是创建了数据库了,所以我们只需要去到各个模块下的cmd文件夹下进行启动微服务即可,例如到 app/user/cmd,启动这个main.go就可以启动user模块了。
快速关闭环境
.PHONY: env-down
env-down:
docker-compose down
以上就是这次grpc重构过程中的重要更新,大家可以把项目clone下来,自己跑跑,切换一下v1,v2分支来进行感受这一次的改变。