今天我们继续完善gRPC服务,在服务中添加数据库操作。
- 操作系统:MacOS 14.2。
- 数据库:MySQL8.0.20。
功能简介
这本篇文章里,我们新建一张用户表,实现简单的用户操作功能。表结构设计如下:
sql
create database if not exists `grpcstudy`;
CREATE TABLE grpcstudy.`user` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`mobile` char(11) NOT NULL COMMENT '手机号',
`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
`modified_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`password` varbinary(60) NOT NULL DEFAULT '' COMMENT '密码hash',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_mobile` (`mobile`) USING BTREE,
UNIQUE KEY `udx_email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
我们将实现如下接口:
- 添加用户
- 根据用户id查询用户信息
- 校验密码
gRPC接口设计
我们在上一章节的基础上进行修改,添加相关接口,修改后的proto文件如下:
proto文件定义
protobuf
syntax= "proto3";
package proto;
option go_package = "git.gqnotes.com/guoqiang/grpcexercises/consuldemo/pb";
service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
rpc GreetManyTimes(GreetRequest) returns (stream GreetResponse) {}
rpc GetUser(GetUserRequest) returns (GetUserResponse) {}
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
rpc CheckPassword(CheckPasswordRequest) returns (CheckPasswordResponse) {}
}
message GreetRequest {
string greeting = 1;
}
message GreetResponse {
string result = 1;
}
message CreateUserRequest {
string username = 1;
string mobile = 2;
string password = 3;
string email = 4;
}
message CreateUserResponse {
int64 id = 1;
}
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
int64 id = 1;
string username = 2;
string mobile = 3;
string email = 4;
}
message CheckPasswordRequest {
string username = 1;
string password = 2;
}
message CheckPasswordResponse {
bool ok = 1;
}
在consuldemo目录下执行相关protoc命令,生成对应的go文件:
bash
protoc -Iproto/ --go_out=. --go_opt=module=git.gqnotes.com/guoqiang/grpcexercises/consuldemo --go-grpc_out=. --go-grpc_opt=module=git.gqnotes.com/guoqiang/grpcexercises/consuldemo proto/*.proto
密码处理
在存储密码时,我们不存储明文密码,而是存储密码的hash值,并且进行加盐处理。密码加盐有如下好处:
- 增加密码破解难度
- 防止撞库攻击
- 提高密码安全性
具体算法上,我们使用go语言的bcrypt库来。bcrypt库生成的密码hash值,包含了加盐信息,所以我们不需要额外字段来存储盐(salt)值。
数据库代码生成工具sqlc介绍
sqlc是一款数据库代码生成工具库,它可以根据sql语句生成相关的数据库操作代码(Go)。更多信息,可以查阅官方文档:docs.sqlc.dev/。
sqlc的安装
如果你和我一样,是mac OS操作系统,可以直接执行brew install sqlc 命令来安装sqlc。其它操作系统,请参考官方文档安装:安装方法。
生成数据库操作代码
我们在consuldemo/db目录下新建如下三个文件::
- schema.sql:数据库表结构
- query.sql:查询语句
- sqlc.yaml:sqlc配置文件
其中表结构我们之前已经介绍过,不再赘述。另外两个文件内容如下:
query.sql
sql
-- name: CreateUser :execresult
INSERT INTO grpcstudy.user (username, mobile, email, password) VALUES (?, ?, ?, ?);
-- name: GetUserById :one
SELECT * FROM grpcstudy.user WHERE id = ? LIMIT 1;
-- name: GetUserByUsername :one
SELECT * FROM grpcstudy.user WHERE username = ? LIMIT 1;
-- name: GetUserByMobile :one
SELECT * FROM grpcstudy.user WHERE mobile = ? LIMIT 1;
-- name: ListUsers :many
SELECT * FROM grpcstudy.user ORDER BY id DESC LIMIT ? OFFSET ?;
sqlc.yaml
yaml
version: "2"
sql:
- engine: "mysql"
queries: "query.sql"
schema: "schema.sql"
gen:
go:
package: "user"
out: "user"
在db目录下执行如下命令,生成相关代码:
bash
sqlc generate
如果一切正常,将会在db/user目录下生成相关数据库操作代码。
实现服务端代码
我们修改服务端main.go,部分新增代码如下:
服务端-新增接口实现
go
// CreateUser 创建用户
func (s *Server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (resp *pb.CreateUserResponse, err error) {
resp = &pb.CreateUserResponse{}
// 获取密码hash值
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
if err != nil {
err = status.Error(codes.Internal, "password hash failed:"+err.Error())
return
}
queries := user.New(conn.GetMySQLConn())
result, err := queries.CreateUser(ctx, user.CreateUserParams{
Username: req.Username,
Mobile: req.Mobile,
Email: req.Email,
Password: passwordHash,
})
if err != nil {
err = status.Error(codes.Internal, "create user failed:"+err.Error())
return
}
// 获取插入的id
id, err := result.LastInsertId()
if err != nil {
err = status.Error(codes.Internal, "create user failed")
return
}
resp.Id = id
return
}
// GetUser 获取用户
func (s *Server) GetUser(ctx context.Context, req *pb.GetUserRequest) (resp *pb.GetUserResponse, err error) {
resp = &pb.GetUserResponse{}
defer func() {
if err1 := recover(); err1 != nil {
s.logger.Fatal("get user failed", zap.Any("err", err1))
err = status.Error(codes.Internal, fmt.Sprintf("panic: %v", err1))
}
if err != nil {
s.logger.Error("get user failed", zap.Error(err))
} else {
s.logger.Info("get user success")
}
}()
queries := user.New(conn.GetMySQLConn())
result, err := queries.GetUserById(ctx, uint32(req.Id))
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
err = status.Error(codes.Internal, "get user failed")
return
}
err = status.Error(codes.NotFound, "user not found")
return
}
resp.Id = int64(result.ID)
resp.Username = result.Username
resp.Mobile = result.Mobile
resp.Email = result.Email
return
}
// CheckPassword 校验密码
func (s *Server) CheckPassword(ctx context.Context, req *pb.CheckPasswordRequest) (resp *pb.CheckPasswordResponse, err error) {
resp = &pb.CheckPasswordResponse{}
defer func() {
if err1 := recover(); err1 != nil {
s.logger.Fatal("check password failed", zap.Any("err", err1))
err = status.Error(codes.Internal, fmt.Sprintf("panic: %v", err1))
}
if err != nil {
s.logger.Error("check password failed", zap.Error(err))
} else {
s.logger.Info("check password success")
}
}()
queries := user.New(conn.GetMySQLConn())
// 查询用户信息
result, err := queries.GetUserByUsername(ctx, req.Username)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
err = status.Error(codes.Internal, "check password failed:"+err.Error())
return
}
err = status.Error(codes.NotFound, "user not found")
return
}
// 校验密码
if err = bcrypt.CompareHashAndPassword(result.Password, []byte(req.Password)); err != nil {
err = status.Error(codes.Unauthenticated, "password incorrect")
return
}
resp.Ok = true
return
}
完整代码,请查阅代码仓库:示例代码。
接下来,我们在仓库根目录(grpcexercises)执行如下命令,来完成服务端代码的编译和运行:
bash
make consuldemo-build && ./consuldemo/bin/server
如果事情ok,将会出现如下界面:

使用evans测试代码
为了方便,我们就不适用go语言编写客户端代码,而是直接使用evans来测试。测试过程如下:
首先,让我们来执行evans -r repl --port 5630来启动evans。
然后,按下图所示操作:

接下来,让我们来分别测试添加用户(CreateUser)和查询用户信息(GetUser)接口。
测试添加用户接口
在evans中,执行如下命令:

如图所示,执行成功,返回的用户id是3。
测试查询用户接口
接下来,让我们测试一下查询用户接口,查询上一步添加成功的用户的信息:

可以看到,查询的结果与我们预期的一致。
结束语
今天我们在grpc服务中添加了数据库操作,并实现了一些简单操作。在现实中,如果业务变复杂以后,一般会有专门的服务来处理用户信息,这个时候,用grpc来实现相关服务,是个不错的选择。
后续,我们将继续完善grpc服务,添加更多功能。
本章示例代码地址:示例代码。
本文由mdnice多平台发布