Caddy 源碼全解析

caddy源碼全解析

Caddy 源碼全解析

Preface

Caddy 是 Go 語言構建的輕量配置化服務器。同時代碼結構因爲 Go 語言的輕便簡潔,比較易讀,推薦學弟學妹學習 Go 的時候也去查看追一下它的源碼。不用怕相信這篇文章能給你很大的信心。git

可能會有點多,建議多看幾遍。github

Overview-CaddyMain

固然,建議看這篇文章的時候,查看上手一下 Caddy 的實際配置操做應用,對理解源碼會有好處,若是沒有操做過也沒有關係。golang

Package

這是 caddy 包的結構
chrome

首先咱們從一切的開始講起,即平時咱們程序運行的 main.go 函數。
這是 上圖 caddy 文件夾下的目錄結構。

在 caddy 文件夾中的 main 函數啓動 caddy 服務器。實際運行的是 run.go 中的文件,這是方便測試使用
main.go的代碼

經過改變 run 變量的值來方便測試,能夠學習一下。數組

啓動流程

啓動 caddy 的流程畫了張圖安全


見到不認識的不用擔憂,查看上文的目錄結構能夠找到他們大概的位置,下文會詳細講解。服務器

能夠在此圖中看到幾個重要的點 caddyfileLoader這是加載 caddyfile 配置來啓動服務器的。
若是配置使用過 caddy ,配置的 caddyfile 就是在這裏被 Loader 讀取後實例化服務器的。若是沒有使用過,大體說一下流程,使用 caddy 很是簡單,只需配置上文所說的 caddyfile 文件,按行配置選項,而後使用 caddy 運行讀取該配置文件便可。簡單示例就是如下的文本。
架構

Instance 是運行操做的實例,能夠看到幾個主要的操做都是在他身上併發

Server 能夠看到擁有 TCP UDP 兩個 Server 的接口。app

咱們首先關心的是 Start() 啓動服務器。

啓動服務器

發送 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

caddy.Start()

閱讀完代碼,畫一張圖幫助理解

是否是很簡單,來一點更詳細的交互

這裏除了 Instance 以外還有兩個新名詞
 Controller:它是用來幫助 Directives 設置它自身的,經過讀取 Token,這裏的 Directives 實際上對應的就是上文所說的 caddyfile 中的配置文件選項。這一點請參照下文中 Loader 下的 excuteDirective 理解。
 Token :是 caddy 本身的 詞法分析器 解析 caddyfile 配置文件出的選項的標記。這一點請參照下文中 Loader 中的 Parser 理解

若是不理解,首先記住 caddy 是配置化的服務器,
經過 caddyfile 配置 ->
那麼確定要讀取它啦 ->
而後要解析它配置的究竟是那些東西 ->
以後呢,就要讓配置的目標作到 caddyfile 中聲明的更改。
記住這個流程繼續看幾遍就能理解了。

Server

在 caddy.go 中定義着 Server 的接口,同時實現了優雅的退出。咱們首先看圖瞭解組織結構

簡單看一下 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

ServerType

最後看到不一樣 serverType 生成不一樣的 server

另外能夠看到 這裏最重要的 Instance 下面咱們進一步查看 Instance 的代碼

Instance

instance 是 Server 用來執行操做的實體。首先來看他的結構。它的代碼在 主文件夾中的 caddy.go

首先咱們看一下 它的結構瞭解下它可能有的功能

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
}

serverType 表明這個實例的服務器類型,一般是 HTTP

caddyfileInputInput 類型,一般咱們配置 caddy 服務器的時候,就是經過編輯 caddyfileInput 的文本實現的修改配置行動。值得注意的是,生成 Instance 的參數一樣是 caddyfile,這裏的 caddyfile 在程序中是一個接口,一下子繼續講解

wg 是用來等待全部 servers 執行他們操做的信號量。

context 是實例 Instance的上下文,其中包含 serverType 信息和服務器配置管理狀態的信息。

servers 是一組 server 和 他們的 listeners,兩種 Server TCP/UDP,即 serverType ,兩種不一樣的 serverType 會對應不一樣的 caddyfile中的選項。

OnXXX 等 6 個函數是一系列回調函數,經過名字可以看出在何時回調觸發。

Storage 是存儲數據的地方,原本能夠設計在 全局狀態中,可是設計在這裏更好,考慮到垃圾回收機制,進程中從新加載時,舊的 Instance be destroyed 以後,會變成垃圾,收集。這和 12-factor 中的 第九條 Disposability 相符合。意思是每一次重載實例 Instance 即便是在進程中重載,也不會出現數據相互影響到狀況,保持冪等


雖然 Instance 操做着衆多操做,可是咱們卻不能從它講起,從農村包圍城市,漸漸瞭解 Instance 能調用的函數,天然 Instance 的功能就清晰了。

Event

首先上圖:

首先咱們看到的是 eventHooks 這個結構,實際上他是存儲 key:name value:EventHook 這樣的一個 map[string]EventHook 的結構,只是從 sync 包中引入保證併發安全。

eventHooks = &sync.Map{}

而後是重要的 caddy.EventHook 結構。

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

而後咱們關注到如何註冊,和圖中的 caddy.EmitEvent 

註冊與分發

註冊 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")
    }
}

分發 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 文件的時候發生了什麼。

Loader

自定義的配置文件都會有讀取分析。在 caddy 中 由 Loader 執行這一項職能。首先咱們看一下它的工做流程。
這個圖來源於 plugin.go 文件

能夠看到這裏經過 Loader 解耦了 caddyfile 文件的讀取,因此把它放在了 plugin.go 文件中,做爲一個插件註冊在 caddy app 中。
這裏能夠看到最終流程是 name -> caddy.Input 那麼這個 Input 是什麼呢?
實際上 Input 就是 caddyfile 在代碼中的映射。能夠理解爲,caddyfile 轉化爲了 Input 給 caddy 讀取。誰來讀取它呢?
那麼幹活的主角登場啦!

Parser

這裏咱們來看,各個流程的終點 Token 是如何被分析出來的,須要知道,這裏的 Token 表明着 caddyfile 中的每行選項配置

詞法分析

// 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 數組中的迭代
關鍵在於移動 cursor 索引的函數
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() 的包裝函數罷了。

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 中調用來執行更改。

DirectiveAction

很容易發現,這裏是經過 操做 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啦,來看看吧。

Plugin

你會注意到,在目錄中有一個 叫 caddyhttp 的文件夾中的文件夾特別多,不用問,這就是 http 的可選 Plugin

Overview

這裏概覽了 Plugin 是如何註冊的。

能夠在這裏看到咱們以前講解的不少的熟悉的概念,這是由於咱們快要讀完 caddy 的架構了,剩下的其實是具體的 Plugin 的各類擴展實現了。
能夠看到,Plugin 是註冊在不一樣的 服務器類型 serverType 下的,其實是在兩重 map 映射的結構中,圖中能夠看出,而後是 Action ,最近的上文才說明了它,用它來進行 Plugin 的安裝。
而後來到 Controller ,實際進行配置的傢伙,看到了以前所說的 DispenserToken 配置,還記得嗎,他們在剛纔的詞法分析裏纔出現過。

接下來咱們看一個 HTTPPlugin 的例子 errors 的實現

caddyHTTP

errors

這裏咱們從下看,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 值得注意的兩個,
一個是他們會實現 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 安裝的全流程。

Directives

前面提到過不少次 Directives 這裏作一個它的整個流程概覽。上文中提到,這些註冊實際上都是 Controller 執行的。下半部分是 關於 HTTP 的服務配置
這裏的重點在 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 函數

謝謝閱讀,若是有不對的地方歡迎指正。

相關文章
相關標籤/搜索