1. 程式人生 > 程式設計 >Caddy 原始碼全解析

Caddy 原始碼全解析

Caddy 原始碼全解析

Preface

Caddy 是 Go 語言構建的輕量配置化伺服器。同時程式碼結構由於 Go 語言的輕便簡潔,比較易讀,推薦學弟學妹學習 Go 的時候也去檢視追一下它的原始碼。不用怕相信這篇文章能給你很大的信心。

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

Overview-CaddyMain

當然,建議看這篇文章的時候,檢視上手一下 Caddy 的實際配置操作應用,對理解原始碼會有好處,如果沒有操作過也沒有關係。

Package

這是 caddy 包的結構

image.png
image.png

首先我們從一切的開始講起,即平時我們程式執行的 main.go 函式。
這是 上圖 caddy 資料夾下的目錄結構。

在 caddy 資料夾中的 main 函式啟動 caddy 伺服器。實際執行的是 run.go 中的檔案,這是方便測試使用
main.go 的程式碼
image.png

通過改變 run 變數的值來方便測試,可以學習一下。

啟動流程

啟動 caddy 的流程畫了張圖

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

見到不認識的不用擔心,檢視上文的目錄結構可以找到他們大概的位置,下文會詳細講解。

可以在此圖中看到幾個重要的點 caddyfileLoader這是載入 caddyfile 配置來啟動伺服器的。
如果配置使用過 caddy ,配置的 caddyfile 就是在這裡被 Loader 讀取後例項化伺服器的。如果沒有使用過,大致說一下流程,使用 caddy 非常簡單,只需配置上文所說的 caddyfile 檔案,按行配置選項,然後使用 caddy 執行讀取該配置檔案即可。簡單示例就是以下的文字。

image.png

Instance 是執行操作的例項,可以看到幾個主要的操作都是在他身上

Server 可以看到擁有 TCP UDP 兩個 Server 的介面。

我們首先關心的是 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()

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

螢幕快照 2019-08-04 下午6.35.03.png

是不是很簡單,來一點更詳細的互動
caddy.Start.svg

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

如果不理解,首先記住 caddy 是配置化的伺服器,
通過 caddyfile 配置 ->
那麼肯定要讀取它啦 ->
然後要解析它配置的到底是那些東西 ->
之後呢,就要讓配置的目標做到 caddyfile 中宣告的更改。
記住這個流程繼續看幾遍就能理解了。

Server

在 caddy.go 中定義著 Server 的介面,同時實現了優雅的退出。我們首先看圖瞭解組織結構

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
複製程式碼

ServerType

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

caddy-serverType.svg

另外可以看到 這裡最重要的 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 即使是在程式中過載,也不會出現資料相互影響到情況,保持冪等

螢幕快照 2019-08-04 下午6.34.33.png

雖然 Instance 操作著眾多操作,但是我們卻不能從它講起,從農村包圍城市,漸漸瞭解 Instance 能呼叫的函式,自然 Instance 的功能就清晰了。

Event

首先上圖:

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
複製程式碼


然後我們關注到如何註冊,和圖中的 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 檔案

caddy-loader.svg

可以看到這裡通過 Loader 解耦了 caddyfile 檔案的讀取,所以把它放在了 plugin.go 檔案中,作為一個外掛註冊在 caddy app 中。
這裡可以看到最終流程是 name -> caddy.Input 那麼這個 Input 是什麼呢?
實際上 Input 就是 caddyfile 在程式碼中的對映。可以理解為,caddyfile 轉化為了 Input 給 caddy 讀取。誰來讀取它呢?
那麼幹活的主角登場啦!

Parser

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

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

螢幕快照 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啦,來看看吧。

Plugin

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

Overview

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

caddy-plugin.svg

可以在這裡看到我們之前講解的很多的熟悉的概念,這是因為我們快要讀完 caddy 的架構了,剩下的實際上是具體的 Plugin 的各種擴充套件實現了。
可以看到,Plugin 是註冊在不同的 伺服器型別 serverType 下的,實際上是在兩重 map 對映的結構中,圖中可以看出,然後是 Action ,最近的上文才說明瞭它,用它來進行 Plugin 的安裝。
然後來到 Controller ,實際進行配置的傢伙,看到了之前所說的 DispenserToken 配置,還記得嗎,他們在剛才的詞法分析裡才出現過。

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

caddyHTTP

errors

overview
caddy-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 值得注意的兩個,
一個是他們會實現 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 函式

caddy-http-error.svg

謝謝閱讀,如果有不對的地方歡迎指正。