Golang Go语言中 Caddy 源码阅读(三)Caddyfile 解析 by Loader & Parser

发布于 1周前 作者 bupafengyu 来自 Go语言

Golang Go语言中 Caddy 源码阅读(三)Caddyfile 解析 by Loader & Parser

Caddy 源码阅读(三) Caddyfile 解析 by Loader & Parser

Preview

前文提到了 Caddy 的启动流程 和 Run 函数,现在我们顺着启动流程的第一步,读取 Caddyfile 的 源码阅读开始。
image.png

Loader

概念

可以理解为读取器。

在 caddy 中 由 Loader 执行 装载 配置文件的职能。看一下它的工作流程。

这个图来源于 plugin.go 文件

caddy-loader.svg

可以看到这里通过 Loader 解耦了 caddyfile 文件的读取,所以把它放在了 plugin.go 文件中,作为一个插件注册在 caddy app 中。

  • 可以多定义 caddyfileloader 在图中由三个长方形的表示为数组
  • 自定义的 caddyfileloader 会实现 右半部分的 Loader 接口
  • caddyfileloader 会有对应读取的 ServerType 的 name
  • 最后生产出的是 Input

下面我们看它的实现

保存

在 plugin.go 中有一个全局变量保存

// caddyfileLoaders is the list of all Caddyfile loaders
// in registration order.
caddyfileLoaders []caddyfileLoader

设计

我们看这个接口

type Loader interface {
	Load(serverType string) (Input, error)
}

这是用来 load  caddyfile 的 你可以为自己的所有独有的 server 自定义读取 caddyfile 的方式

Usage

我们自定义的时候,只需要实现一个  LoaderFunc 即可。因为它使用了 类似 http.HandlerFunc 的模式,帮你自动实现了上文所说的 Loader 接口

// LoaderFunc is a convenience type similar to http.HandlerFunc
// that allows you to use a plain function as a Load() method.
type LoaderFunc func(serverType string) (Input, error)

// Load loads a Caddyfile. func (lf LoaderFunc) Load(serverType string) (Input, error) { return lf(serverType) }

扩展

两个方法,

  1. 可以注册新的 Loader
  2. 或者设置为默认的 Loader
// RegisterCaddyfileLoader registers loader named name.
func RegisterCaddyfileLoader(name string, loader Loader) {
	caddyfileLoaders = append(caddyfileLoaders, caddyfileLoader{name: name, loader: loader})
}

// SetDefaultCaddyfileLoader registers loader by name // as the default Caddyfile loader if no others produce // a Caddyfile. If another Caddyfile loader has already // been set as the default, this replaces it. // // Do not call RegisterCaddyfileLoader on the same // loader; that would be redundant. func SetDefaultCaddyfileLoader(name string, loader Loader) { defaultCaddyfileLoader = caddyfileLoader{name: name, loader: loader} }

Logic

// loadCaddyfileInput iterates the registered Caddyfile loaders
// and, if needed, calls the default loader, to load a Caddyfile.
// It is an error if any of the loaders return an error or if
// more than one loader returns a Caddyfile.
func loadCaddyfileInput(serverType string) (Input, error) {
	var loadedBy string
	var caddyfileToUse Input
	for _, l := range caddyfileLoaders {
		cdyfile, err := l.loader.Load(serverType)
		if err != nil {
			return nil, fmt.Errorf("loading Caddyfile via %s: %v", l.name, err)
		}
		if cdyfile != nil {
			if caddyfileToUse != nil {
				return nil, fmt.Errorf("Caddyfile loaded multiple times; first by %s, then by %s", loadedBy, l.name)
			}
			loaderUsed = l
			caddyfileToUse = cdyfile
			loadedBy = l.name
		}
	}
	if caddyfileToUse == nil && defaultCaddyfileLoader.loader != nil {
		cdyfile, err := defaultCaddyfileLoader.loader.Load(serverType)
		if err != nil {
			return nil, err
		}
		if cdyfile != nil {
			loaderUsed = defaultCaddyfileLoader
			caddyfileToUse = cdyfile
		}
	}
	return caddyfileToUse, nil
}

很轻松的看到,装载 caddyfile 的逻辑 是尝试调用 所有的 Loader 并且重复装载会报错。
如果尝试过之后装载失败,则使用 默认的 defaultCaddyfileLoader 
这里可以看到最终流程是 caddyfile -> caddy.Input 那么这个 Input 是什么呢?

实际上 Input 就是 caddyfile 在代码中的映射。可以理解为,caddyfile 转化为了 Input 给 caddy 读取。

谁来读取它呢?
那么干活的主角登场啦!

Parser

文件结构

我们想看到各个流程中的 Token 是如何被分析出来的,需要知道,这里的 Token 代表着 caddyfile 中的每行选项配置

lexer.go

一个实用工具,它可以从 Reader获取一个token接一个token的值。通过 next()函数
token 是一个单词,token由空格分隔。如果一个单词包含空格,可以用引号括起来。
lexer 用来做 caddyfile 的语法分析 被其他程序调用

词法分析

next()函数就是 lexer 用来提供功能的函数

// 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)
}

}

他会根据 caddyfile 定义的写法,进行多种判断来实现分词

理解了 next 函数,就很容易知道如何分析一块选项的 token 了,不过都是 next() 的包装函数罢了。
这就是 lexer.go 中的解读,接下来我们看 parse.go

Parser.go

serverBlock

实际上,ServerBlock 存储的是 一组 token 信息,

//ServerBlock associates any number of keys 
//(usually addresses of some sort) with tokens 
//(grouped by directive name).
type ServerBlock struct {
    Keys []string
    Tokens map[string][]Token
}

它包含的 Token 正是 Parser 在 Caddyfile 中得来的。

context 还负责生成 caddy 管理的 Server,用来提供供 caddy Start 的信息
注意:这里的 Server 是 TCPServer 和 UDPServer,用来 Listen 等操作的。

parse

在 parser.go 中,由 Parse 函数进行生成 ServerBlock 的操作。

// Parse parses the input just enough to group tokens, in
// order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
// Directives that do not appear in validDirectives will cause
// an error. If you do not want to check for valid directives,
// pass in nil instead.
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
	p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
	return p.parseAll()
}

这里使用的 Dispenser,是令牌分发器,我们下面马上讨论

allTokens

在 praser.go 中使用 allTokens 进行 lexer 分词生成 token

// 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

allTokens 会在 新建 Dispenser 的时候调用

// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
	tokens, _ := allTokens(input) // ignoring error because nothing to do with it
	return Dispenser{
		filename: filename,
		tokens:   tokens,
		cursor:   -1,
	}
}

如此,整个解析流程就串起来了, lexer 负责词法分析,Parse 用于将一组 tokens 分类(因为很多的插件的设置稍微复杂一些),Dispenser 负责分析去的 tokens 令牌消费

我们看一下纵览

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

Dispenser 的关键是 把不同的 tokens 转换成 Directives 命令来执行

dispenser 中由 cursor 来进行 Token 数组中的迭代

关键在于移动 cursor 索引的函数

需要注意到的是 Dispenser 中的函数实际上都是 获取 tokens 的函数,意思是,在 Dispenser 中不会做任何配置,而是在相应的 controller.go ,caddy.go 中使用
请看下集,Plugin 的逻辑和相应的配置如何更改


更多关于Golang Go语言中 Caddy 源码阅读(三)Caddyfile 解析 by Loader & Parser的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang Go语言中 Caddy 源码阅读(三)Caddyfile 解析 by Loader & Parser的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中,Caddy作为一个灵活且强大的Web服务器,其配置文件的解析机制是其核心功能之一。Caddyfile作为Caddy的主要配置文件格式,通过Loader和Parser两大组件实现了高效且易读的配置体验。

Caddyfile的Loader组件主要负责加载配置文件的内容。它可以从文件、标准输入或其他输入源读取配置信息,并将其转换为Caddy可以处理的内部表示形式。Loader的设计充分考虑了灵活性和易用性,使得用户可以通过多种方式提供配置信息。

而Parser组件则负责解析Loader加载的配置内容。它将Caddyfile中的文本解析为Caddy能够理解和执行的指令。Parser的设计非常注重配置的层次结构和指令的语法规则,以确保配置的正确性和可读性。

在Caddyfile的解析过程中,Loader和Parser紧密协作。Loader提供原始的配置数据,而Parser则根据Caddy的语法规则对其进行解析和转换。这种分工使得Caddy能够高效地处理复杂的配置文件,同时保持了配置的灵活性和可读性。

此外,Caddy的Caddyfile解析机制还支持多种扩展和自定义功能。用户可以通过编写自定义的插件或修改现有的配置指令来扩展Caddy的功能,从而满足各种特定的需求。

总的来说,Caddy的Caddyfile解析机制是Caddy强大功能的重要组成部分。通过Loader和Parser的紧密协作,Caddy能够高效地处理复杂的配置文件,为用户提供了灵活、易用的配置体验。

回到顶部