Caddy 源碼全解析
<a name="Aj7SD"></a>
Preface
Caddy 是 Go 語言構建的輕量配置化服務器。同時代碼結構由于 Go 語言的輕便簡潔,比較易讀,推薦學弟學妹學習 Go 的時候也去查看追一下它的源碼。不用怕相信這篇文章能給你很大的信心。
可能會有點多,建議多看幾遍。
<a name="jkAbX"></a>
Overview-CaddyMain
當然,建議看這篇文章的時候,查看上手一下 Caddy 的實際配置操作應用,對理解源碼會有好處,如果沒有操作過也沒有關系。
<a name="cHsfS"></a>
Package
這是 caddy 包的結構<br />

首先我們從一切的開始講起,即平時我們程序運行的 main.go 函數。<br />這是 上圖 caddy 文件夾下的目錄結構。
在 caddy 文件夾中的 main 函數啟動 caddy 服務器。實際運行的是 run.go 中的文件,這是方便測試使用<br />看 main.go 的代碼<br />

<a name="F1ozR"></a>
啟動流程
啟動 caddy 的流程畫了張圖

<br />見到不認識的不用擔心,查看上文的目錄結構可以找到他們大概的位置,下文會詳細講解。
可以在此圖中看到幾個重要的點 caddyfileLoader
這是加載 caddyfile 配置來啟動服務器的。<br />如果配置使用過 caddy ,配置的 caddyfile 就是在這里被 Loader
讀取后實例化服務器的。如果沒有使用過,大致說一下流程,使用 caddy 非常簡單,只需配置上文所說的 caddyfile 文件,按行配置選項,然后使用 caddy 運行讀取該配置文件即可。簡單示例就是以下的文本。<br />

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 />
<br />是不是很簡單,來一點更詳細的交互<br />
這里除了 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>
簡單看一下 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
另外可以看到 這里最重要的 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>
caddyfileInput
是 Input
類型,通常我們配置 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 即使是在進程中重載,也不會出現數據相互影響到情況,保持冪等。

<br />雖然 Instance 操作著眾多操作,但是我們卻不能從它講起,從農村包圍城市,漸漸了解 Instance 能調用的函數,自然 Instance 的功能就清晰了。
<a name="Js2Dd"></a>
Event
首先上圖:
<a name="zXi6Q"></a>
首先我們看到的是 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 文件
可以看到這里通過 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

這里我們來看,各個流程的終點 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

很容易發現,這里是通過 操作 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
就是 Plugin
的 SetupFunc
啦,來看看吧。
<a name="XarZm"></a>
Plugin
你會注意到,在目錄中有一個 叫 caddyhttp 的文件夾中的文件夾特別多,不用問,這就是 http 的可選 Plugin
啦
<a name="g4ohI"></a>
Overview
這里概覽了 Plugin
是如何注冊的。
可以在這里看到我們之前講解的很多的熟悉的概念,這是因為我們快要讀完 caddy 的架構了,剩下的實際上是具體的 Plugin
的各種擴展實現了。<br />可以看到,Plugin
是注冊在不同的 服務器類型 serverType
下的,實際上是在兩重 map 映射的結構中,圖中可以看出,然后是 Action
,最近的上文才說明了它,用它來進行 Plugin
的安裝。<br />然后來到 Controller
,實際進行配置的家伙,看到了之前所說的 Dispenser
和 Token
配置,還記得嗎,他們在剛才的詞法分析里才出現過。
接下來我們看一個 HTTP
的 Plugin
的例子 errors
的實現
<a name="ACP7a"></a>
caddyHTTP
<a name="z5G1t"></a>
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 值得注意的兩個,<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 />
謝謝閱讀,如果有不對的地方歡迎指正。