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

http.Server 概述

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

package mainimport (    "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/pinghello worldcurl http://127.0.0.1:8080/ping/1404 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 mainimport (    "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//获取申请headerfunc (c *Context) GetHeader(key string) string//响应后果增加headerfunc (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 []byteif c.Request.Body != nil {    body, _ = ioutil.ReadAll(c.Request.Body)}//将数据再塞回去c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
//封装gin.ResponseWritertype bodyLogWriter struct {    gin.ResponseWriter    body *bytes.Buffer}//将响应数据同时缓存在bufferfunc (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)}//替换writerblw := &bodyLogWriter{body: bytes.NewBuffer([]byte{}), ResponseWriter: c.Writer}c.Writer = blw

总结

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