User rpc创建
cd
到 FoodGuides
目录下。创建 rpc
文件夹
mkdir -p usermanage/rpc/user && cd usermanage/rpc/user
在rpc/user
目录下编写user.proto
文件
goctl rpc template -o user.proto
编写 user.proto
文件
syntax = "proto3";
package user;
message LoginRequest {
string email = 1;
string password = 2;
}
message RegisterRequest {
string Username = 1;
string Email = 2;
string Password = 3;
}
message UserinfoRequest {
string Userid = 1;
string Token = 2;
}
message Response {
int64 id = 1;
string email = 2;
string accessToken = 3;
int64 accessExpire = 4;
int64 refreshAfter = 5;
}
service User {
rpc Login(LoginRequest) returns(Response);
rpc Register(RegisterRequest) returns(Response);
rpc Userinfo(UserinfoRequest) returns(Response);
}
我们定义了三个接口 :Login Register UserInfo
生成 user-rpc
服务
goctl rpc proto -src user.proto -dir .
查看一下 rpc/user
目录
➜ user git:(master) ✗ tree
.
├── etc
│ └── user.yaml
├── internal
│ ├── config
│ │ └── config.go
│ ├── logic
│ │ ├── loginlogic.go
│ │ ├── registerlogic.go
│ │ └── userinfologic.go
│ ├── server
│ │ └── userserver.go
│ └── svc
│ └── servicecontext.go
├── user
│ └── user.pb.go
├── user.go
├── user.proto
└── userclient
└── user.go
8 directories, 11 files
➜ user git:(master) ✗
API Gateway
代码调用 user rpc
服务
编辑 api/etc
下的 user-api.yaml
文件,新增 user.rpc
配置
Name: user-api
Host: 0.0.0.0
Port: 8888
User:
Etcd:
Hosts:
- localhost:2379
Key: user.rpc
编辑 api/internal/config
下的 config.go
文件,新增 User
变量
type Config struct {
rest.RestConf
User zrpc.RpcClientConf
}
编辑 api/internal/svc
下的 servicecontext.go
文件,新增 User
变量 ,新增实例化代码。
type ServiceContext struct {
Config config.Config
User userclient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
User: userclient.NewUser(zrpc.MustNewClient(c.User)),
}
}
编辑 api/internal/logic
下的 loginlogic.go
文件,新增 调用 user rpc
的 login
方法
func (l *LoginLogic) Login(req types.LoginRequest) (*types.LoginResponse, error) {
// todo: add your logic here and delete this line
resp,err := l.svcCtx.User.Login(l.ctx, &user.LoginRequest{
Email: req.Email,
Password: req.Password,
});
if err != nil {
return nil, err
}
token := types.JwtToken{
AccessToken: resp.AccessToken,
AccessExpire: resp.AccessExpire,
RefreshAfter: resp.RefreshAfter,
}
response := types.UserReply{
Id: resp.Id,
Email: resp.Email,
JwtToken: token,
}
return &types.LoginResponse{
response,
},nil
}
定义数据库表结构,并生成CRUD+cache代码
在 usermanage
下创建 model
文件夹。
mkdir -p model & cd model
在 model
下新建 user.sql
文件并编写如下内容。
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户Id',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名称',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `name_index` (`name`),
UNIQUE KEY `email_index` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
在 model
目录下执行如下命令生成 CRUD+cache
代码,-c
表示使用 redis cache
goctl model mysql ddl -c -src user.sql -dir .
查看 model
目录结构
➜ model git:(master) ✗ tree
.
├── user.sql
├── usermodel.go // CRUD+cache代码
└── vars.go // 定义常量和变量
在本机 mysql
中创建 foodguides
数据库,并新建 user
表。
新增一条数据
INSERT INTO `foodguides`.`user`(`id`, `name`, `password`, `email`) VALUES (1, 'Ningxi', 'd89617870c6f8a028f5728be69cc09d4cd3585b4651b7f206f1cd674bb4351ec', 'ningxi@ningxi.com');
rpc
代码调用 crud+cache
代码
编辑 rpc/user/etc
下的 user.yaml
文件,新增如下内容。
注意这里的 mysql
使用的是 ningxi-compose
跑的 docker
容器,因此参数需要注意。
DataSource: root:2e70F5E6@(localhost:13306)/foodguides?parseTime=true
Table: user
Cache:
- Host: localhost:16379
AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
AccessExpire: 86400
Salt: ^&yh
编辑 rpc/user/internal/config
下的 config.go
文件,新增 DataSource Cache AccessSecret AccessExpire
变量
type Config struct {
zrpc.RpcServerConf
DataSource string
Cache cache.CacheConf
AccessSecret string
AccessExpire int64
Salt string
}
编辑 rpc/user/internal/svc
下的 serviceContext.go
文件,新增 Model
变量 ,新增实例化代码。
type ServiceContext struct {
Config config.Config
Model model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Model: model.NewUserModel(sqlx.NewMysql(c.DataSource),c.Cache),
}
}
编辑 rpc/user/internal/logic
下的 loginlogic.go
文件, 新增如下代码
func (l *LoginLogic) Login(in *user.LoginRequest) (*user.Response, error) {
res, err := l.svcCtx.Model.FindOneByEmail(in.Email)
if err == nil {
passwords := ningxi.PasswordEncrypt(l.svcCtx.Config.Salt,in.Password)
if passwords == res.Password {
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.AccessExpire
jwtToken, err := l.getJwtToken(l.svcCtx.Config.AccessSecret, now, accessExpire, res.Id)
if err != nil {
return nil, err
}
response := user.Response{
Email: res.Email,
Id: res.Id,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}
return &response, nil
} else {
return nil, errors.New("密码错误")
}
}
return nil, err
}
我在 loginlogic
中增加了 token
生成、密码加密验证的相关代码,这样当用户登录成功时,我们将为用户生成一个 token
返回给客户端。
修改 api
response
返回格式
我希望客户端接口请求数据的返回格式是这样子的
{
"code": 1,
"message": "",
"result": {
"id": 1,
"username": "",
"email": "ningxi@ningxi.com",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE0NzAzOTQsImlhdCI6MTYxMTM4Mzk5NCwidXNlcklkIjoxfQ.8EJU0XDZ535NZvtCPgyOg9RVw3FAdG5AJktHYcjEGo0",
"accessExpire": 1611470394,
"refreshAfter": 1611427194
}
}
在 foodguides
文件夹下 新增 ningxi
文件夹,并创建 ningxi.go
文件,新增如下代码
import (
"fmt"
"golang.org/x/crypto/scrypt"
)
type HttpResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Result interface{} `json:"result"`
}
func SuccessResponse(resData interface{},message string) HttpResponse {
return HttpResponse{Code:1,Message: message,Result: resData}
}
func FailureResponse(resData interface{},message string,code int) HttpResponse {
return HttpResponse{Code:code,Message: message,Result: resData}
}
func PasswordEncrypt(salt,password string) string {
dk,_ := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
return fmt.Sprintf("%x",string(dk))
}
编辑 api/internal/handler 下的loginhandler.go 新增如下代码
func LoginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginRequest
if err := httpx.Parse(r, &req); err != nil {
httpx.OkJson(w, ningxi.FailureResponse(nil,err.Error(),1000))
return
}
l := logic.NewLoginLogic(r.Context(), ctx)
resp, err := l.Login(req)
if err != nil {
httpx.OkJson(w, ningxi.FailureResponse(nil,err.Error(),1000))
} else {
httpx.OkJson(w, ningxi.SuccessResponse(resp,""))
}
}
}
启动服务
启动服务,注意 在启动服务前,需要确保 上一篇文章用到的 ningxi-compose
正常运行起来。
启动 user rpc
服务, 运行成功后,user rpc
则运行在本机的 8080
端口
➜ FoodGuides git:(master) ✗ go run usermanage/rpc/user/user.go -f usermanage/rpc/user/etc/user.yaml
Starting rpc server at 127.0.0.1:8080...
启动 user api
服务, 运行成功后,user api
则运行在本机的 8888
端口
➜ FoodGuides git:(master) ✗ go run usermanage/api/user.go -f usermanage/api/etc/user-api.yaml
Starting server at 0.0.0.0:8888...
api
测试 得到如下数据则说明 服务运行正常
➜ ~ curl http://localhost:8888/users/login -X POST -d '{"email": "ningxi@ningxi.com","password": "809161"}' --header "Content-Type: application/json"
{"code":1,"message":"","result":{"id":1,"username":"","email":"ningxi@ningxi.com","accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTE0OTU3MjAsImlhdCI6MTYxMTQwOTMyMCwidXNlcklkIjoxfQ.EN9og9owK5eW--qUIJCf0UvMKNaeenVac1lmMQFcHSM","accessExpire":1611495720,"refreshAfter":1611452520}}%
➜ ~
理解服务是怎么跑起来的
老样子还是要理解一下服务的执行流程
user/etc
下的user.yaml
文件。该文件配置了user rpc
服务所需的一些变量,这个与api
服务是一样的。user
下的user.proto
文件。该文件定义了user rpc
服务所提供的接口信息,之后的接口增加同样是在这里处理。然后 调用goctl
重新生成服务。user
下的user.go
文件。该文件是user rpc
服务的入口文件,一切都是从这里开始。user/user
下的user.pb.go
文件。该文件是user.proto
生成rpc
服务文件,里面包含了rpc
中client
和server
的实现。
userclient 文件夹
userclient
下的 user.go
文件是 rpc
服务 中 client
的具体实现代码,该文件实现了 user.pb.go
中的 三个接口
User interface {
Login(ctx context.Context, in *LoginRequest) (*Response, error)
Register(ctx context.Context, in *RegisterRequest) (*Response, error)
Userinfo(ctx context.Context, in *UserinfoRequest) (*Response, error)
}
当 api
服务中的 serviceContext.go
中 初始化 userclient
时,api
服务就具备了调用这三个接口的能力
type ServiceContext struct {
Config config.Config
User userclient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
User: userclient.NewUser(zrpc.MustNewClient(c.User)),
}
}
internal 文件夹
rpc
服务中的内部实现代码都放在了该文件夹下面。
internal/config
下的 config.go
文件。你会发现,该文件的定义和 user.yaml
的定义类似。是的。user.yaml
在 user.go
入口文件在 main
方法里,就被解析成了 config
对象。所以他们的值是一一对应的。
internal/logic
下的 xxxlogic.go
文件,这里是最终业务逻辑实现的地方。
internal/svc
下的 servicecontext.go
文件。该文件保存了 rpc
服务的 config
对象,数据库连接对象 Model。通过 UserServer
对象 传递到 logic
对象中。
internal/sercer
下的 userserver.go
文件。该文件是 user rpc
服务中 server
的 具体实现代码,userserver
保存了 svc.ServiceContext
对象,并且实现了 三个接口
func (s *UserServer) Login(ctx context.Context, in *user.LoginRequest) (*user.Response, error) {
l := logic.NewLoginLogic(ctx, s.svcCtx)
return l.Login(in)
}
func (s *UserServer) Register(ctx context.Context, in *user.RegisterRequest) (*user.Response, error) {
l := logic.NewRegisterLogic(ctx, s.svcCtx)
return l.Register(in)
}
func (s *UserServer) Userinfo(ctx context.Context, in *user.UserinfoRequest) (*user.Response, error) {
l := logic.NewUserinfoLogic(ctx, s.svcCtx)
return l.Userinfo(in)
}
当 api
服务通过 userclient
调用 rpc
服务时,userserver
将触发相对应的方法,并最终调用相对应的 logic
方法。
调用过程梳理
以客户端调用 login 接口为例。
client
部分
api
服务 在启动时,初始化了 userclient
对象,于是 api
服务具备了调用 user rpc
服务的能力
type ServiceContext struct {
Config config.Config
User userclient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
User: userclient.NewUser(zrpc.MustNewClient(c.User)),
}
}
server
部分
user.go
入口文件 通过 yaml
配置文件,实例化 config
对象。
var configFile = flag.String("f", "etc/user.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
}
实例化 ServiceContext
对象
ctx := svc.NewServiceContext(c)
实例化 UserServer
对象
srv := server.NewUserServer(ctx)
实例化 rpc
服务
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
user.RegisterUserServer(grpcServer, srv)
})
user.RegisterUserServer(grpcServer, srv)
里做了类似 api
服务中路由实现的功能。
启动 rpc
服务
s.Start()
当 api
服务通过 client
调用 login
方式时,rpc server
将触发 userserve
中 的 login
方法。
func (s *UserServer) Login(ctx context.Context, in *user.LoginRequest) (*user.Response, error) {
l := logic.NewLoginLogic(ctx, s.svcCtx)
return l.Login(in)
}
login
方法中调用 logic
中 的 login
方法
处理完数据后,接口逐层响应回去,最终完成客户端接口的调用。
本片内容相对较长,其中关于 JwtToken
、 rpc
中 client
和 server
等相关知识,大家可以自行查阅资料能更好理解教程中的内容。