golang iris mvc框架的服务端加载过程

fjxCode · · 1330 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

整个iris框架共三层结构:

  • 应用的配置和注册信息,如路由、中间件、日志。
  • 中间的服务端实例,从iris实例拿配置信息进行配置。
  • 底层net/http包,负责TCP连接建立、监听接受,请求收取并解析,缓冲区管理,写入响应。

监听服务的闭包构建

iris框架包装了Server为更完善的Supervisor类,数据结构如下:

type Supervisor struct {
    Server         *http.Server
    closedManually int32 
    manuallyTLS    bool  server begin.
    shouldWait     int32 
    unblockChan    chan struct{}

    mu sync.Mutex

    onServe []func(TaskHost)
    IgnoredErrors []string
    onErr         []func(error)
    onShutdown    []func()
}

调用NewHost(srv)他建Supervisor实例。先初始化以下字段:

srv.Addr
srv.Handler //app.Router
su.unblockChan //chan struct{}

关闭ShutdownGracefully

RegisterOnServe,注册一个向日志打印闭包。按下CMD+C中断的闭包。全局中断注册地:

// onInterrupt contains a list of the functions that should be called when CTRL+C/CMD+C or a unix kill //command received.
var Interrupt = new(interruptListener)

type interruptListener struct {
    mu   sync.Mutex
    once sync.Once

    onInterrupt []func()
}

具体实现的闭包是RegisterOnInterrupt:

先设置shutdownTimeout超时
su.closedManually++ //要用atomic包原子操作

su.onShutdown[:]() //执行所有的闭包
su.Server.Shutdown(ctx)//net/http标准库方法

//RestoreFlow
su.shouldWait=0 //atomic
su.unblockChan <- //mutex

//defer
ctx.cancel()

优雅关闭流程:

  • 先关闭open listeners
  • 关闭idle connections
  • 等待连接至idle再关闭
  • 忽略hijacked connections。不主动关闭,仅通知其关闭
Deferflow和shoudlWait标志位
func (su *Supervisor) DeferFlow() {
    atomic.StoreInt32(&su.shouldWait, 1)
}
func (su *Supervisor) isWaiting() bool {
    return atomic.LoadInt32(&su.shouldWait) != 0
}

配合unblockChan实现手动关闭su实例的功能。

    if su.isWaiting() {
    blockStatement:
        for {
            select {
            case <-su.unblockChan:
                break blockStatement
            }
        }
    }

使用样例:

su.DeferFlow() //不关闭直到主动调用

su.Shutdown(ctx) //关闭
su.RestoreFlow()
ctx.cancel()

su.closedManually==1表示主动调用过su.Shutdown()方法。

导入app.config.IgnoreServerErrors。

由app加载配置,iris并没有使用一个字段的struct。而是使用了闭包的写法。直接修改Supervisor实例,改变配置,减少数据传递提高效率。

[]Configurator func(su *Supervisor)

共有两次加载配置,一次是NewHost内部,调用su.Configure(app.hostConfigurators...),第二次外部调用su.Configure(hostConfigurators...)。

最后把Supervisor实例注册到app.Hosts。

ListenAndServe创建监听和服务

创建监听逻辑:

func (su *Supervisor) newListener() (net.Listener, error) {
    l, err := netutil.TCPKeepAlive(su.Server.Addr)
    if err != nil {
        return nil, err
    }

    if netutil.IsTLS(su.Server) {
        // means tls
        tlsl := tls.NewListener(l, su.Server.TLSConfig)
        return tlsl, nil
    }
    
    return l, nil
}
func TCPKeepAlive(addr string) (ln net.Listener, err error) {
    ln, err = TCP(addr)
    if err != nil {
        return nil, err
    }
    return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil
}
func TCP(addr string) (net.Listener, error) {
    l, err := net.Listen("tcp", addr)
    if err != nil {
        return nil, err
    }
    return l, nil
}

iris框架把net包封装成了更易用的netutil包。方便创建keepAlive TLS letsencrypt.org 的监听。API如下:

(l tcpKeepAliveListener) Accept() (c net.Conn, err error)
TCP(addr string) (net.Listener, error)
TCPKeepAlive(addr string) (ln net.Listener, err error)
TLS(addr, certFile, keyFile string) (net.Listener, error)
CERT(addr string, cert tls.Certificate) (net.Listener, error)
UNIX(socketFile string, mode os.FileMode) (net.Listener, error)
LETSENCRYPT(addr string, serverName string, cacheDirOptional ...string) (net.Listener, error)

由netutil包建立的TCP常连接keepalive时间为3分钟。

调用su.Serve(l)方法即调用net/http标准库中的su.Server.Serve(l)。

框架将ListenAndServe及supervisor构建包装成了闭包。由于闭包内的代码需要iris实例和supervisor实例均已构建完成之后执行,所以封装成闭包,等前iris对象实例化之后再调用。

构建iris实例并运行服务

前文说明

前几篇文章已详述过,在iris服务端运行之前。iris实例已经加载了handler和middleware。建立好了ctx的对象池,这里的对象池,是iris自定义的实例,包裹了http请求、响应以及其它连接参数,不同于TCP连接时的KV参数,ctx cancelCtx timerCtx等。日志加载和配置信息加载,将在调用Runner闭包时执行。

实际iris框架,在Runner闭包调用前后,共对iris服务端实例进行了三次配置:

  • 注册阶段的配置
  • Run()阶段调用app.Build,功能:builds the default router and the template function。
  • Runner闭包内加载func Configurator,功能是配置*Server实例及其包裹实例。仍由少量信息反馈给iris实例。如包裹*Server的Supervisor实例本身,就会被注册到app.Host字段。(可能多实例,要考虑到并发)

在调用app.Build()之前,app.APIBuilder字段已构建完成,包括了路由信息。ApiBuider是一个路由构建器,提供了路由构建的接口:

type RoutesProvider interface {
    GetRoutes() []*Route
    GetRoute(routeName string) *Route
}

路由按路径及其子路径。路由的存储结构如下:

type routerHandler struct {
    trees []*tree
    hosts bool // true if at least one route contains a Subdomain.
}
type tree struct {
    Method string
    Subdomain string
    Nodes     *node.Nodes
}
type node struct {
    s                 string
    routeName         string
    wildcardParamName string   
    paramNames        []string 
    childrenNodes     Nodes
    handlers          context.Handlers
    root              bool
    rootWildcard      bool 
}

每一段路由路径,都包含了一系列context.Handlers。路由数组,只要执行第一向就会向后链式执行,并包裹子节点的路由。

经过以上复杂的准备,可以执行构建路由了。

最后处理app.view。routeProvider.Path()方法功能是由路由名解析出路由路径,其中参数用%v代替。

构建routeHandler

先对路由排序,排序规则是:subDomain长的在前。如果子域长度相等且,则第一段路径在前。如果仍相等,则请求路径中参数较多的在前。

路由中handler的排序为:beginHandlers Handlers doneHandlers。

接下来完成将路由route添加到routerHandler。路由是用树的数组保存的,每个树是同一类请求方法,同一个子域,节点是树状图。每个节点包含了:一段路径 参数名字列表 handlers 子节点。

再看路由的数据结构:

type Route struct {
    Name       string       
    Method     string        
    methodBckp string        
    Subdomain  string   
    
    tmpl       *macro.Template 
    beginHandlers context.Handlers
    Handlers        context.Handlers 
    MainHandlerName string          
    doneHandlers context.Handlers
    Path         string 
    FormattedPath string `json:"formattedPath"`
}

先由method, subdomain找到所在tree,再根据path, handler添加路由树中。

为什么要三次构建路由

这里区别APIBuilder和routerHandler保存路的方式。以及为什么要三次构建路由。

在mvc controller中:http请求方式 http请求路径 subdomain(就是controller类的RequestMapping) 一组handlers,共同构成了一条路由路径。在注册controller时,以列表的方式加入到APIBuilder,是工作量最小的保存方式。

但这样有问题,在服务端拿到httpRequest时,查找路由的效率极低,需要遍历整个个数组。因而需要将路由表按subdomain+path构成树状结构,加快查找效率。

整个过程,由控制器添加线性的路由表,并转成树状路由表是在服务端接受http请求之前完成,不会影响http处理效率。接受请求之后,遍历树进行查找,命中则重新构建一条路径,处理http请求。

树状路由添加节点

执行方法:

t.Nodes.Add(routeName, path, handlers)

收尾工作

路由自带一个context的sync.pool,复用对象加快httpRequest的构建。

保留路由构建工具APIBuilder作为一个字段。

最后作为Server.Handler类型,要实现ServeHTTP接口。

    router.mainHandler = func(w http.ResponseWriter, r *http.Request) {
        ctx := cPool.Acquire(w, r)
        router.requestHandler.HandleRequest(ctx)
        cPool.Release(ctx)
    }

    if router.wrapperFunc != nil { 
        router.mainHandler = NewWrapper(router.wrapperFunc, router.mainHandler).ServeHTTP
    }

需要说明的是router本身也实现了Hanler接口,可以调用router.ServeHTTP()。但是这样使用不会复用contex对象,会带来性能损耗

将符合net/http包Server.Handler接口的实现,内部交由requestHandler.HandleRequest实现。Router是对requestHandler的包装:

type Router struct {
    mu sync.Mutex 
    requestHandler RequestHandler   
    mainHandler    http.HandlerFunc 
    wrapperFunc    func(http.ResponseWriter, *http.Request, http.HandlerFunc)

    cPool          *context.Pool 
    routesProvider RoutesProvider
}

如果使用了包裹,先执行包裹代码。包裹哪些代码,被包裹的代码段,可以自由配置,利益于闭包的使用。将两个闭包打包成一个struct{},再实现一个类方法,造知这两个闭包的代码以何种顺序执行。如果没有闭包,函数是写死在包裹中的,不能更换,进而会增加许多重复的代码。

type wrapper struct {
    router      http.HandlerFunc 
    wrapperFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc)
}
func (wr *wrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    wr.wrapperFunc(w, r, wr.router)
}

更进一步,源代码中的WrapRouter方法实现了两层包裹。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:fjxCode

查看原文:golang iris mvc框架的服务端加载过程

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1330 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传