目录导航:
- 环境配置
- demouser的实现
- 运行user RPC Server
- 参考资料
# 1.项目配置
技术路线:
- 接口描述语言:thrift [thrift官方文档](https://thrift.apache.org/docs/idl)
- RPC框架:kitex [kitex说明文档](https://www.cloudwego.io/zh/docs/kitex/getting-started/)
- go web框架:gin [gin官方文档](https://github.com/gin-gonic/gin#quick-start)
环境配置:
- go 1.18:[go下载地址](https://studygolang.com/dl) 注意与kitex的版本兼容性
- Goland: [Goland下载地址](https://www.jetbrains.com/zh-cn/go/) IDE方便编译运行代码
- docker: [安装教程](https://www.runoob.com/docker/windows-docker-install.html) 项目需要etcd服务
- git: [安装教程](https://www.liaoxuefeng.com/wiki/896043488029600/896067074338496) 方便代码管理
- wsl2: [安装教程](https://learn.microsoft.com/zh-cn/windows/wsl/install) window需要,kitex只能在linux内核下使用
网络环境:
- 参考了两篇文章[windows抖音项目的环境配置](https://juejin.cn/post/7197033247283445817)、
[抖音项目Windows下调试环境搭建](https://juejin.cn/post/7096857967747661831)。
- 如果是Linux系统,本地IP查看指令为:`ip addr show`。
# 2.user微服务的实现
## user.thirft
新建项目文件夹douyin,进入目录新建idl目录用来存放整个项目需要的idl文件,创建文本user.thirft:
namespace go user
struct BaseResp {
1:i64 status_code
2:string status_message
}
struct douyin_user_register_request {
1:required string username
2:required string password
}
struct douyin_user_register_response {
1:BaseResp base_resp
2:required i64 user_id
3:required string token
}
struct douyin_user_login_request {
1:required string username
2:required string password
}
struct douyin_user_login_response {
1:BaseResp base_resp
2:required i64 user_id
3:required string token
}
struct douyin_user_request {
1:required i64 user_id
2:required string token
}
struct douyin_user_response {
1:BaseResp base_resp
2:required User user
}
struct User {
1:required i64 id
2:required string name
}
service UserService {
douyin_user_register_response CreateUser(1:douyin_user_register_request req)
douyin_user_login_response CheckUser(1:douyin_user_login_request req)
douyin_user_response QueryCurUser(1:douyin_user_request req)
}
## dao层操作
代码存放在douyin/cmd/user/dal/db/。db文件夹存放数据库初始化文件init.go以及对user进行底层增删改查操作的user.go。user.go如下:
package db
import (
“context”
“douyin/pkg/constants”
“gorm.io/gorm”
)
type User struct {
ID int64 gorm:"column:id;primaryKey;not null"
Username string gorm:"column:u_name;unique;type:varchar(30);not null"
Password string gorm:"column:passwd;type:varchar(60);not null"
}
func (u *User) TableName() string {
return constants.UserTableName
}
// MGetUsers multiple get list of user info
func MGetUsers(ctx context.Context, userIDs []int64) ([]*User, error) {
res := make([]*User, 0)
if len(userIDs) == 0 {
return res, nil
}
if err := DB.WithContext(ctx).Where(“id in ?”, userIDs).Find(&res).Error; err != nil {
return nil, err
}
return res, nil
}
// CreateUser create user info
func CreateUser(ctx context.Context, uname, password string) ([]*User, error) {
users := []*User{{
Username: uname,
Password: password,
}}
res := make([]*User, 0)
err := DB.WithContext(ctx).Create(users).Error
if err != nil {
return nil, err
}
err = DB.WithContext(ctx).Where(“u_name = ?”, uname).Limit(1).Find(&res).Error
if err != nil {
return nil, err
}
return res, nil
}
// QueryUser query list of user info
func QueryUser(ctx context.Context, userName string) ([]*User, error) {
res := make([]*User, 0)
if err := DB.WithContext(ctx).Where(“u_name = ?”, userName).Find(&res).Error; err != nil {
return nil, err
}
return res, nil
}
## kitex生成部分代码
在douyin/cmd/user文件夹下进行kitex代码的生成,kitex只能够在Linux系统下使用,在window需要配合wsl进行生成代码。命令如下:
`kitex -module douyin -service userservise ../../idl/user.thrift`
生成的go.mod文件以及kitex_gen文件夹移至douyin文件夹,方便后续添加其他服务。在handler.go获取参数和返回数据对象,douyin/cmd/user/service层调用dao层的方法返回handler.go对应的接口方法需要的数据对象。
## service层
service层调用dao层的方法返回handler.go对应的接口方法需要的数据对象,各个文件如下:
check_user.go
package service
import (
“context”
“crypto/md5”
“fmt”
“io”
“douyin/pkg/errno”
“douyin/kitex_gen/user”
“douyin/cmd/user/dal/db”
)
type CheckUserService struct {
ctx context.Context
}
// NewCheckUserService new CheckUserService
func NewCheckUserService(ctx context.Context) *CheckUserService {
return &CheckUserService{
ctx: ctx,
}
}
// CheckUser check user info
func (s *CheckUserService) CheckUser(req *user.DouyinUserLoginRequest) (int64, error) {
h := md5.New()
if _, err := io.WriteString(h, req.Password); err != nil {
return 0, err
}
passWord := fmt.Sprintf(“%x”, h.Sum(nil))
userName := req.Username
users, err := db.QueryUser(s.ctx, userName)
if err != nil {
return 0, err
}
if len(users) == 0 {
return 0, errno.AuthorizationFailedErr
}
u := users[0]
if u.Password != passWord {
return 0, errno.AuthorizationFailedErr
}
return int64(u.ID), nil
}
create_user.go
package service
import (
“context”
“crypto/md5”
“fmt”
“io”
"douyin/cmd/user/dal/db"
"douyin/kitex_gen/user"
"douyin/pkg/errno"
)
type CreateUserService struct {
ctx context.Context
}
// NewCreateUserService new CreateUserService
func NewCreateUserService(ctx context.Context) *CreateUserService {
return &CreateUserService{ctx: ctx}
}
// CreateUser create user info.
func (s *CreateUserService) CreateUser(req *user.DouyinUserRegisterRequest) (int64, error) {
users, err := db.QueryUser(s.ctx, req.Username)
if err != nil {
return -1, err
}
if len(users) != 0 {
return -1, errno.UserAlreadyExistErr
}
h := md5.New()
if _, err = io.WriteString(h, req.Password); err != nil {
return -1, err
}
passWord := fmt.Sprintf("%x", h.Sum(nil))
res, err := db.CreateUser(s.ctx, req.Username, passWord)
if err != nil {
return -1, err
}
u := res[0]
return u.ID, nil
}
query_user.go
package service
import (
“context”
"douyin/cmd/user/dal/db"
"douyin/cmd/user/pack"
"douyin/kitex_gen/user"
)
type QueryCurUserService struct {
ctx context.Context
}
// NewQueryCurUserService new MGetUserService
func NewQueryCurUserService(ctx context.Context) *QueryCurUserService {
return &QueryCurUserService{ctx: ctx}
}
// MGetUser multiple get list of user info
func (s *QueryCurUserService) MGetUser(req *user.DouyinUserRequest) ([]*user.User, error) {
userIDs := make([]int64, 0)
userIDs[0] = req.UserId
modelUsers, err := db.MGetUsers(s.ctx, userIDs)
if err != nil {
return nil, err
}
return pack.Users(modelUsers), nil
}
## handler.go修改
修改douyin/cmd/user/handler.go就可以了,handler.go如下:
package main
import (
“context”
“douyin/cmd/user/pack”
“douyin/cmd/user/service”
“douyin/kitex_gen/user”
“douyin/pkg/constants”
“douyin/pkg/errno”
“douyin/pkg/middleware”
)
// UserServiceImpl implements the last service interface defined in the IDL.
type UserServiceImpl struct{}
// CreateUser implements the UserServiceImpl interface.
func (s *UserServiceImpl) CreateUser(ctx context.Context, req *user.DouyinUserRegisterRequest) (resp *user.DouyinUserRegisterResponse, err error) {
// TODO: Your code here…
resp = new(user.DouyinUserRegisterResponse)
if len(req.Username) == 0 || len(req.Password) == 0 {
resp.BaseResp = pack.BuildBaseResp(errno.ParamErr)
resp.UserId = constants.EmptyUserId
resp.Token = constants.EmptyToken
return resp, nil
}
uid, err := service.NewCreateUserService(ctx).CreateUser(req)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
resp.UserId = constants.EmptyUserId
resp.Token = constants.EmptyToken
return resp, nil
}
// token, _ := global.Jwt.CreateToken(userID, global.JWTSetting.AppKey, global.JWTSetting.AppSecret)
token, err := middleware.CreateToken(uid)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
resp.UserId = constants.EmptyUserId
resp.Token = constants.EmptyToken
return resp, nil
}
resp.BaseResp = pack.BuildBaseResp(errno.Success)
resp.UserId = uid
resp.Token = token
return resp, nil
}
// CheckUser implements the UserServiceImpl interface.
func (s *UserServiceImpl) CheckUser(ctx context.Context, req *user.DouyinUserLoginRequest) (resp *user.DouyinUserLoginResponse, err error) {
// TODO: Your code here…
resp = new(user.DouyinUserLoginResponse)
if len(req.Username) == 0 || len(req.Password) == 0 {
resp.BaseResp = pack.BuildBaseResp(errno.ParamErr)
resp.UserId = constants.EmptyUserId
resp.Token = constants.EmptyToken
return resp, nil
}
uid, err := service.NewCheckUserService(ctx).CheckUser(req)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
resp.UserId = constants.EmptyUserId
resp.Token = constants.EmptyToken
return resp, nil
}
// token, _ := global.Jwt.CreateToken(userID, global.JWTSetting.AppKey, global.JWTSetting.AppSecret)
token, err := middleware.CreateToken(uid)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
resp.UserId = constants.EmptyUserId
resp.Token = constants.EmptyToken
return resp, nil
}
resp.BaseResp = pack.BuildBaseResp(errno.Success)
resp.UserId = uid
resp.Token = token
return resp, nil
}
// QueryCurUser implements the UserServiceImpl interface.
func (s *UserServiceImpl) QueryCurUser(ctx context.Context, req *user.DouyinUserRequest) (resp *user.DouyinUserResponse, err error) {
// TODO: Your code here…
resp = new(user.DouyinUserResponse)
if req.UserId == 0 {
resp.BaseResp = pack.BuildBaseResp(errno.ParamErr)
resp.User = nil
return resp, nil
}
users, err := service.NewQueryCurUserService(ctx).MGetUser(req)
if err != nil {
resp.BaseResp = pack.BuildBaseResp(err)
resp.User = nil
return resp, nil
}
resp.BaseResp = pack.BuildBaseResp(errno.Success)
resp.User = users[0]
return resp, nil
}
## 服务注册
将服务注册到etcd上,修改douyin/cmd/user/main.go:
package main
import (
“douyin/cmd/user/dal”
user “douyin/kitex_gen/user/userservice”
“douyin/pkg/bound”
“douyin/pkg/constants”
“douyin/pkg/middleware”
tracer2 “douyin/pkg/tracer”
“github.com/cloudwego/kitex/pkg/klog”
“github.com/cloudwego/kitex/pkg/limit”
“github.com/cloudwego/kitex/pkg/rpcinfo”
“github.com/cloudwego/kitex/server”
etcd “github.com/kitex-contrib/registry-etcd”
trace “github.com/kitex-contrib/tracer-opentracing”
“net”
)
func Init() {
tracer2.InitJaeger(constants.UserServiceName)
dal.Init()
}
func main() {
r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
addr, err := net.ResolveTCPAddr(“tcp”, constants.UserServiceAddr)
if err != nil {
panic(err)
}
Init()
svr := user.NewServer(new(UserServiceImpl),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: constants.UserServiceName}), // server name
server.WithMiddleware(middleware.CommonMiddleware), // middleware
server.WithMiddleware(middleware.ServerMiddleware),
server.WithServiceAddr(addr), // address
server.WithLimit(&limit.Option{MaxConnections: 1000, MaxQPS: 100}), // limit
server.WithMuxTransport(), // Multiplex
server.WithSuite(trace.NewDefaultServerSuite()), // tracer
server.WithBoundHandler(bound.NewCpuLimitHandler()), // BoundHandler
server.WithRegistry(r), // registry
)
err = svr.Run()
if err != nil {
klog.Fatal(err)
}
}
# 3. 运行user RPC Server
终端根据docker-compose.yml文件拉取创建docker容器并启动:
sudo docker-compose up
另起一个终端运行user RPC Server:
cd ./cmd/user
sh build.sh
sh output/bootstrap.sh
终端提示server listen at addr=127.0.0.1:8889,已经开启了微服务,等待客户端链接。
之后再完成api层使用gin框架,下一章更新
项目地址:https://github.com/LaiYuShuang/douyin
# 4.资料
参考了以下资料:
- [Web框架:Gin、FastHttp、Hertz ](https://zhuanlan.zhihu.com/p/588670733)
- [抖音方案说明](https://bytedance.feishu.cn/docs/doccnKrCsU5Iac6eftnFBdsXTof#)
- [极简抖音APP的使用](https://bytedance.feishu.cn/docs/doccnM9KkBAdyDhg8qaeGlIz7S7)
- [极简版抖音](https://www.apifox.cn/apidoc/shared-09d88f32-0b6c-4157-9d07-a36d32d7a75c/api-50707521)
- [[kitex + gorm-gen + hertz] 快速写出一个基于go的微服务](https://blog.csdn.net/qq_41080854/article/details/128804495)
- [kitex说明文档](https://www.cloudwego.io/zh/docs/kitex/getting-started/)