Gin 框架源码学习(二) -- 服务启动

BitRhapsodyMaster
• 阅读 2722

本篇主要介绍gin服务启动过程的源码 (gin版本v1.7.0)

Run() 启动入口

我们的程序都是通过调用Run函数来启动gin的实例,下面来看一下Run的源码:

func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()
    // 解析服务地址
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    // 此处会block
    err = http.ListenAndServe(address, engine)
    return
}

该方法其实是对http.ListenAndServe的简单封装,以下逻辑就进入net/http包中了,继续往下看:

func ListenAndServe(addr string, handler Handler) error {
    // 配置tcp的监听地址和监听到来后的处理函数
    server := &Server{Addr: addr, Handler: handler}
    // 继续调用。。。
    return server.ListenAndServe()
}

// Handler定义
type Handler interface {
    // 请求到来后的处理逻辑,gin框架会自己实现
    ServeHTTP(ResponseWriter, *Request)
}

func (srv *Server) ListenAndServe() error {
    // 异常处理
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    // 监听配置
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(ln)
}

func (srv *Server) Serve(l net.Listener) error {
    if fn := testHookServerServe; fn != nil {
        fn(srv, l) // call hook with unwrapped listener
    }

    origListener := l
    l = &onceCloseListener{Listener: l}
    defer l.Close()

    if err := srv.setupHTTP2_Serve(); err != nil {
        return err
    }

    if !srv.trackListener(&l, true) {
        return ErrServerClosed
    }
    defer srv.trackListener(&l, false)

    baseCtx := context.Background()
    if srv.BaseContext != nil {
        baseCtx = srv.BaseContext(origListener)
        if baseCtx == nil {
            panic("BaseContext returned a nil context")
        }
    }

    var tempDelay time.Duration // how long to sleep on accept failure

    // 带值的上下文
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    // 死循环,监听&处理请求
    for {
        // 请求过来了
        rw, err := l.Accept()
        if err != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := err.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return err
        }
        connCtx := ctx
        if cc := srv.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        // 创建一个新连接,一个conn对象,代表服务端的http连接,里边包含该次请求的数据
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        // 开启一个goroutine处理请求,将上下文传递进去,高并发的保障
        go c.serve(connCtx)
    }
}

// serve函数只摘取最重要的一行
func (c *conn) serve(ctx context.Context) {

    ...
    // 这里就会调用gin框架实现的ServeHTTP逻辑
    serverHandler{c.server}.ServeHTTP(w, w.req)

    ...
}

ServeHTTP 逻辑

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从对象池子里获取一个Context对象,池子里有可以直接拿来用,没有则创建一个新的对象返回
    c := engine.pool.Get().(*Context)
    // 把请求的相关数据都写入Context
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    // 请求处理逻辑
    engine.handleHTTPRequest(c)
    // 使用完毕,放回池子,复用
    engine.pool.Put(c)
}

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }

    if engine.RemoveExtraSlash {
        rPath = cleanPath(rPath)
    }

    // Find root of the tree for the given HTTP method
    t := engine.trees
    // 遍历路由数组,根据请求方法找出对应的那棵树
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // 从树中找到路由对应的函数处理链和参数
        // Find route in tree
        value := root.getValue(rPath, c.params, unescape)
        if value.params != nil {
            c.Params = *value.params
        }
        if value.handlers != nil { // 找到了
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            // 按顺序调用处理函数链
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != "CONNECT" && rPath != "/" {
            if tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }

    if engine.HandleMethodNotAllowed {
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
                c.handlers = engine.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

// 看一下Next方法,很简单
func (c *Context) Next() {
    // 初始化的时候是-1
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

总结

  1. 启动流程很清晰,通过net/http包监听请求,核心逻辑就是一个死循环,无限等待,当有请求到达指定端口时,启动一个goroutine异步处理该请求。
  2. 请求的处理逻辑使用的是gin框架自己实现的ServeHTTP函数,gin对Conext的封装和复用,也是一大亮点。
  3. context的Next方法在中间件中的使用,可以实现后置中间件。
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Wesley13 Wesley13
4年前
RAC环境单实例启动数据库收到ORA
     在RAC环境中,如果你在没有启动节点的集群服务的情况下单实例启动数据库,将收到类似如下的报错:\oracle@rhel1u01\$sqlSQL\Plus:Release10.2.0.5.0ProductiononTueApr215:00:272013Copyright(
Easter79 Easter79
4年前
springboot学习心得
1、mvnpackage加载运行一个含有pom.xml的目录并生成target目录2、mvndependency:tree显示项目所有依赖的树状结构3、业务委托给了SpringBoot的SpringApplication类通过调用run()执行4、mvnspringboot:run启动服务程序【localhost:8
Stella981 Stella981
4年前
Python+Selenium自动化篇
本篇文字主要学习selenium定位页面元素的集中方法,以百度首页为例子。0.元素定位方法主要有:id定位:find\_element\_by\_id('')name定位:find\_element\_by\_name('')class定位:find\_element\_by\_class\_name(''
Wesley13 Wesley13
4年前
Java面试系列
实现多线程的方式继承Thread类,重写run方法,调用start方法启动线程实现Runnable接口,重写run方法,调用start方法启动线程实现Callable接口,重写call方法,并用FutureTask包装,在newThread中传入FutureTask,然后调用start方
Wesley13 Wesley13
4年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n