关于golang:gozero之web框架

1次阅读

共计 7768 个字符,预计需要花费 20 分钟才能阅读完成。

go-zero 是一个集成了各种工程实际的 web 和 rpc 框架,其中 rest 是 web 框架模块,基于 Go 语言原生的 http 包进行构建,是一个轻量的,高性能的,性能残缺的,简略易用的 web 框架

服务创立

go-zero 中创立 http 服务非常简单,官网举荐应用 goctl 工具来生成。为了不便演示,这里通过手动创立服务,代码如下

package main

import (
    "log"
    "net/http"

    "github.com/tal-tech/go-zero/core/logx"
    "github.com/tal-tech/go-zero/core/service"
    "github.com/tal-tech/go-zero/rest"
    "github.com/tal-tech/go-zero/rest/httpx"
)

func main() {
    srv, err := rest.NewServer(rest.RestConf{
        Port: 9090, // 侦听端口
        ServiceConf: service.ServiceConf{Log: logx.LogConf{Path: "./logs"}, // 日志门路
        },
    })
    if err != nil {log.Fatal(err)
    }
    defer srv.Stop()
    // 注册路由
    srv.AddRoutes([]rest.Route{ 
        {
            Method:  http.MethodGet,
            Path:    "/user/info",
            Handler: userInfo,
        },
    })
    
    srv.Start() // 启动服务}

type User struct {
    Name  string `json:"name"`
    Addr  string `json:"addr"`
    Level int    `json:"level"`
}

func userInfo(w http.ResponseWriter, r *http.Request) {
    var req struct {UserId int64 `form:"user_id"` // 定义参数}
    if err := httpx.Parse(r, &req); err != nil { // 解析参数
        httpx.Error(w, err)
        return
    }
    users := map[int64]*User{1: &User{"go-zero", "shanghai", 1},
        2: &User{"go-queue", "beijing", 2},
    }
    httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回后果
}

通过 rest.NewServer 创立服务,示例配置了端口号和日志门路,服务启动后侦听在 9090 端口,并在当前目录下创立 logs 目录同时创立各等级日志文件

而后通过 srv.AddRoutes 注册路由,每个路由须要定义该路由的办法、Path 和 Handler,其中 Handler 类型为 http.HandlerFunc

最初通过 srv.Start 启动服务,启动服务后通过拜访 http://localhost:9090/user/info?user_id= 1 能够看到返回后果

{
    name: "go-zero",
    addr: "shanghai",
    level: 1
}

到此一个简略的 http 服务就创立实现了,可见应用 rest 创立 http 服务非常简单,次要分为三个步骤:创立 Server、注册路由、启动服务

JWT 鉴权

鉴权简直是每个利用必备的能力,鉴权的形式很多,而 jwt 是其中比较简单和牢靠的一种形式,在 rest 框架中内置了 jwt 鉴权性能,jwt 的原理流程如下图

<img src=”https://oscimg.oschina.net/oscnet/up-cd9dc0dbd93e7be4b46a3e8cba1f3438ccd.png” alt=”jwt” style=”zoom:50%;”>

rest 框架中通过 rest.WithJwt(secret) 启用 jwt 鉴权,其中 secret 为服务器秘钥是不能泄露的,因为须要应用 secret 来算签名验证 payload 是否被篡改,如果 secret 泄露客户端就能够自行签发 token,黑客就能肆意篡改 token 了。咱们基于下面的例子进行革新来验证在 rest 中如何应用 jwt 鉴权

获取 jwt

第一步客户端须要先获取 jwt,在登录接口中实现 jwt 生成逻辑

srv.AddRoute(rest.Route{
        Method:  http.MethodPost,
        Path:    "/user/login",
        Handler: userLogin,
})

为了演示不便,userLogin 的逻辑非常简单,次要是获取信息而后生成 jwt,获取到的信息存入 jwt payload 中,而后返回 jwt

func userLogin(w http.ResponseWriter, r *http.Request) {
    var req struct {
        UserName string `json:"user_name"`
        UserId   int    `json:"user_id"`
    }
    if err := httpx.Parse(r, &amp;req); err != nil {httpx.Error(w, err)
        return
    }
    token, _ := genToken(accessSecret, map[string]interface{}{
        "user_id":   req.UserId,
        "user_name": req.UserName,
    }, accessExpire)

    httpx.WriteJson(w, http.StatusOK, struct {
        UserId   int    `json:"user_id"`
        UserName string `json:"user_name"`
        Token    string `json:"token"`
    }{
        UserId:   req.UserId,
        UserName: req.UserName,
        Token:    token,
    })
}

生成 jwt 的办法如下

func genToken(secret string, payload map[string]interface{}, expire int64) (string, error) {now := time.Now().Unix()
    claims := make(jwt.MapClaims)
    claims["exp"] = now + expire
    claims["iat"] = now
    for k, v := range payload {claims[k] = v
    }
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = claims
    return token.SignedString([]byte(secret))
}

启动服务后通过 cURL 拜访

curl -X "POST" "http://localhost:9090/user/login" \
     -H 'Content-Type: application/json; charset=utf-8' \
     -d $'{"user_name":"gozero","user_id": 666}'

会失去如下返回后果

{
  "user_id": 666,
  "user_name": "gozero",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM"
}

增加 Header

通过 rest.WithJwt(accessSecret) 启用 jwt 鉴权

srv.AddRoute(rest.Route{
        Method:  http.MethodGet,
        Path:    "/user/data",
        Handler: userData,
}, rest.WithJwt(accessSecret))

拜访 /user/data 接口返回 401 Unauthorized 鉴权不通过,增加 Authorization Header,即能失常拜访

curl "http://localhost:9090/user/data?user_id=1" \
      -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM'

获取信息

个别会将用户的信息比方用户 id 或者用户名存入 jwt 的 payload 中,而后从 jwt 的 payload 中解析出咱们预存的信息,即可晓得本次申请时哪个用户发动的

func userData(w http.ResponseWriter, r *http.Request) {
    var jwt struct {
        UserId   int    `ctx:"user_id"`
        UserName string `ctx:"user_name"`
    }
    err := contextx.For(r.Context(), &amp;jwt)
    if err != nil {httpx.Error(w, err)
    }
    httpx.WriteJson(w, http.StatusOK, struct {
        UserId   int    `json:"user_id"`
        UserName string `json:"user_name"`
    }{
        UserId:   jwt.UserId,
        UserName: jwt.UserName,
    })
}

实现原理

jwt 鉴权的实现在 authhandler.go 中,实现原理也比较简单,先依据 secret 解析 jwt token,验证 token 是否无效,有效或者验证出错则返回 401 Unauthorized

func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) {writer := newGuardedResponseWriter(w)

    if err != nil {detailAuthLog(r, err.Error())
    } else {detailAuthLog(r, noDetailReason)
    }
    if callback != nil {callback(writer, r, err)
    }

    writer.WriteHeader(http.StatusUnauthorized)
}

验证通过后把 payload 中的信息存入 http request 的 context 中

ctx := r.Context()
for k, v := range claims {
  switch k {
    case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
    // ignore the standard claims
    default:
    ctx = context.WithValue(ctx, k, v)
  }
}

next.ServeHTTP(w, r.WithContext(ctx))

中间件

web 框架中的中间件是实现业务和非业务性能解耦的一种形式,在 web 框架中咱们能够通过中间件来实现诸如鉴权、限流、熔断等等性能,中间件的原理流程如下图

rest 框架中内置了十分丰盛的中间件,在 rest/handler 门路下,通过 alice 工具把所有中间件链接起来,当发动申请时会顺次通过每一个中间件,当满足所有条件后最终申请才会达到真正的业务 Handler 执行业务逻辑,下面介绍的 jwt 鉴权就是通过 authHandler 来实现的。因为内置中间件比拟多篇幅无限不能一一介绍,感兴趣的搭档能够自行学习,这里咱们介绍一下 prometheus 指标收集的中间件 PromethousHandler,代码如下

func PromethousHandler(path string) func(http.Handler) http.Handler {return func(next http.Handler) http.Handler {return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {startTime := timex.Now() // 起始工夫
            cw := &amp;security.WithCodeResponseWriter{Writer: w}
            defer func() {
        // 耗时
                metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), path)
        // code 码
                metricServerReqCodeTotal.Inc(path, strconv.Itoa(cw.Code))
            }()
            
            next.ServeHTTP(cw, r)
        })
    }
}

在该中间件中,在申请开始时记录了起始工夫,在申请完结后在 defer 中通过 prometheus 的 Histogram 和 Counter 数据类型别离记录了以后申请 path 的耗时和返回的 code 码,此时咱们通过拜访 http://127.0.0.1:9101/metrics 即可查看相干的指标信息

路由原理

rest 框架中通过 AddRoutes 办法来注册路由,每一个 Route 有 Method、Path 和 Handler 三个属性,Handler 类型为 http.HandlerFunc,增加的路由会被换成 featuredRoutes 定义如下

featuredRoutes struct {
        priority  bool // 是否优先级
        jwt       jwtSetting  // jwt 配置
        signature signatureSetting // 验签配置
        routes    []Route  // 通过 AddRoutes 增加的路由}

featuredRoutes 通过 engine 的 AddRoutes 增加到 engine 的 routes 属性中

func (s *engine) AddRoutes(r featuredRoutes) {s.routes = append(s.routes, r)
}

调用 Start 办法启动服务后会调用 engine 的 Start 办法,而后会调用 StartWithRouter 办法,该办法内通过 bindRoutes 绑定路由

func (s *engine) bindRoutes(router httpx.Router) error {metrics := s.createMetrics()

    for _, fr := range s.routes {if err := s.bindFeaturedRoutes(router, fr, metrics); err != nil { // 绑定路由
            return err
        }
    }

    return nil
}

最终会调用 patRouter 的 Handle 办法进行绑定,patRouter 实现了 Router 接口

type Router interface {
    http.Handler
    Handle(method string, path string, handler http.Handler) error
    SetNotFoundHandler(handler http.Handler)
    SetNotAllowedHandler(handler http.Handler)
}

patRouter 中每一种申请办法都对应一个树形构造,每个树节点有两个属性 item 为 path 对应的 handler,而 children 为带门路参数和不带门路参数对应的树节点, 定义如下:

node struct {item     interface{}
  children [2]map[string]*node
}

Tree struct {root *node}

通过 Tree 的 Add 办法把不同 path 与对应的 handler 注册到该树上咱们通过一个图来展现下该树的存储构造,比方咱们定义路由如下

{
  Method:  http.MethodGet,
  Path:    "/user",
  Handler: userHander,
},
{
  Method:  http.MethodGet,
  Path:    "/user/infos",
  Handler: infosHandler,
},
{
  Method:  http.MethodGet,
  Path:    "/user/info/:id",
  Handler: infoHandler,
},

路由存储的树形构造如下图

<img src=”https://oscimg.oschina.net/oscnet/up-fc2d96766688c6ce4f652588786cd46021d.png” style=”zoom:40%;”>

当申请来的时候会调用 patRouter 的 ServeHTTP 办法,在该办法中通过 tree.Search 办法找到对应的 handler 进行执行,否则会执行 notFound 或者 notAllow 的逻辑

func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {reqPath := path.Clean(r.URL.Path)
    if tree, ok := pr.trees[r.Method]; ok {if result, ok := tree.Search(reqPath); ok { // 在树中搜寻对应的 handler
            if len(result.Params) &gt; 0 {r = context.WithPathVars(r, result.Params)
            }
            result.Item.(http.Handler).ServeHTTP(w, r)
            return
        }
    }

    allow, ok := pr.methodNotAllowed(r.Method, reqPath)
    if !ok {pr.handleNotFound(w, r)
        return
    }

    if pr.notAllowed != nil {pr.notAllowed.ServeHTTP(w, r)
    } else {w.Header().Set(allowHeader, allow)
        w.WriteHeader(http.StatusMethodNotAllowed)
    }
}

总结

本文从整体上介绍了 rest,通过该篇文章可能根本理解 rest 的设计和次要性能,其中中间件局部是重点,外面集成了各种服务治理相干的性能,并且是主动集成的不须要咱们做任何配置,其余性能比方参数主动效验等性能因为篇幅无限在这里就不做介绍了,感兴趣的敌人能够自行查看官网文档进行学习。go-zero 中不光有 http 协定还提供了 rpc 协定和各种进步性能和开发效率的工具,是一款值得咱们深刻学习和钻研的框架。

我的项目地址

https://github.com/tal-tech/go-zero

如果感觉文章不错,欢送 github 点个 star ????

我的项目地址:
https://github.com/tal-tech/go-zero

正文完
 0