关于go:41-Golang常用标准库nethttpserver

5次阅读

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

  Go 语言创立 HTTP 服务还是十分不便的,基于 http.Server 几行代码就能实现,本篇文章次要介绍 http.Server 的根本应用形式以及 HTTP 申请解决流程。当然,目前很多 web 服务都基于 gin 框架实现,所以咱们也会简略介绍下 gin 框架的一些应用套路。

http.Server 概述

  基于 http.Server 只须要短短几行代码就能创立一个 HTTP 服务,最简略的只须要配置好监听地址,以及申请解决 handler 就能够了,如上面程序所示:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    server := &http.Server{Addr: "0.0.0.0:8080",}

    // 注册路由(也就是申请解决办法),解决 /ping 申请,准确匹配(申请地址必须完全一致)http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("hello world"))
    })

    // 启动 HTTP 服务
    err := server.ListenAndServe()
    if err != nil {fmt.Println(err)
    }
}

/*
curl http://127.0.0.1:8080/ping
hello world

curl http://127.0.0.1:8080/ping/1
404 page not found
*/

  如下面程序所示,http.Server.Addr 设置 HTTP 服务监听地址,如果没有设置,默认监听 80 端口;http.HandleFunc 函数用于注册路由,也就是申请对应的解决办法,有两个参数:第一个参数含意是 pattern,有两种匹配形式,”/ping” 为准确匹配即申请地址必须等于 ”/ping”,如申请 ”/ping/1″ 无奈匹配,而 ”/ping/” 为前缀匹配,如申请 ”/ping/1″ 也会匹配胜利;第二个参数是函数类型,func(ResponseWriter, * Request),ResponseWriter 可用于向客户端返回数据,Request 代表以后 HTTP 申请。

  函数 http.Server.ListenAndServe 用于启动 HTTP 服务,想想流程应该是怎么的呢?必定须要 Listen 吧(底层必定少不了 socket,bind,listen 三个零碎调用),其次呢?期待客户端连贯呗,也就是循环期待 accept,一旦返回创立新的协程解决该客户端申请就行了(包含读取数据,解析 HTTP 申请,解决,返回数据)。还是比较简单的,整个流程的代码如下:

func (srv *Server) ListenAndServe() error {ln, err := net.Listen("tcp", addr)
    
    return srv.Serve(ln)
}

func (srv *Server) Serve(l net.Listener) error {

    for {rw, err := l.Accept()
        c := srv.newConn(rw)
        // 子协程解决申请
        go c.serve(connCtx)
    }
}

func (c *conn) serve(ctx context.Context) {

    for {
        // 读取 & 解析 HTTP 申请
        w, err := c.readRequest(ctx)

        // 解决 HTTP 申请
        serverHandler{c.server}.ServeHTTP(w, w.req)

        // 完结 & 返回
        w.finishRequest()

        if !w.conn.server.doKeepAlives() {return}
    }
}

  整个流程框架其实非常简单,与咱们料想的基本一致,当然这里咱们省略了很多细节问题。如,c.serve 函数主流程为什么是一个 for 循环呢?会屡次解决申请吗?当然是的,因为 HTTP 协定 keepalive 存在,客户端建设连贯之后,能够基于这一个连贯发送多个 HTTP 申请。而且,个别解决 HTTP 申请是不是都有超时工夫,如何解决超时问题咱们也省略了,咱们只关注都有哪些超时配置,这些配置都定义在 http.Server 构造,如下:

type Server struct {
    // 读取 HTTP 申请 head 超时工夫
    ReadHeaderTimeout time.Duration
    // 读取 HTTP 申请超时工夫
    ReadTimeout time.Duration

    // 写申请响应超时工夫(在此之前必须向客户端返回响应数据),超时敞开连贯
    WriteTimeout time.Duration
    // 闲暇长连贯超时工夫(留神如果没有设置,默认应用 ReadTimeout),超时敞开连贯
    IdleTimeout time.Duration
}

  有两个超时配置须要关注下:WriteTimeout 设置的是写申请响应超时工夫,也就是说从收到客户端申请开始,该工夫内必须向客户端返回响应数据,否则触发超时,敞开与客户端的连贯(划重点:可能会导致 502 或 reset),所以这也是业务申请最大解决工夫;IdleTimeout 设置的是闲暇长连贯超时工夫,也就是说解决完以后申请之后,Go 服务期待下一个申请达到的最大工夫(在此时间段认为长连贯闲暇),超过该时间段,会敞开与客户端的长连贯(划重点:可能会导致 502 或 reset),划重点,如果 IdleTimeout 没有设置,默认应用 ReadTimeout。

  另外,留神解决 HTTP 申请时,是通过 http.serverHandler.ServeHTTP 函数解决的,而咱们创立 HTTP 服务设置申请解决办法是通过函数 http.HandleFunc 实现的,这之间有什么关系吗?Go 语言 HTTP 申请处理器都必须实现接口 http.Handler,该接口只定义了一个办法 ServeHTTP:

type Handler interface {ServeHTTP(ResponseWriter, *Request)
}

//http.serverHandler 构造实现了 http.Handler 接口
type serverHandler struct {srv *Server}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    // http.Server.Handler 字段的类型就是 http.Handler,也就是说通过这个字段也能定义申请解决办法
    handler := sh.srv.Handler
    if handler == nil {
        //http.HandleFunc 注册路由到全局 DefaultServeMux
        handler = DefaultServeMux
    }

    // 解决申请
    handler.ServeHTTP(rw, req)
}

  http.serverHandler 构造实现了 http.Handler 接口(实现了办法 ServeHTTP),该办法可依据申请地址,匹配对应的解决办法并执行。另外留神到 http.Server 还有一个字段 Handler 也能定义申请解决办法,因为该字段的类型就是 http.Handler,也就是说咱们能够本人定义一个构造(只有实现 ServeHTTP 办法),自定义申请匹配形式。

  下面提到的多个构造 / 接口关系如下图所示:

gin 概述

  gin 是一款十分热门的的 web 框架,很多 web 服务都是基于他搭建的。gin 为咱们提供了更加丰盛的路由匹配形式,还有 middleware 拦截器更是为咱们提供了极大便当。gin 的应用非常简单,如下所示:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {r := gin.Default()

    // 注册路由
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "pong",})
    })
    r.Run()}

  gin 框架有三个十分重要的构造定义:1)gin.RouterGroup 提供多个路由注册办法,这些办法对应着 HTTP 申请 method,如 gin.RouterGroup.GET 只注册 GET 申请路由,gin.RouterGroup.Any 注册的路由与申请 method 无关;2)gin.Engine 就是 gin 框架实例,其继承了构造 gin.RouterGroup,所以下面的事例程序能力通过 ”r.GET” 注册路由。另外,gin 框架底层其实还是基于 http.Server 创立的 web 服务,也就是 HTTP 申请的解析、解决流程、响应阶段仍然是 http.Server 实现的。那么,HTTP 申请的解决 gin 框架是如何接管的呢?还记得 http.Handler 接口吗(只有一个办法 ServeHTTP),Go 语言 HTTP 申请处理器都必须实现接口 http.Handler,所以只须要 gin.Engine 实现 http.Handler 接口就能够了。

type RouterGroup struct {
    // 申请解决链(拦截器)Handlers HandlersChain
    // 根本门路
    basePath string
    //engin 实例
    engine   *Engine
}

// 路由组
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
}

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodPost, relativePath, handlers)
}


type Engine struct {
    RouterGroup
    // 路由
    trees            methodTrees
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {}

  gin.Engine 实现了办法 ServeHTTP,所以能够间接替换 Go 语言 HTTP 申请解决阶段,实现自定义的路由匹配,参数解析,申请解决,返回响应后果等等。gin.Engine 构造蕴含一个字段类型为 methodTrees,注册的所有路由都存储在该字段,gin 框架路由存储与匹配基于前缀树(Tire)实现,晋升路由匹配效率,有趣味的读者能够本人钻研下前缀树。

  基于 gin 框架注册路由时,申请解决办法只有一个输出参数,类型为 gin.Context,该构造封装了原生的 http.ResponseWriter 以及 http.Request,基于此定义了很多办法,如解析参数,返回响应后果:

// 解析 json 申请,obj 构造体指针类型变量
func (c *Context) BindJSON(obj interface{}) error 
// 获取申请 query 参数
func (c *Context) Query(key string) string
// 获取申请 header
func (c *Context) GetHeader(key string) string

// 响应后果增加 header
func (c *Context) Header(key, value string)
// 返回 json 后果,code 为 HTTP 状态码,obj 为返回构造变量
func (c *Context) JSON(code int, obj interface{})

  最初不得不提 gin 框架十分重要的概念,middleware 拦截器。构想有这么一个需要,服务的局部接口须要校验登录态怎么办?将登录态逻辑写到每一个申请解决办法吗?老本貌似有点高。不晓得你有没有留神到,gin 框架注册路由的第二个参数,是可变参数,也就是能够传递多个 HandlerFunc(解决链),路由匹配胜利后遍历执行所有的 HandlerFunc。middleware 拦截器与此相似,gin.Engine.Use 办法能够注册全局的拦截器,gin.RouterGroup.Group 办法也能够注册路由组的拦截器,通过在蓝拦截器校验登录态老本更低(校验胜利,设置用户信息上下文,执行下一个拦截器;校验失败,间接返回),校验登录拦截器能够如下所示:

loginGroup := router.Group("/admin", middleware.AuthCheck())
// 所有以 /admin 开始的申请都匹配到 loginGroup 组,并且执行 AuthCheck 办法

func AuthCheck() gin.HandlerFunc {return func(c *gin.Context) {
        if 失败 {
            // 返回未登录
            c.AbortWithStatusJSON(http.StatusOK, errorCode.UserNotLogin)
        }

        // 执行下一个拦截器
        c.Next()}

}

// 遍历执行下一个拦截器
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)
        c.index++
    }
}

  gin.Context.Next 办法用于执行下一个拦截器(也能够没有,以后拦截器执行结束后,主动执行下一个拦截器);联合 Next 办法,咱们能够在同一个拦截器实现,既拦挡申请达到又拦挡申请返回(Next 办法之后的逻辑,在申请返回时执行)。

  通过 web 服务会注册多个拦截器,包含 trace 用于全链路追踪,recover 用于捕捉申请 panic,accesslog 用于记录拜访日志等等,而 gin 框架自身也提供了几个默认的拦截器能够应用,如 gin.Logger()、gin.Recovery()。

如何记录 access 日志

  咱们写的服务不可能没有任何异样,而且日常工作中可能常常须要排查客户反馈问题,这时候你能失去的信息可能非常少,只有客户的手机号或者用户 ID 等,依据这些简略的信息如何去定位排查问题呢?第一步必定是要搞清楚,客户在什么时候出的什么错,也就是什么时候申请的什么接口,返回的什么数据。这些信息也能够去接入层如网关查问,不过网关可能不会记录用户 ID(网关不会校验登录态),甚至没有申请参数与返回数据。

  这就须要咱们本身服务具备这样的性能了,记录申请拜访日志(包含用户信息,申请参数,申请接口,返回数据等等),当然日志记录与采集也是须要老本的,这就须要在问题排查与老本之间均衡抉择了。

  参考 gin 框架,咱们能够注册一个全局的拦截器 accesslog 用于记录拜访日志,gin 框架自身也提供有默认的拦截器能够应用:

func LoggerWithConfig(conf LoggerConfig) HandlerFunc {return func(c *Context) {
        // Start timer
        start := time.Now()
        path := c.Request.URL.Path
        raw := c.Request.URL.RawQuery

        // Process request
        c.Next()

        // Log only when path is not being skipped
        if _, ok := skip[path]; !ok {
            param := LogFormatterParams{
                Request: c.Request,
                isTerm:  isTerm,
                Keys:    c.Keys,
            }

            // Stop timer
            param.TimeStamp = time.Now()
            param.Latency = param.TimeStamp.Sub(start)

            param.ClientIP = c.ClientIP()
            param.Method = c.Request.Method
            param.StatusCode = c.Writer.Status()
            param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()

            param.BodySize = c.Writer.Size()

            if raw != "" {path = path + "?" + raw}

            param.Path = path

            fmt.Fprint(out, formatter(param))
        }
    }
}

//[GIN] 2022/09/14 - 11:21:26 | 200 |      201.43µs |       127.0.0.1 | GET      "/ping"

  能够看到,默认日志拦截器记录了客户端 IP,申请接口,耗时,响应状态码等信息。当然你本人实现时也能够增加其余额定信息,如用户信息。只不过在记录申请参数以及响应后果时,你会发现,申请参数 Request.Body 只能读取一次,拦截器读取了后续流程怎么办?响应数据压根获取不到!

  针对这两个问题,有一些办法能够解决,申请参数 Request.Body 只能读取一次,那就读取之后再将数据塞回去,那后续流程就能持续获取申请数据了。至于获取响应数据无奈获取的问题,能够封装并替换 gin 框架原生的 ResponseWriter,在写响应数据的时候,先缓存到 buffer,再调用原生 ResponseWriter 返回数据即可。这两个问题的解决方案如上面代码所示:

//body 就是申请参数了,也不影响后续流程
var body []byte
if c.Request.Body != nil {body, _ = ioutil.ReadAll(c.Request.Body)
}
// 将数据再塞回去
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
// 封装 gin.ResponseWriter
type bodyLogWriter struct {
    gin.ResponseWriter
    body *bytes.Buffer
}
// 将响应数据同时缓存在 buffer
func (w bodyLogWriter) Write(b []byte) (int, error) {if _, err := w.body.Write(b); err != nil {log.Printf("bodyLogWriter err:%v", err)
    }
    return w.ResponseWriter.Write(b)
}
// 替换 writer
blw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer}
c.Writer = blw

总结

  本篇文章首先介绍了 http.Server 的次要流程,包含监听,解析申请,解决申请,响应数据等阶段,咱们也直到了 Go 语言 HTTP 申请处理器都必须实现接口 http.Handler,通过实现该接口,咱们能够自定义申请匹配以及解决形式。另外,还解说了热门 gin 框架的根本应用,包含路由注册形式,middleware 拦截器的概念以及原理,以及在此之上如何记录 access 日志。

正文完
 0