go hertz框架和kitex的入门笔记
hertz
hertz是字节跳动使用的go的web框架,跟业界开源的gin和其他框架一样,提供能快速搭建的web框架去方便开发 人员使用,可以去开发API层接口,hertz底层使用了netpoll(应该是字节重新实现的一套net底层框架,与go net差不多,但是性能应该比go net要好),具体参考链接可以点击此处
kitex
kitex也是字节跳动使用的go的rpc框架,类似protobuf\trpc\tarf\thrift,可以用于内部应用之间的高效通讯,根据指定好的idl文件(kitex可以使用thrift也可以使用proto文件去定义idl),生成对应的server代码和stub代码,提供给服务端和客户端的使用,由于底层的编码逻辑比http所使用的json要高效,所以内部应用比较适合使用rpc协议进行通讯,具体参考链接可以点击此处
overview
本篇笔记主要是实现一个登录功能的demo,里面集成hertz\kitex\gorm的框架去完成,其他更高级的功能可以在上文链接中看使用手册自己实现具体逻辑。具体代码逻辑
创建hertz项目
1.安装hertz命令行,用于初始化项目
go install github.com/cloudwego/hertz/cmd/hz@latest
hz --version
如果hz是command not found
,可以去配置一下GO_PATH的目录,PATH需要有GO_PATH
export PATH=$GOPATH/bin:$PATH
或者 /etc/profile里添加 PATH=$GOPATH/bin:$PATH
,然后source /etc/profile
2.创建目录
mkdir hertz_demo
cd hertz_demo
hz new -module hertz_demo
go mod tidy
tree ./
产出如下文件
[root@hecs-74066 hertz_demo]# tree ./
./
├── biz
│ ├── handler
│ │ └── ping.go # handler层,实现具体接口逻辑
│ └── router
│ └── register.go # 路由注册层,这个适用于使用thrift生成的hz项目
├── go.mod
├── go.sum
├── main.go
├── router_gen.go
└── router.go # 注册路由的地方,将方法注册到对应的url中
3 directories, 7 files
我们启动一下项目
go build -o api
./api
2023/01/15 14:33:55.520003 engine.go:617: [Debug] HERTZ: Method=GET absolutePath=/ping --> handlerName=hertz_demo/biz/handler.Ping (num=2 handlers)
2023/01/15 14:33:55.520344 engine.go:389: [Info] HERTZ: Using network library=netpoll
2023/01/15 14:33:55.520442 transport.go:110: [Info] HERTZ: HTTP server listening on address=[::]:8888
curl http://127.0.0.1:8888/ping
{"message":"pong"}
后续逻辑主要实现在main.go
和handler目录
即可,ping.go
是已经实现好的逻辑,就是一个/ping
接口
// ...
func customizedRegister(r *server.Hertz) {
r.GET("/ping", handler.Ping)
// your code ...
}
// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
c.JSON(consts.StatusOK, utils.H{
"message": "pong",
})
}
// ...
我们按照同样的逻辑去实现一个登录和登出的逻辑,并且注册在hertz的路由上
3.具体逻辑 在handler里创建一个user.go,实现用户的登录登出
package handler
import (
"context"
"sync"
"github.com/cloudwego/hertz/pkg/app"
)
type UserImpl struct {
user map[string]string // key:username, value: password
online map[string]struct{} // key:username
mu sync.Mutex
}
type data map[string]interface{}
func NewUserImpl() *UserImpl {
return &UserImpl{user: map[string]string{
"test": "123456",
}, online: map[string]struct{}{}, mu: sync.Mutex{}}
}
// GetUsers: Get请求,获取内存中的user信息
func (u *UserImpl) GetUsers(ctx context.Context, c *app.RequestContext) {
c.JSON(200, data{
"msg": "success",
"data": u.user[c.DefaultQuery("username", "test")],
"code": 1,
})
}
// Login: Post请求,header为application/x- www-form-urlencoded,如果为multipart/form-data需要使用另一种方式获取post的内容:c.FormValue
func (u *UserImpl) Login(ctx context.Context, c *app.RequestContext) {
username, password := c.PostForm("username"), c.PostForm("password")
if len(username) == 0 || len(password) == 0 {
c.JSON(200, data{
"msg": "username or password can't be empty, login failed",
"data": username,
"code": -1,
})
return
}
if pass, ok := u.user[username]; ok {
if pass == password {
u.mu.Lock()
defer u.mu.Unlock()
u.online[username] = struct{}{}
c.JSON(200, data{
"msg": "success",
"data": username,
"code": 1,
})
return
}
c.JSON(200, data{
"msg": "passwrod is not equal",
"data": username,
"code": -1,
})
return
}
c.JSON(200, data{
"msg": "username is not found",
"data": username,
"code": -1,
})
}
// LogOut: Post请求, 同上
func (u *UserImpl) LogOut(ctx context.Context, c *app.RequestContext) {
username := c.PostForm("username")
if len(username) == 0 {
c.JSON(200, data{
"msg": "username is empty",
"data": username,
"code": -1,
})
return
}
u.mu.Lock()
defer u.mu.Unlock()
if _, ok := u.online[username]; ok {
delete(u.online, username)
c.JSON(200, data{
"msg": "success",
"data": username,
"code": 1,
})
return
}
c.JSON(200, data{
"msg": "username is not found",
"data": username,
"code": -1,
})
}
router.go
package main
import (
handler "hertz_demo/biz/handler"
"github.com/cloudwego/hertz/pkg/app/server"
)
// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
r.GET("/ping", handler.Ping)
ui := handler.NewUserImpl()
// 区分路由组,以v1开头
rg := r.Group("/v1")
rg.POST("/login", ui.Login)
rg.POST("/logout", ui.LogOut)
rg.GET("/users", ui.GetUsers)
}
然后执行go build
再运行一下
go build -o api
./api
2023/01/15 15:04:36.973849 engine.go:617: [Debug] HERTZ: Method=GET absolutePath=/ping --> handlerName=hertz_demo/biz/handler.Ping (num=2 handlers)
2023/01/15 15:04:36.973888 engine.go:617: [Debug] HERTZ: Method=POST absolutePath=/v1/login --> handlerName=hertz_demo/biz/handler.(*UserImpl).Login-fm (num=2 handlers)
2023/01/15 15:04:36.973899 engine.go:617: [Debug] HERTZ: Method=POST absolutePath=/v1/logout --> handlerName=hertz_demo/biz/handler.(*UserImpl).LogOut-fm (num=2 handlers)
2023/01/15 15:04:36.973908 engine.go:617: [Debug] HERTZ: Method=GET absolutePath=/v1/users --> handlerName=hertz_demo/biz/handler.(*UserImpl).GetUsers-fm (num=2 handlers)
2023/01/15 15:04:36.974305 engine.go:389: [Info] HERTZ: Using network library=netpoll
2023/01/15 15:04:36.974402 transport.go:110: [Info] HERTZ: HTTP server listening on address=[::]:8888
可以看到服务已经正常将我们定义的path给注册到路由上了,接下来我们来测试一下这几个接口是否能正常工作
curl -X POST -d 'username=test&password=123456' http://127.0.0.1:8888/v1/login
{"code":1,"data":"test","msg":"success"}
curl -X POST -d 'username=test' http://127.0.0.1:8888/v1/logout
{"code":1,"data":"test","msg":"success"}
curl http://127.0.0.1:8888/v1/users
{"code":1,"data":"123456","msg":"success"}
在这里hertz就能正常执行我们的业务逻辑了
在正常的微服务体系中,一般api层都会调用下游服务进行一些核心逻辑的执行,在demo中,我们后续把logint和logout的逻辑转移到下游服务中,用kitex-client执行调用,因此我们需要对代码进行一定的更改
创建kitex-server项目
安装kitex命令
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/thriftgo@latest
kitex --version
thriftgo --version // 出现cmd找不到的情况,如同hertz的处理办法
创建kitex项目需要依赖idl文件的生成,一份idl文件可以定义整个服务的对外暴露的接口,可以让后台服务间的调用更加明确,因此我们先定义一份thrift文件,根据thrift文件让kitex工具帮助我们生成框架代码
cd .. #回到hertz_demo的前一个目录
vim biz.thrift
namespace go biz
struct BaseResponse {
1: i32 code; // 1成功,-1失败
2: string msg;
}
struct LoginRequest {
1: required string username;
2: required string password;
}
struct LoginResponse {
1: BaseResponse base;
2: string userToken; // token使用username代替
}
struct LogoutRequest {
1: required string userToken; // token使用username代替
}
struct LogOutResponse {
1: BaseResponse base;
}
struct User {
1: string username;
2: string password;
3: string email;
}
service UserService {
LoginResponse Login(1: LoginRequest request)
LogOutResponse LogOut(1: LogoutRequest request)
list<User> GetUsers()
}
执行以下命令创建kitex-server项目
mkdir kitex_demo
cd kitex_demo
kitex -module kitex_demo -service kitex_demo ../biz.thrift
go mod tidy
项目结构如下
[root@hecs-74066 kitex_demo]# tree ./
./
├── build.sh #编译脚本
├── go.mod
├── handler.go # 目前阶段实现的逻辑为handler.go
├── kitex_gen # kitex根据thrift生成的一堆struct的定义和调用框架层的细节,暂时可以不关注
│ └── biz
│ ├── biz.go
│ ├── k-biz.go
│ ├── k-consts.go
│ └── userservice
│ ├── client.go
│ ├── invoker.go
│ ├── server.go
│ └── userservice.go
├── kitex.yaml
├── main.go # server启动的main.go
└── script
└── bootstrap.sh
4 directories, 13 files
启动一下项目
go mod edit -replace github.com/apache/thrift=github.com/apache/thrift@v0.13.0 #防止编译报错,如果编译没有thrift的依赖报错,不需要添加这个replace到go.mod
sh build.sh
./output/bin/kitex_demo
2023/01/15 15:55:55.415010 server.go:81: [Info] KITEX: server listen at addr=[::]:8888
我们主要在handler.go中实现业务逻辑,实现我们真正后端的login和logout逻辑(copy一下即可)
handler.go
package main
import (
"context"
biz "kitex_demo/kitex_gen/biz"
"sync"
)
// UserServiceImpl implements the last service interface defined in the IDL.
type UserServiceImpl struct {
user map[string]string
online map[string]struct{}
mu sync.Mutex
}
func NewUserServiceImpl() *UserServiceImpl {
return &UserServiceImpl{user: map[string]string{
"test": "123456",
"ok": "test",
}, online: map[string]struct{}{}, mu: sync.Mutex{},
}
}
// Login implements the UserServiceImpl interface.
func (s *UserServiceImpl) Login(ctx context.Context, request *biz.LoginRequest) (resp *biz.LoginResponse, err error) {
// TODO: Your code here...
resp = &biz.LoginResponse{}
if request.GetUsername() == "" || request.GetPassword() == "" {
resp.Base = &biz.BaseResponse{Code: -1, Msg: "username or password is empty"}
return
}
if pass, ok := s.user[request.GetUsername()]; ok {
if pass == request.GetPassword() {
s.mu.Lock()
defer s.mu.Unlock()
s.online[request.GetUsername()] = struct{}{}
resp.Base = &biz.BaseResponse{Code: 1, Msg: "success"}
resp.UserToken = request.GetUsername()
return
}
resp.Base = &biz.BaseResponse{Code: -1, Msg: "password is not equal to user data"}
return
}
resp.Base = &biz.BaseResponse{Code: -1, Msg: "user not found"}
return
}
// LogOut implements the UserServiceImpl interface.
func (s *UserServiceImpl) LogOut(ctx context.Context, request *biz.LogoutRequest) (resp *biz.LogOutResponse, err error) {
// TODO: Your code here...
resp = &biz.LogOutResponse{}
if request.GetUserToken() == "" {
resp.Base = &biz.BaseResponse{Code: -1, Msg: "username is empty"}
return
}
if _, ok := s.online[request.GetUserToken()]; ok {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.online, request.GetUserToken())
resp.Base = &biz.BaseResponse{Code: 1, Msg: "success"}
return
}
resp.Base = &biz.BaseResponse{Code: -1, Msg: "user is not online"}
return
}
// GetUsers implements the UserServiceImpl interface.
func (s *UserServiceImpl) GetUsers(ctx context.Context) (resp []*biz.User, err error) {
// TODO: Your code here...
resp = []*biz.User{}
for k, v := range s.user {
resp = append(resp, &biz.User{Username: k, Password: v, Email: k + "@qq.com"})
}
return
}
main.go
package main
import (
biz "kitex_demo/kitex_gen/biz/userservice"
"log"
"net"
"github.com/cloudwego/kitex/server"
)
func main() {
addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
svr := biz.NewServer(NewUserServiceImpl(), server.WithServiceAddr(addr)) // 指定server的ip:port
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
启动服务
go mod tidy
sh build.sh
./output/bin/kitex_demo
2023/01/15 21:15:56.089091 server.go:81: [Info] KITEX: server listen at addr=127.0.0.1:9999
服务监听已经改为localhost:9999
,接下来需要在hertz_demo中修改原有代码,调用下游的kitex服务进行逻辑处理
生成kitex的client端代码
kitex -module hertz_demo ../biz.thrift #自己区分相对路径,当前路径在hertz_demo下
user.go
package handler
import (
"context"
"fmt"
"hertz_demo/kitex_gen/biz"
"hertz_demo/kitex_gen/biz/userservice"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/kitex/client"
)
type UserImpl struct {
client userservice.Client
}
type data map[string]interface{}
func NewUserImpl() *UserImpl {
c, err := userservice.NewClient("kitex_demo", client.WithHostPorts("127.0.0.1:9999"))
if err != nil {
panic(fmt.Sprintf("create user client error: %v", err))
}
return &UserImpl{client: c} // 指定下游的ip,高级用法可以使用resolver去调用服务注册中心
}
// GetUsers: Get请求,获取内存中的user信息
func (u *UserImpl) GetUsers(ctx context.Context, c *app.RequestContext) {
r, err := u.client.GetUsers(ctx)
if err != nil {
c.JSON(200, data{
"msg": err.Error(),
"data": r,
"code": -1,
})
return
}
c.JSON(200, data{
"msg": "success",
"data": r,
"code": 1,
})
}
// Login: Post请求
func (u *UserImpl) Login(ctx context.Context, c *app.RequestContext) {
username, password := c.PostForm("username"), c.PostForm("password")
lr, err := u.client.Login(ctx, &biz.LoginRequest{Username: username, Password: password})
if err != nil {
c.JSON(200, data{
"msg": err.Error(),
"data": lr.GetUserToken(),
"code": -1,
})
return
}
if lr.GetBase().GetCode() == -1 {
c.JSON(200, data{
"msg": lr.GetBase().GetMsg(),
"data": lr,
"code": -1,
})
return
}
c.JSON(200, data{
"msg": lr.GetBase().GetMsg(),
"data": lr.GetUserToken(),
"code": lr.GetBase().GetCode(),
})
}
// LogOut: Post请求, 同上
func (u *UserImpl) LogOut(ctx context.Context, c *app.RequestContext) {
username := c.PostForm("username")
lor, err := u.client.LogOut(ctx, &biz.LogoutRequest{UserToken: username})
if err != nil {
c.JSON(200, data{
"msg": err.Error(),
"data": "",
"code": -1,
})
return
}
if lor.GetBase().GetCode() == -1 {
c.JSON(200, data{
"msg": lor.GetBase().GetMsg(),
"data": "",
"code": lor.GetBase().GetCode(),
})
return
}
c.JSON(200, data{
"msg": lor.GetBase().GetMsg(),
"data": "",
"code": lor.GetBase().GetCode(),
})
}
启动hertz_demo服务
go mod edit -replace github.com/apache/thrift=github.com/apache/thrift@v0.13.0
go mod tidy
go build -o api
./api
2023/01/15 21:50:59.894847 engine.go:617: [Debug] HERTZ: Method=GET absolutePath=/ping --> handlerName=hertz_demo/biz/handler.Ping (num=2 handlers)
2023/01/15 21:50:59.894989 engine.go:617: [Debug] HERTZ: Method=POST absolutePath=/v1/login --> handlerName=hertz_demo/biz/handler.(*UserImpl).Login-fm (num=2 handlers)
2023/01/15 21:50:59.895000 engine.go:617: [Debug] HERTZ: Method=POST absolutePath=/v1/logout --> handlerName=hertz_demo/biz/handler.(*UserImpl).LogOut-fm (num=2 handlers)
2023/01/15 21:50:59.895007 engine.go:617: [Debug] HERTZ: Method=GET absolutePath=/v1/users --> handlerName=hertz_demo/biz/handler.(*UserImpl).GetUsers-fm (num=2 handlers)
2023/01/15 21:50:59.895228 engine.go:389: [Info] HERTZ: Using network library=netpoll
2023/01/15 21:50:59.895344 transport.go:110: [Info] HERTZ: HTTP server listening on address=[::]:8888
测试一下接口逻辑
curl -X POST -d 'username=test&password=123456' http://127.0.0.1:8888/v1/login
{"code":1,"data":"test","msg":"success"}
curl -X POST -d 'username=test' http://127.0.0.1:8888/v1/logout
{"code":1,"data":"","msg":"success"}
curl http://127.0.0.1:8888/v1/users
{"code":1,"data":[{"username":"test","password":"123456","email":"test@qq.com"},{"username":"ok","password":"test","email":"ok@qq.com"}],"msg":"success"}
说明hertz_demo可以正常与下游kitex_demo的服务通信了
kitex集成gorm,使用sqlite存储数据
一开始我们的服务原信息只存在于kitex服务的内存中,服务一旦中断,内存信息也就没了,需要集成一下orm的框架去完成数据的落库,本文因为是demo级别就只使用sqlite作为存储,不使用gorm也可以直接使用原生的driver,就是比较原始
在kitex_demo
中安装gorm
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
修改handler.go
package main
import (
"context"
biz "kitex_demo/kitex_gen/biz"
"sync"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Username string
Password string
Email string
}
// TableName: 指定user的表名, 具体看gorm的doc
func (*User) TableName() string {
return "user"
}
// UserServiceImpl implements the last service interface defined in the IDL.
type UserServiceImpl struct {
online map[string]struct{}
mu sync.Mutex
db *gorm.DB
}
func NewUserServiceImpl() *UserServiceImpl {
d, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
d.AutoMigrate(&User{}) // create table
d.Create(&User{Username: "test", Password: "123456", Email: "test@qq.com"}) // insert one
d.Create(&User{Username: "btdc", Password: "tcc", Email: "btdc@qq.com"}) // insert one
if err != nil {
panic("failed to connect database")
}
return &UserServiceImpl{online: map[string]struct{}{}, mu: sync.Mutex{}, db: d}
}
// Login implements the UserServiceImpl interface.
func (s *UserServiceImpl) Login(ctx context.Context, request *biz.LoginRequest) (resp *biz.LoginResponse, err error) {
// TODO: Your code here...
resp = &biz.LoginResponse{}
if request.GetUsername() == "" || request.GetPassword() == "" {
resp.Base = &biz.BaseResponse{Code: -1, Msg: "username or password is empty"}
return
}
user := &User{}
s.db.Where("username = ?", request.GetUsername()).First(user)
if user.Password == request.GetPassword() && user.Username == request.GetUsername() {
s.mu.Lock()
defer s.mu.Unlock()
s.online[request.GetUsername()] = struct{}{}
resp.Base = &biz.BaseResponse{Code: 1, Msg: "success"}
resp.UserToken = request.GetUsername()
return
}
resp.Base = &biz.BaseResponse{Code: -1, Msg: "user not found"}
return
}
// LogOut implements the UserServiceImpl interface.
func (s *UserServiceImpl) LogOut(ctx context.Context, request *biz.LogoutRequest) (resp *biz.LogOutResponse, err error) {
// TODO: Your code here...
resp = &biz.LogOutResponse{}
if request.GetUserToken() == "" {
resp.Base = &biz.BaseResponse{Code: -1, Msg: "username is empty"}
return
}
if _, ok := s.online[request.GetUserToken()]; ok {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.online, request.GetUserToken())
resp.Base = &biz.BaseResponse{Code: 1, Msg: "success"}
return
}
resp.Base = &biz.BaseResponse{Code: -1, Msg: "user is not online"}
return
}
// GetUsers implements the UserServiceImpl interface.
func (s *UserServiceImpl) GetUsers(ctx context.Context) (resp []*biz.User, err error) {
// TODO: Your code here...
resp = []*biz.User{}
var users []User
d := s.db.Find(&users)
if d.RowsAffected == 0 {
return
}
for i := int64(0); i < d.RowsAffected; i++ {
resp = append(resp, &biz.User{Username: users[i].Username, Password: users[i].Password, Email: users[i].Email})
}
return
}
sh build.sh
./output/bin/kitex_demo
[root@hecs-74066 kitex_demo]# ./output/bin/kitex_demo
2023/01/15 22:13:53.845117 server.go:81: [Info] KITEX: server listen at addr=127.0.0.1:9999
老规矩测试一下接口
curl -X POST -d 'username=test&password=123456' http://127.0.0.1:8888/v1/login
{"code":1,"data":"test","msg":"success"}
curl -X POST -d 'username=test' http://127.0.0.1:8888/v1/logout
{"code":1,"data":"","msg":"success"}
curl http://127.0.0.1:8888/v1/users
{"code":1,"data":[{"username":"test","password":"123456","email":"test@qq.com"},{"username":"btdc","password":"tcc","email":"btdc@qq.com"}],"msg":"success"}
这样我们就完成了hertz\kitex\gorm的整合demo,以上就是入门篇的全部