Caddy 源碼閱讀【原創】

Caddy

Caddy 是一個go編寫的輕量配置化web server。
類似於nginx。有豐富的插件,配置也很簡單,自定義插件也很容易。更人性化。
官網上是這麼介紹的:Caddy is the HTTP/2 web server with automatic HTTPS.
(說實話官網v1版本的介紹並不怎麼清楚,反而是v2版本的介紹更明確)

Caddy官方文檔: https://caddyserver.com/v1/tutorial
GitHub地址: https://github.com/caddyserver/caddy

Caddy的功能

官方首頁盜來的圖
TLS證書的續訂 : TLS certificate renewal。
Caddy可以通過ACME協議(Let’s Encrypt 爲了實現自動化證書管理,制訂了 ACME 協議)和Let’s Encrypt(一個免費、開放、自動化的數字證書認證機構)進行證書的簽發、續訂等。這也是官方介紹的automatic HTTPS.
(這個功能嘗試了一次因爲網絡timeout,就沒有繼續研究。等有需求的時候,再嘗試吧。)

OCSP裝訂: OCSP Staplin。
在線證書狀態協議(Online Certificate Status )Protocol),簡稱 OCSP,是一個用於獲取 X.509 數字證書撤銷狀態的網際協議。 Web 服務端將主動獲取 OCSP 查詢結果,並隨證書一起發送給客戶端,以此讓客戶端跳過自己去尋求驗證的過程,提高 TLS 握手效率。

靜態文件服務器: static file serving。
這個可以用來進行文件管理、文件上傳、基於 MarkDown 的博客系統等等。只需簡單的配置。附鏈接:Caddy服務器搭建和實現文件共享
(這個值得嘗試,之後打算用caddy搭建一個MarkDown的博客玩玩)

反向代理
這個和nginx的功能一樣。什麼叫反向代理,什麼叫正向代理,請看我的這篇博客:反向代理和正向代理

Kubernetes入口 : Kubernetes Ingress.
k8s集羣的網絡入口。這個是要和traefik搶工作啊。(還是習慣使用treafik,不打算嘗試caddy)。

自定義中間件
這個可nginx可以寫lua插件一樣,caddy也支持寫插件。是用golang寫,得益於caddy代碼結構的組織,在caddy源碼基礎上擴展很容易。(TODO: 下一篇文章寫怎麼給caddy添加插件)

自定義服務器(ServerType)
Caddy本身是一個http服務器(const serverType = "http") ,但是通過擴展ServerType可以變成 SSH、SFTP、TCP等等,教科書一樣的典範是DNS服務器CoreDNS

Caddy代碼目錄

下面是caddy項目源碼的目錄,去除了相關文檔文件、test文件等。

directives 指令: 比如log、limits、proxy都是指令。

.
├── access.log          // 訪問日誌
├── assets.go           // 工具方法,用來獲取環境變量CADDYPATH和用戶目錄的路徑
├── caddy
│   ├── caddymain
│   │   ├── run.go      // caddy服務啓動入口,解析參數、日誌輸出、讀取Caddyfile\設置cpu等等。
│   ├── main.go         // 程序入口, main函數
├── caddy.go            // 定義了caddy服務器相關概念和接口     
├── caddyfile           // caddy的配置文件caddyfile的解析和使用
│   ├── dispenser.go    // 定義了一系列方便使用配置裏Token的方法,如Next、NextBlock、NextLine等
│   ├── json.go         // caddyfile同樣支持json,兩種形式可以用caddy相互轉換
│   ├── lexer.go        // 詞法分析器
│   ├── parse.go        // 讀入配置文件並使用lexer進行解析
├── caddyhttp           // caddy的http服務器
│   ├── caddyhttp.go    // 用來加載插件,import了所有caddy http服務器相關的指令(中間件)
│   ├── httpserver
│   │   ├── error.go    // 定義了常見的錯誤,都實現了error接口
│   │   ├── https.go    // 處理https相關邏輯
│   │   ├── logger.go   // 日誌相關邏輯
│   │   ├── plugin.go   // httpserver插件邏輯,定義了一個directives的字符串slice,自定義插件時,這裏要改!!
│   │   ├── server.go   // HTTP server的實現,包裹了一層標準庫的http.Server
│   │   ├── ...
│   ├── bind
│   ├── browse
│   ├── errors
│   ├── basicauth
│   ├── ...
├── caddytls             // caddy tls 相關邏輯,不影響主要流程先不看。
├── commands.go          // 命令行終端命令的處理邏輯,處理終端執行的時候加入的參數。
├── controller.go        // 用於從配置caddyfile中的配置來設置directive
├── onevent              // 插件,on在觸發指定事件時執行命令。舉個栗子:在服務器啓動時,啓動php-fpm。
├── plugins.go           // 維護了caddy的所有插件,event hook等
├── rlimit_nonposix.go
├── rlimit_posix.go      // 啓動服務器的時候,如果文件句柄數限制過低就提醒你設置ulimits
├── sigtrap.go           // 信號機關,用來處理信號,如中斷、掛起等。
├── sigtrap_nonposix.go
├── sigtrap_posix.go
├── telemetry            // 遙測,就是監控。個人覺得使用prometheus插件做更好。
└── upgrade.go           // 優雅重啓

Caddy 啓動過程

main 入口

caddy/main.go

package main

import "github.com/caddyserver/caddy/caddy/caddymain"

var run = caddymain.Run // replaced for tests

func main() {
	run()
}

main函數很簡單,調用了caddy/caddymain/run.go的run函數

run方法
package caddymain

import (
	...

	_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
	// This is where other plugins get plugged in (imported)
)

const appName = "Caddy"

// Flags that control program flow or startup
var (
	serverType      string
	conf            string
	cpu             string
	envFile         string
	...
)

// EnableTelemetry defines whether telemetry is enabled in Run.
var EnableTelemetry = true

func init() {...}

// Run is Caddy's main() function.
func Run() {...}

根據執行順序來看,main包import了caddymain這個包,且調用了caddymain.Run。那麼執行步驟如下:

  1. import caddymain包的相關依賴,這裏主要看import卻沒有使用的caddyhttp
  2. 初始化caddymain包中的包內變量。
  3. 執行caddymain包的init函數。
  4. 調用Run函數。

run.go主要用處

package caddymain

import (
	...

	_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
	// This is where other plugins get plugged in (imported)
)

const appName = "Caddy"

// Flags that control program flow or startup
var (
	serverType      string
	conf            string
	cpu             string
	envFile         string
	fromJSON        bool
	logfile         string
	logTimestamps   bool
	logRollMB       int
	logRollCompress bool
	revoke          string
	toJSON          bool
	version         bool
	plugins         bool
	printEnv        bool
	validate        bool
	disabledMetrics string
)

// EnableTelemetry defines whether telemetry is enabled in Run.
var EnableTelemetry = true

func init() {
	caddy.TrapSignals()
	...
	flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
	flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
	...
	// Run is Caddy's main() function.
func Run() {
	flag.Parse()

	// Set up process log before anything bad happens
	switch logfile {
	case "stdout":
		log.SetOutput(os.Stdout)
	case "stderr":
		log.SetOutput(os.Stderr)
	case "":
	    ...
	}

	// load all additional envs as soon as possible
	if err := LoadEnvFromFile(envFile); err != nil {
		mustLogFatalf("%v", err)
	}

	if printEnv {
		for _, v := range os.Environ() {
			fmt.Println(v)
		}
	}

	// initialize telemetry client
	if EnableTelemetry {
		err := initTelemetry()
		...
	}

	// Check for one-time actions
	if revoke != "" {
		err := caddytls.Revoke(revoke)
		...
	}
	if version {
		...
	}
	if plugins {
		fmt.Println(caddy.DescribePlugins())
		os.Exit(0)
	}

	// Check if we just need to do a Caddyfile Convert and exit
	checkJSONCaddyfile()

	// Set CPU cap
	err := setCPU(cpu)
	if err != nil {
		mustLogFatalf("%v", err)
	}

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

	// Get Caddyfile input
	caddyfileinput, err := caddy.LoadCaddyfile(serverType)
	if err != nil {
		mustLogFatalf("%v", err)
	}

	if validate {
		err := caddy.ValidateAndExecuteDirectives(caddyfileinput, nil, true)
		if err != nil {
			mustLogFatalf("%v", err)
		}
		msg := "Caddyfile is valid"
		fmt.Println(msg)
		log.Printf("[INFO] %s", msg)
		os.Exit(0)
	}

	// Log Caddy version before start
	log.Printf("[INFO] Caddy version: %s", module.Version)

	// Start your engines
	instance, err := caddy.Start(caddyfileinput)
	if err != nil {
		mustLogFatalf("%v", err)
	}

	// Begin telemetry (these are no-ops if telemetry disabled)
	telemetry.Set("caddy_version", module.Version)
	...
	telemetry.Set("cpu", struct {
		BrandName  string `json:"brand_name,omitempty"`
		NumLogical int    `json:"num_logical,omitempty"`
		AESNI      bool   `json:"aes_ni,omitempty"`
	}{
		BrandName:  cpuid.CPU.BrandName,
		NumLogical: runtime.NumCPU(),
		AESNI:      cpuid.CPU.AesNi(),
	})
	if containerized := detectContainer(); containerized {
		telemetry.Set("container", containerized)
	}
	telemetry.StartEmitting()

	// Twiddle your thumbs
	instance.Wait()
}
	
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章