这是我参与「第五届青训营 」伴学笔记创作活动的第1天。Go语言相关框架有web框架hertz,RPC框架kitex,ORM框架gorm。easy-note项目中初步应用了这三个框架的内容,实现了用户的登录、注册与笔记内容的增删改查。本文记录easy-note的源码阅读笔记并兼以学习三个框架。
架构
http
┌────────────────────────┐
┌─────────────────────────┤ ├───────────────────────────────┐
│ │ demoapi │ │
│ ┌──────────────────► │◄──────────────────────┐ │
│ │ └───────────▲────────────┘ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ resolve │ │
│ │ │ │ │
req resp │ resp req
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ ┌──────────▼─────────┐ │ │
│ │ │ │ │ │
│ │ ┌───────────► Etcd ◄─────────────────┐ │ │
│ │ │ │ │ │ │ │
│ │ │ └────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ register register │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
┌▼──────┴───────┴───┐ ┌──┴────────┴───────▼─┐
│ │───────────────── req ────────────────────►│ │
│ demonote │ │ demouser │
│ │◄──────────────── resp ────────────────────│ │
└───────────────────┘ └─────────────────────┘
thrift protobuf
demoapi模块基于hertz框架建立HTTP服务器,接收前端请求。 demonote和demouser模块分别基于gorm框架建立微服务笔记增删改查与用户登录注册模块。 这三个模块通过kitex框架进行RPC通信,利用etcd实现服务发现和服务注册。其中demoapi和demouser之间通过protobuf协议通信,demoapi和demouser之间通过thrift协议通信。
demoapi模块介绍
demoapi实质为两个部分,其一为HTTP服务器,其二为kitex框架RPC通信中的客户端。因此本文将分别介绍这两个部分。
HTTP服务器
demoapi基于hertz建立了http服务器,回答http请求。
func main() {
Init()
r := server.New(
server.WithHostPorts("127.0.0.1:8080"),
server.WithHandleMethodNotAllowed(true),
)
// https://www.cnblogs.com/qinjunlin/p/13936971.html
authMiddleware, _ := jwt.New(&jwt.HertzJWTMiddleware{
Key: []byte(constants.SecretKey), // 设置签名密钥
Timeout: time.Hour,
MaxRefresh: time.Hour,
// 用于设置登录成功后向token中添加自定义负载信息的函数
PayloadFunc: func(data interface{}) jwt.MapClaims {
if v, ok := data.(int64); ok {
return jwt.MapClaims{
constants.IdentityKey: v,
}
}
return jwt.MapClaims{}
},
// 用于设置jwt校验流程发生错误时响应所包含的错误信息
HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
switch e.(type) {
case errno.ErrNo:
return e.(errno.ErrNo).ErrMsg
default:
return e.Error()
}
},
// 设置登录的响应函数
LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
c.JSON(consts.StatusOK, map[string]interface{}{
"code": errno.SuccessCode,
"token": token,
"expire": expire.Format(time.RFC3339),
})
},
// 设置jwt验证流程失败的响应函数
Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
c.JSON(code, map[string]interface{}{
"code": errno.AuthorizationFailedErrCode,
"message": message,
})
},
// 设置登录时认证用户信息的函数
Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
var loginVar handlers.UserParam
if err := c.Bind(&loginVar); err != nil {
return "", jwt.ErrMissingLoginValues
}
if len(loginVar.UserName) == 0 || len(loginVar.PassWord) == 0 {
return "", jwt.ErrMissingLoginValues
}
return rpc.CheckUser(context.Background(), &userdemo.CheckUserRequest{UserName: loginVar.UserName, Password: loginVar.PassWord})
},
TokenLookup: "header: Authorization, query: token, cookie: jwt",
TokenHeadName: "Bearer",
TimeFunc: time.Now,
})
r.Use(recovery.Recovery(recovery.WithRecoveryHandler(
func(ctx context.Context, c *app.RequestContext, err interface{}, stack []byte) {
hlog.SystemLogger().CtxErrorf(ctx, "[Recovery] err=%v\nstack=%s", err, stack)
c.JSON(consts.StatusInternalServerError, map[string]interface{}{
"code": errno.ServiceErrCode,
"message": fmt.Sprintf("[Recovery] err=%v\nstack=%s", err, stack),
})
})))
v1 := r.Group("/v1")
user1 := v1.Group("/user")
user1.POST("/login", authMiddleware.LoginHandler)
user1.POST("/register", handlers.Register)
note1 := v1.Group("/note")
note1.Use(authMiddleware.MiddlewareFunc())
note1.GET("/query", handlers.QueryNote)
note1.POST("", handlers.CreateNote)
note1.PUT("/:note_id", handlers.UpdateNote)
note1.DELETE("/:note_id", handlers.DeleteNote)
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "no route")
})
r.NoMethod(func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "no method")
})
r.Spin()
}
具体的处理函数在handlers包中。其中值得注意的一点是jwt鉴权中间件和recovery中间件。前者为经典的分布式微服务系统的鉴权方法,后者提供故障恢复方法。
RPC客户端
func InitRPC() {
initUserRpc()
initNoteRpc()
}
func initUserRpc() {
r, err := etcd.NewEtcdResolver([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
c, err := userservice.NewClient(
constants.UserServiceName,
client.WithMiddleware(middleware.CommonMiddleware),
client.WithInstanceMW(middleware.ClientMiddleware),
client.WithMuxConnection(1), // mux
client.WithRPCTimeout(3*time.Second), // rpc timeout
client.WithConnectTimeout(50*time.Millisecond), // conn timeout
client.WithFailureRetry(retry.NewFailurePolicy()), // retry
client.WithSuite(trace.NewDefaultClientSuite()), // tracer
client.WithResolver(r), // resolver
)
if err != nil {
panic(err)
}
userClient = c
}
在demoapi,创建RPC客户端,同时使用etcd.NewEtcdResolver指定服务器地址,并在选项中使用WithResolver应用该ETCD服务器进行服务发现,指定服务端服务名。
WithMiddleware:指定中间件,在Service 熔断和超时中间件之后执行;WithInstanceMW:指定中间件,在服务发现和负载均衡之后执行;WithMuxConnection:设置连接多路复用;WithRPCTimeout:设置RPC超时;WithConnectTimeout:设置连接超时;WithSuite:指定特定配置,在这里使用默认特定配置
demouser模块
主要处理用户的注册和登录功能
func main() {
r, err := etcd.NewEtcdRegistry([]string{constants.EtcdAddress})
if err != nil {
panic(err)
}
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8889")
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)
}
}
模块使用server.NewServer()函数将自身注册到etcd服务器中,等待服务发现。
WithServerBasicInfo:指定服务器的基本属性,制定服务名WithMiddleware:指定中间件,在Service 熔断和超时中间件之后执行;WithInstanceMW:指定中间件,在服务发现和负载均衡之后执行;WithServiceAddr:指定模块服务器的具体TCP地址;WithMuxTransport:指定多路复用相关规则;WithLimit:指定流量限制阈值;WithSuite:指定特定配置,在这里使用默认特定配置WithBoundHandler:自定义IO Bound,在这里设置了一个用于CPU限流的IO Bound。