Caddy 源码全解析

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.png
image.png
image.png
image.png

首先我们从一切的开始讲起,即平时我们程序运行的 main.go 函数。<br />这是 上图 caddy 文件夹下的目录结构。

image

在 caddy 文件夹中的 main 函数启动 caddy 服务器。实际运行的是 run.go 中的文件,这是方便测试使用<br />看 main.go 的代码<br />
image.png
image.png
<br />通过改变 run 变量的值来方便测试,可以学习一下。

<a name="F1ozR"></a>

启动流程

启动 caddy 的流程画了张图

屏幕快照 2019-08-04 下午6.34.21.png
屏幕快照 2019-08-04 下午6.34.21.png

<br />见到不认识的不用担心,查看上文的目录结构可以找到他们大概的位置,下文会详细讲解。

可以在此图中看到几个重要的点 caddyfileLoader这是加载 caddyfile 配置来启动服务器的。<br />如果配置使用过 caddy ,配置的 caddyfile 就是在这里被 Loader 读取后实例化服务器的。如果没有使用过,大致说一下流程,使用 caddy 非常简单,只需配置上文所说的 caddyfile 文件,按行配置选项,然后使用 caddy 运行读取该配置文件即可。简单示例就是以下的文本。<br />

image.png
image.png

Instance 是运行操作的实例,可以看到几个主要的操作都是在他身上

Server 可以看到拥有 TCP UDP 两个 Server 的接口。

我们首先关心的是 Start() 启动服务器。

<a name="4HB1R"></a>

启动服务器

发送 StartupEvent, 参照下文中 Event 理解

// Executes Startup events
caddy.EmitEvent(caddy.StartupEvent, nil)

读取配置文件:

caddyfileinput, err := caddy.LoadCaddyfile(serverType)

启动:

instance, err := caddy.Start(caddyfileinput)

发送 InstanceStartupEvent

caddy.EmitEvent(caddy.InstanceStartupEvent, instance

<a name="1tN78"></a>

caddy.Start()

阅读完代码,画一张图帮助理解<br />
屏幕快照 2019-08-04 下午6.35.03.png
屏幕快照 2019-08-04 下午6.35.03.png

<br />是不是很简单,来一点更详细的交互<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 即使是在进程中重载,也不会出现数据相互影响到情况,保持幂等
屏幕快照 2019-08-04 下午6.34.33.png
屏幕快照 2019-08-04 下午6.34.33.png

<br />虽然 Instance 操作着众多操作,但是我们却不能从它讲起,从农村包围城市,渐渐了解 Instance 能调用的函数,自然 Instance 的功能就清晰了。

<a name="Js2Dd"></a>

Event

首先上图:
<a name="zXi6Q"></a>


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>

屏幕快照 2019-08-04 下午6.35.33.png
屏幕快照 2019-08-04 下午6.35.33.png

这里我们来看,各个流程的终点 Token 是如何被分析出来的,需要知道,这里的 Token 代表着 caddyfile 中的每行选项配置
<a name="rmOWu"></a>

词法分析

// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) ([]Token, error) {
    l := new(lexer)
    err := l.load(input)
    if err != nil {
        return nil, err
    }
    var tokens []Token
    for l.next() {
        tokens = append(tokens, l.token)
    }
    return tokens, nil
}

这里实际上关键在于 读取,可以看到在 dispenser 中由 cursor 来进行 Token 数组中的迭代<br />关键在于移动 cursor 索引的函数<br />next()

// next loads the next token into the lexer.
// A token is delimited by whitespace, unless
// the token starts with a quotes character (")
// in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// Inside quoted strings, quotes may be escaped
// with a preceding \ character. No other chars
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
    var val []rune
    var comment, quoted, escaped bool

    makeToken := func() bool {
        l.token.Text = string(val)
        return true
    }

    for {
        ch, _, err := l.reader.ReadRune()
        if err != nil {
            if len(val) > 0 {
                return makeToken()
            }
            if err == io.EOF {
                return false
            }
            panic(err)
        }

        if quoted {
            if !escaped {
                if ch == '\\' {
                    escaped = true
                    continue
                } else if ch == '"' {
                    quoted = false
                    return makeToken()
                }
            }
            if ch == '\n' {
                l.line++
            }
            if escaped {
                // only escape quotes
                if ch != '"' {
                    val = append(val, '\\')
                }
            }
            val = append(val, ch)
            escaped = false
            continue
        }

        if unicode.IsSpace(ch) {
            if ch == '\r' {
                continue
            }
            if ch == '\n' {
                l.line++
                comment = false
            }
            if len(val) > 0 {
                return makeToken()
            }
            continue
        }

        if ch == '#' {
            comment = true
        }

        if comment {
            continue
        }

        if len(val) == 0 {
            l.token = Token{Line: l.line}
            if ch == '"' {
                quoted = true
                continue
            }
        }

        val = append(val, ch)
    }
}

理解了 next 函数,就很容易知道如何分析一块选项的 token 了,不过都是 next() 的包装函数罢了。

<a name="qGe2M"></a>

excuteDirective

func executeDirectives(inst *Instance, filename string,
    directives []string, sblocks []caddyfile.ServerBlock, justValidate bool) error {
    // map of server block ID to map of directive name to whatever.
    storages := make(map[int]map[string]interface{})

    // It is crucial that directives are executed in the proper order.
    // We loop with the directives on the outer loop so we execute
    // a directive for all server blocks before going to the next directive.
    // This is important mainly due to the parsing callbacks (below).
    for _, dir := range directives {
        for i, sb := range sblocks {
            var once sync.Once
            if _, ok := storages[i]; !ok {
                storages[i] = make(map[string]interface{})
            }

            for j, key := range sb.Keys {
                // Execute directive if it is in the server block
                if tokens, ok := sb.Tokens[dir]; ok {
                    controller := &Controller{
                        instance:  inst,
                        Key:       key,
                        Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
                        OncePerServerBlock: func(f func() error) error {
                            var err error
                            once.Do(func() {
                                err = f()
                            })
                            return err
                        },
                        ServerBlockIndex:    i,
                        ServerBlockKeyIndex: j,
                        ServerBlockKeys:     sb.Keys,
                        ServerBlockStorage:  storages[i][dir],
                    }

                    setup, err := DirectiveAction(inst.serverType, dir)
                    if err != nil {
                        return err
                    }

                    err = setup(controller)
                    if err != nil {
                        return err
                    }

                    storages[i][dir] = controller.ServerBlockStorage // persist for this server block
                }
            }
        }

        if !justValidate {
            // See if there are any callbacks to execute after this directive
            if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok {
                callbacks := allCallbacks[dir]
                for _, callback := range callbacks {
                    if err := callback(inst.context); err != nil {
                        return err
                    }
                }
            }
        }
    }

    return nil
}

caddyfile 既然被解析完毕,那么就要开始执行配置更改了,这里实际上是 caddy.go 中的 函数,最后在 caddy 的 main.go 中调用来执行更改。

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

DirectiveAction

<a name="XHiBo"></a>

屏幕快照 2019-08-04 下午6.35.54.png
屏幕快照 2019-08-04 下午6.35.54.png

很容易发现,这里是通过 操作 Controller 来实现的,此时可以再返回最上文查看上一次提到 Controller 的时候。

// DirectiveAction gets the action for directive dir of
// server type serverType.
func DirectiveAction(serverType, dir string) (SetupFunc, error) {
    if stypePlugins, ok := plugins[serverType]; ok {
        if plugin, ok := stypePlugins[dir]; ok {
            return plugin.Action, nil
        }
    }
    if genericPlugins, ok := plugins[""]; ok {
        if plugin, ok := genericPlugins[dir]; ok {
            return plugin.Action, nil
        }
    }
    return nil, fmt.Errorf("no action found for directive '%s' with server type '%s' (missing a plugin?)",
        dir, serverType)
}

了解完这些,我们注意到有一个 叫做 Action 的东西,它又是怎么来的?别急,他就在 Plugin 包中。我们知道了,配置文件实际上是配置各种 plugin 作为插件安装在 caddy 服务器上,而 caddyfile 正是被转化为了 Token,Dispenser 来执行配置更改,即不同的插件安装。那么 Action 就是 PluginSetupFunc啦,来看看吧。

<a name="XarZm"></a>

Plugin

你会注意到,在目录中有一个 叫 caddyhttp 的文件夹中的文件夹特别多,不用问,这就是 http 的可选 Plugin

<a name="g4ohI"></a>

Overview

这里概览了 Plugin 是如何注册的。

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容