go hertz框架和kitex的入门笔记

913 阅读10分钟

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.gohandler目录即可,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,以上就是入门篇的全部

参考

gorm

kitex

hertz