Caddy 源碼全解析

Caddy 源碼全解析

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

Preface

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

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

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

Overview-CaddyMain

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

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

Package

這是 caddy 包的結構<br />image.pngimage.png服務器

首先咱們從一切的開始講起,即平時咱們程序運行的 main.go 函數。<br />這是 上圖 caddy 文件夾下的目錄結構。 在 caddy 文件夾中的 main 函數啓動 caddy 服務器。實際運行的是 run.go 中的文件,這是方便測試使用<br />看 main.go 的代碼<br />image.png<br />經過改變 run 變量的值來方便測試,能夠學習一下。架構

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

啓動流程

啓動 caddy 的流程畫了張圖app

屏幕快照 2019-08-04 下午6.34.21.png<br />見到不認識的不用擔憂,查看上文的目錄結構能夠找到他們大概的位置,下文會詳細講解。

能夠在此圖中看到幾個重要的點 caddyfileLoader這是加載 caddyfile 配置來啓動服務器的。<br />若是配置使用過 caddy ,配置的 caddyfile 就是在這裏被 Loader 讀取後實例化服務器的。若是沒有使用過,大體說一下流程,使用 caddy 很是簡單,只需配置上文所說的 caddyfile 文件,按行配置選項,而後使用 caddy 運行讀取該配置文件便可。簡單示例就是如下的文本。<br />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<br />是否是很簡單,來一點更詳細的交互<br />caddy.Start.svg

這裏除了 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>

caddy-server-interface.svg

簡單看一下 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 生成不一樣的 servercaddy-serverType.svg

另外能夠看到 這裏最重要的 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<br />雖然 Instance 操做着衆多操做,可是咱們卻不能從它講起,從農村包圍城市,漸漸瞭解 Instance 能調用的函數,天然 Instance 的功能就清晰了。

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

Event

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

caddy-event.svg

首先咱們看到的是 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 文件

<a name="QX05p"></a>

caddy-loader.svg

能夠看到這裏經過 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 就是 PluginSetupFunc啦,來看看吧。

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

Plugin

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

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

Overview

這裏概覽了 Plugin 是如何註冊的。 <a name="SEj4t"></a>

caddy-plugin.svg

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

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

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

caddyHTTP

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

errors

<a name="aaSgV"></a>

overviewcaddy-http-plugin-overview.svg

這裏咱們從下看,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 />caddy-http-error.svg

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

相關文章
相關標籤/搜索