美文网首页技术猫Go
Caddy 源码全解析

Caddy 源码全解析

作者: 阿碎Abser | 来源:发表于2019-08-05 00:22 被阅读0次

    Caddy 源码全解析

    <a name="Aj7SD"></a>

    Preface

    Caddy 是 Go 语言构建的轻量配置化服务器。同时代码结构由于 Go 语言的轻便简洁,比较易读,推荐学弟学妹学习 Go 的时候也去查看追一下它的源码。不用怕相信这篇文章能给你很大的信心。

    可能会有点多,建议多看几遍。

    <a name="jkAbX"></a>

    Overview-CaddyMain

    当然,建议看这篇文章的时候,查看上手一下 Caddy 的实际配置操作应用,对理解源码会有好处,如果没有操作过也没有关系。

    <a name="cHsfS"></a>

    Package

    这是 caddy 包的结构<br /> image
    在 caddy 文件夹中的 main 函数启动 caddy 服务器。实际运行的是 run.go 中的文件,这是方便测试使用<br />看 main.go 的代码<br /> image.png

    这里除了 Instance 之外还有两个新名词<br /> Controller:它是用来帮助 Directives 设置它自身的,通过读取 Token,这里的 Directives 实际上对应的就是上文所说的 caddyfile 中的配置文件选项。这一点请参照下文中 Loader 下的 excuteDirective 理解。<br /> Token :是 caddy 自己的 词法分析器 解析 caddyfile 配置文件出的选项的标记。这一点请参照下文中 Loader 中的 Parser 理解

    如果不理解,首先记住 caddy 是配置化的服务器,<br />通过 caddyfile 配置 -><br />那么肯定要读取它啦 -><br />然后要解析它配置的到底是那些东西 -><br />之后呢,就要让配置的目标做到 caddyfile 中声明的更改。<br />记住这个流程继续看几遍就能理解了。

    <a name="xIBOb"></a>

    Server

    在 caddy.go 中定义着 Server 的接口,同时实现了优雅的退出。我们首先看图了解组织结构
    <a name="ttsMh"></a>

    image.png

    简单看一下 Stopper 的接口

    // Stopper is a type that can stop serving. The stop
    // does not necessarily have to be graceful.
    type Stopper interface {
        // Stop stops the server. It blocks until the
        // server is completely stopped.
        Stop() error
    }
    

    GracefulServer 包含 Stopper 的接口实现了优雅退出,这是拦截了 系统 signal 的信号之后执行的结果,意在意外中断的时候保存好需要保存的东西。

    它同时包含着 WrapListener 函数。可以看出,他用来做中间件。

        // WrapListener wraps a listener with the
        // listener middlewares configured for this
        // server, if any.
        WrapListener(net.Listener) net.Listener
    

    <a name="gSEKj"></a>

    ServerType

    最后看到不同 serverType 生成不同的 server

    image.png

    另外可以看到 这里最重要的 Instance 下面我们进一步查看 Instance 的代码

    <a name="tsNvG"></a>

    Instance

    instance 是 Server 用来执行操作的实体。首先来看他的结构。它的代码在 主文件夹中的 caddy.go

    首先我们看一下 它的结构了解下它可能有的功能
    <a name="wmOLJ"></a>

    struct

    type Instance struct {
        serverType string
        caddyfileInput Input
        wg *sync.WaitGroup
        context Context
        servers []ServerListener
        OnFirstStartup  []func() error // starting, not as part of a restart
        OnStartup       []func() error // starting, even as part of a restart
        OnRestart       []func() error // before restart commences
        OnRestartFailed []func() error // if restart failed
        OnShutdown      []func() error // stopping, even as part of a restart
        OnFinalShutdown []func() error // stopping, not as part of a restart
        Storage   map[interface{}]interface{}
        StorageMu sync.RWMutex
    }
    

    <a name="7sdQp"></a>

    serverType 代表这个实例的服务器类型,通常是 HTTP

    <a name="7iHKm"></a>

    caddyfileInputInput 类型,通常我们配置 caddy 服务器的时候,就是通过编辑 caddyfileInput 的文本实现的修改配置行动。值得注意的是,生成 Instance 的参数同样是 caddyfile,这里的 caddyfile 在程序中是一个接口,一会儿继续讲解

    <a name="oi3MF"></a>

    wg 是用来等待所有 servers 执行他们操作的信号量。

    <a name="pEb0L"></a>

    context 是实例 Instance的上下文,其中包含 serverType 信息和服务器配置管理状态的信息。

    <a name="M8dIp"></a>

    servers 是一组 server 和 他们的 listeners,两种 Server TCP/UDP,即 serverType ,两种不同的 serverType 会对应不同的 caddyfile中的选项。

    <a name="7Bo3V"></a>

    OnXXX 等 6 个函数是一系列回调函数,通过名字能够看出在什么时候回调触发。

    <a name="yNLDF"></a>

    Storage 是存储数据的地方,本来可以设计在 全局状态中,但是设计在这里更好,考虑到垃圾回收机制,进程中重新加载时,旧的 Instance be destroyed 之后,会变成垃圾,收集。这和 12-factor 中的 第九条 Disposability 相符合。意思是每一次重载实例 Instance 即使是在进程中重载,也不会出现数据相互影响到情况,保持幂等
    image.png

    首先我们看到的是 eventHooks 这个结构,实际上他是存储 key:name value:EventHook 这样的一个 map[string]EventHook 的结构,只是从 sync 包中引入保证并发安全。

    eventHooks = &sync.Map{} 
    

    然后是重要的 caddy.EventHook 结构。

    type EventHook func(eventType EventName, eventInfo interface{}) error
    

    <br />然后我们关注到如何注册,和图中的 caddy.EmitEvent
    <a name="vCCBy"></a>

    注册与分发

    <a name="ytuJU"></a>

    注册 EventHook

    可以看到使用 eventHooks.LoadOrStore方法,不必赘述

    func RegisterEventHook(name string, hook EventHook){
        if name == "" {
            panic("event hook must have a name")
        }
        _, dup := eventHooks.LoadOrStore(name, hook)
        if dup {
            panic("hook named" + name + "already registered")
        }
    }
    

    <a name="3Ifb3"></a>

    分发 EmitEvent

    通过传入函数为参数调用回调函数

    // EmitEvent executes the different hooks passing the EventType as an
    // argument. This is a blocking function. Hook developers should
    // use 'go' keyword if they don't want to block Caddy.
    func EmitEvent(event EventName, info interface{}) {
        eventHooks.Range(func(k, v interface{}) bool {
            err := v.(EventHook)(event, info)
            if err != nil {
                log.Printf("error on '%s' hook: %v", k.(string), err)
            }
            return true //注意这里返回的是 true
        })
    }
    

    这里使用的 Range 函数,实际上是把事件信息给每一个上述提过 map 中的 EventHook 提供参数进行回调执行,按顺序调用,但是如果 传入函数返回 false ,迭代遍历执行就会中断。

    可以知道,上文 Overview中启动服务器 所说的发送 caddy.StartupEvent 事件就是调用的

    caddy.EmitEvent(caddy.StartupEvent, nil)
    

    讲到这,相信已经对大致的流程有了一点框架的概念。

    下面我们继续深入了解 在读取 caddyfile 文件的时候发生了什么。

    <a name="xFQKc"></a>

    Loader

    自定义的配置文件都会有读取分析。在 caddy 中 由 Loader 执行这一项职能。首先我们看一下它的工作流程。<br />这个图来源于 plugin.go 文件

    image.png

    可以看到这里通过 Loader 解耦了 caddyfile 文件的读取,所以把它放在了 plugin.go 文件中,作为一个插件注册在 caddy app 中。<br />这里可以看到最终流程是 name -> caddy.Input 那么这个 Input 是什么呢?<br />实际上 Input 就是 caddyfile 在代码中的映射。可以理解为,caddyfile 转化为了 Input 给 caddy 读取。谁来读取它呢?<br />那么干活的主角登场啦!
    <a name="vNLTs"></a>

    Parser

    <a name="LwtxS"></a>

    image.png

    可以在这里看到我们之前讲解的很多的熟悉的概念,这是因为我们快要读完 caddy 的架构了,剩下的实际上是具体的 Plugin 的各种扩展实现了。<br />可以看到,Plugin 是注册在不同的 服务器类型 serverType 下的,实际上是在两重 map 映射的结构中,图中可以看出,然后是 Action ,最近的上文才说明了它,用它来进行 Plugin 的安装。<br />然后来到 Controller ,实际进行配置的家伙,看到了之前所说的 DispenserToken 配置,还记得吗,他们在刚才的词法分析里才出现过。

    接下来我们看一个 HTTPPlugin 的例子 errors 的实现

    <a name="ACP7a"></a>

    caddyHTTP

    <a name="z5G1t"></a>

    errors

    image.png

    这里我们从下看,caddy.Listener 定义在 caddy.go 中,用来支持 零停机时间加载。

    往上看到 Middleware 调用,我们来看看 errorsHandle 的结构

    // ErrorHandler handles HTTP errors (and errors from other middleware).
    type ErrorHandler struct {
        Next             httpserver.Handler
        GenericErrorPage string         // default error page filename
        ErrorPages       map[int]string // map of status code to filename
        Log              *httpserver.Logger
        Debug            bool // if true, errors are written out to client rather than to a log
    }
    

    可以看到,Next 字段明显是 Chain 调用的下一个 Handler 处理。事实上,每一个 Plugin 或者算是 HTTP 服务中的中间件都有这个字段用于 构建链式调用。

    每一个 Plugin 值得注意的两个,<br />一个是他们会实现 ServeHTTP 接口进行 HTTP 请求处理。

    func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
        defer h.recovery(w, r)
    
        status, err := h.Next.ServeHTTP(w, r)
    
        if err != nil {
            errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
            if h.Debug {
                // Write error to response instead of to log
                w.Header().Set("Content-Type", "text/plain; charset=utf-8")
                w.WriteHeader(status)
                fmt.Fprintln(w, errMsg)
                return 0, err // returning 0 signals that a response has been written
            }
            h.Log.Println(errMsg)
        }
    
        if status >= 400 {
            h.errorPage(w, r, status)
            return 0, err
        }
    
        return status, err
    }
    

    另一个是安装到 caddy 中的 setup.go 文件,我们看一下 Plugin 安装的全流程。

    <a name="O1c84"></a>

    Directives

    前面提到过很多次 Directives 这里做一个它的整个流程概览。上文中提到,这些注册实际上都是 Controller 执行的。下半部分是 关于 HTTP 的服务配置<br />这里的重点在 errors.serup() 可以看到,它创建了 errors.ErrHandler 并注册到了 httpserver 的一对中间件中

    // setup configures a new errors middleware instance.
    func setup(c *caddy.Controller) error {
        handler, err := errorsParse(c)
        ···
        httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
            handler.Next = next
            return handler
        })
        return nil
    }
    

    实际上这里还有一个关于 caddy.Controller 到 ErrorHandler 的一个转换 通过 errorsParse 函数<br />

    image.png

    谢谢阅读,如果有不对的地方欢迎指正。

    相关文章

      网友评论

        本文标题:Caddy 源码全解析

        本文链接:https://www.haomeiwen.com/subject/ujiudctx.html