EASY-NOTE项目笔记(1)|青训营笔记

151 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第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。