Caddy源码全解析-《GO开发知识笔记》

admin 2025-11-04 01:07:44 编程 来源:ZONE.CI 全球网 0 阅读模式
  • Preface
  • Overview-CaddyMain
    • Package
      • 启动流程
    • 启动服务器
      • caddy.Start()
  • Server
    • ServerType
  • Instance
    • struct">struct
      • serverType 代表这个实例的服务器类型,通常是 HTTP
      • caddyfileInput 是 Input 类型,通常我们配置 caddy 服务器的时候,就是通过编辑 caddyfileInput 的文本实现的修改配置行动。值得注意的是,生成 Instance 的参数同样是 caddyfile,这里的 caddyfile 在程序中是一个接口,一会儿继续讲解
      • wg 是用来等待所有 servers 执行他们操作的信号量。
      • context 是实例 Instance的上下文,其中包含 serverType 信息和服务器配置管理状态的信息。
      • servers 是一组 server 和 他们的 listeners,两种 Server TCP/UDP,即 serverType ,两种不同的 serverType 会对应不同的 caddyfile中的选项。
      • OnXXX 等 6 个函数是一系列回调函数,通过名字能够看出在什么时候回调触发。
      • 12-factor 中的 第九条 Disposability 相符合。意思是每一次重载实例 Instance 即使是在进程中重载,也不会出现数据相互影响到情况,保持幂等。">Storage 是存储数据的地方,本来可以设计在 全局状态中,但是设计在这里更好,考虑到垃圾回收机制,进程中重新加载时,旧的 Instance be destroyed 之后,会变成垃圾,收集。这和 12-factor 中的 第九条 Disposability 相符合。意思是每一次重载实例 Instance 即使是在进程中重载,也不会出现数据相互影响到情况,保持幂等。
  • Event
    • 注册与分发
      • 注册 EventHook
      • EmitEvent">分发 EmitEvent
  • Loader
    • Parser">Parser
    • ">Caddy 源码全解析 - 图1
      • 词法分析
    • excuteDirective">excuteDirective
    • DirectiveAction
    • ">Caddy 源码全解析 - 图2
  • Plugin
    • Overview
    • caddyHTTP
      • errors
    • Directives

    Preface

    Caddy 是 Go 语言构建的轻量配置化服务器。同时代码结构由于 Go 语言的轻便简洁,比较易读,推荐学弟学妹学习 Go 的时候也去查看追一下它的源码。不用怕相信这篇文章能给你很大的信心。可能会有点多,建议多看几遍。

    Overview-CaddyMain

    当然,建议看这篇文章的时候,查看上手一下 Caddy 的实际配置操作应用,对理解源码会有好处,如果没有操作过也没有关系。

    Package

    这是 caddy 包的结构Caddy 源码全解析 - 图3Caddy 源码全解析 - 图4首先我们从一切的开始讲起,即平时我们程序运行的 main.go 函数。这是 上图 caddy 文件夹下的目录结构。Caddy 源码全解析 - 图5在 caddy 文件夹中的 main 函数启动 caddy 服务器。实际运行的是 run.go 中的文件,这是方便测试使用看 main.go的代码Caddy 源码全解析 - 图6通过改变 run 变量的值来方便测试,可以学习一下。

    启动流程

    启动 caddy 的流程画了张图Caddy 源码全解析 - 图7见到不认识的不用担心,查看上文的目录结构可以找到他们大概的位置,下文会详细讲解。可以在此图中看到几个重要的点 caddyfileLoader这是加载 caddyfile 配置来启动服务器的。如果配置使用过 caddy ,配置的 caddyfile 就是在这里被 Loader 读取后实例化服务器的。如果没有使用过,大致说一下流程,使用 caddy 非常简单,只需配置上文所说的 caddyfile 文件,按行配置选项,然后使用 caddy 运行读取该配置文件即可。简单示例就是以下的文本。Caddy 源码全解析 - 图8Instance 是运行操作的实例,可以看到几个主要的操作都是在他身上Server 可以看到拥有 TCP UDP 两个 Server 的接口。我们首先关心的是 Start() 启动服务器。

    启动服务器

    发送 StartupEvent, 参照下文中 Event 理解

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

    读取配置文件:

    1. caddyfileinput, err := caddy.LoadCaddyfile(serverType)

    启动:

    1. instance, err := caddy.Start(caddyfileinput)

    发送 InstanceStartupEvent

    1. caddy.EmitEvent(caddy.InstanceStartupEvent, instance

    caddy.Start()

    阅读完代码,画一张图帮助理解Caddy 源码全解析 - 图9是不是很简单,来一点更详细的交互Caddy 源码全解析 - 图10这里除了 Instance 之外还有两个新名词Controller:它是用来帮助 Directives 设置它自身的,通过读取 Token,这里的 Directives 实际上对应的就是上文所说的 caddyfile 中的配置文件选项。这一点请参照下文中 Loader 下的 excuteDirective 理解。Token :是 caddy 自己的 词法分析器 解析 caddyfile 配置文件出的选项的标记。这一点请参照下文中 Loader 中的 Parser 理解如果不理解,首先记住 caddy 是配置化的服务器,通过 caddyfile 配置 ->那么肯定要读取它啦 ->然后要解析它配置的到底是那些东西 ->之后呢,就要让配置的目标做到 caddyfile 中声明的更改。记住这个流程继续看几遍就能理解了。

    Server

    在 caddy.go 中定义着 Server 的接口,同时实现了优雅的退出。我们首先看图了解组织结构Caddy 源码全解析 - 图11简单看一下 Stopper 的接口

    1. // Stopper is a type that can stop serving. The stop
    2. // does not necessarily have to be graceful.
    3. type Stopper interface {
    4. // Stop stops the server. It blocks until the
    5. // server is completely stopped.
    6. Stop() error
    7. }

    GracefulServer 包含 Stopper 的接口实现了优雅退出,这是拦截了 系统 signal 的信号之后执行的结果,意在意外中断的时候保存好需要保存的东西。它同时包含着 WrapListener 函数。可以看出,他用来做中间件。

    1. // WrapListener wraps a listener with the
    2. // listener middlewares configured for this
    3. // server, if any.
    4. WrapListener(net.Listener) net.Listener

    ServerType

    最后看到不同 serverType 生成不同的 serverCaddy 源码全解析 - 图12另外可以看到 这里最重要的 Instance 下面我们进一步查看 Instance 的代码

    Instance

    instance 是 Server 用来执行操作的实体。首先来看他的结构。它的代码在 主文件夹中的 caddy.go 中首先我们看一下 它的结构了解下它可能有的功能

    struct

    1. type Instance struct {
    2. serverType string
    3. caddyfileInput Input
    4. wg *sync.WaitGroup
    5. context Context
    6. servers []ServerListener
    7. OnFirstStartup []func() error // starting, not as part of a restart
    8. OnStartup []func() error // starting, even as part of a restart
    9. OnRestart []func() error // before restart commences
    10. OnRestartFailed []func() error // if restart failed
    11. OnShutdown []func() error // stopping, even as part of a restart
    12. OnFinalShutdown []func() error // stopping, not as part of a restart
    13. Storage map[interface{}]interface{}
    14. StorageMu sync.RWMutex
    15. }

    serverType 代表这个实例的服务器类型,通常是 HTTP

    caddyfileInput 是 Input 类型,通常我们配置 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 即使是在进程中重载,也不会出现数据相互影响到情况,保持幂等。

    Caddy 源码全解析 - 图13虽然 Instance 操作着众多操作,但是我们却不能从它讲起,从农村包围城市,渐渐了解 Instance 能调用的函数,自然 Instance 的功能就清晰了。

    Event

    首先上图:Caddy 源码全解析 - 图14首先我们看到的是 eventHooks 这个结构,实际上他是存储 key:name value:EventHook 这样的一个 map[string]EventHook 的结构,只是从 sync 包中引入保证并发安全。

    1. eventHooks = &sync.Map{}

    然后是重要的 caddy.EventHook 结构。

    1. type EventHook func(eventType EventName, eventInfo interface{}) error

    然后我们关注到如何注册,和图中的 caddy.EmitEvent

    注册与分发

    注册 EventHook

    可以看到使用 eventHooks.LoadOrStore方法,不必赘述

    1. func RegisterEventHook(name string, hook EventHook){
    2. if name == "" {
    3. panic("event hook must have a name")
    4. }
    5. _, dup := eventHooks.LoadOrStore(name, hook)
    6. if dup {
    7. panic("hook named" + name + "already registered")
    8. }
    9. }

    分发 EmitEvent

    通过传入函数为参数调用回调函数

    1. // EmitEvent executes the different hooks passing the EventType as an
    2. // argument. This is a blocking function. Hook developers should
    3. // use 'go' keyword if they don't want to block Caddy.
    4. func EmitEvent(event EventName, info interface{}) {
    5. eventHooks.Range(func(k, v interface{}) bool {
    6. err := v.(EventHook)(event, info)
    7. if err != nil {
    8. log.Printf("error on '%s' hook: %v", k.(string), err)
    9. }
    10. return true //注意这里返回的是 true
    11. })
    12. }

    这里使用的 Range函数,实际上是把事件信息给每一个上述提过 map 中的 EventHook 提供参数进行回调执行,按顺序调用,但是如果 传入函数返回 false ,迭代遍历执行就会中断。可以知道,上文 Overview中启动服务器 所说的发送 caddy.StartupEvent 事件就是调用的caddy.EmitEvent(caddy.StartupEvent, nil) 讲到这,相信已经对大致的流程有了一点框架的概念。下面我们继续深入了解 在读取 caddyfile 文件的时候发生了什么。

    Loader

    自定义的配置文件都会有读取分析。在 caddy 中 由 Loader 执行这一项职能。首先我们看一下它的工作流程。这个图来源于 plugin.go 文件Caddy 源码全解析 - 图15可以看到这里通过 Loader 解耦了 caddyfile 文件的读取,所以把它放在了 plugin.go 文件中,作为一个插件注册在 caddy app 中。这里可以看到最终流程是 name -> caddy.Input 那么这个 Input 是什么呢?实际上 Input 就是 caddyfile 在代码中的映射。可以理解为,caddyfile 转化为了 Input 给 caddy 读取。谁来读取它呢?那么干活的主角登场啦!

    Parser

    Caddy 源码全解析 - 图16

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

    词法分析

    1. // allTokens lexes the entire input, but does not parse it.
    2. // It returns all the tokens from the input, unstructured
    3. // and in order.
    4. func allTokens(input io.Reader) ([]Token, error) {
    5. l := new(lexer)
    6. err := l.load(input)
    7. if err != nil {
    8. return nil, err
    9. }
    10. var tokens []Token
    11. for l.next() {
    12. tokens = append(tokens, l.token)
    13. }
    14. return tokens, nil
    15. }

    这里实际上关键在于 读取,可以看到在 dispenser 中由 cursor 来进行 Token 数组中的迭代关键在于移动 cursor 索引的函数next()

    1. // next loads the next token into the lexer.
    2. // A token is delimited by whitespace, unless
    3. // the token starts with a quotes character (")
    4. // in which case the token goes until the closing
    5. // quotes (the enclosing quotes are not included).
    6. // Inside quoted strings, quotes may be escaped
    7. // with a preceding character. No other chars
    8. // may be escaped. The rest of the line is skipped
    9. // if a "#" character is read in. Returns true if
    10. // a token was loaded; false otherwise.
    11. func (l *lexer) next() bool {
    12. var val []rune
    13. var comment, quoted, escaped bool
    14. makeToken := func() bool {
    15. l.token.Text = string(val)
    16. return true
    17. }
    18. for {
    19. ch, _, err := l.reader.ReadRune()
    20. if err != nil {
    21. if len(val) > 0 {
    22. return makeToken()
    23. }
    24. if err == io.EOF {
    25. return false
    26. }
    27. panic(err)
    28. }
    29. if quoted {
    30. if !escaped {
    31. if ch == '\' {
    32. escaped = true
    33. continue
    34. } else if ch == '"' {
    35. quoted = false
    36. return makeToken()
    37. }
    38. }
    39. if ch == '
    40. ' {
    41. l.line++
    42. }
    43. if escaped {
    44. // only escape quotes
    45. if ch != '"' {
    46. val = append(val, '\')
    47. }
    48. }
    49. val = append(val, ch)
    50. escaped = false
    51. continue
    52. }
    53. if unicode.IsSpace(ch) {
    54. if ch == '
    55. ' {
    56. continue
    57. }
    58. if ch == '
    59. ' {
    60. l.line++
    61. comment = false
    62. }
    63. if len(val) > 0 {
    64. return makeToken()
    65. }
    66. continue
    67. }
    68. if ch == '#' {
    69. comment = true
    70. }
    71. if comment {
    72. continue
    73. }
    74. if len(val) == 0 {
    75. l.token = Token{Line: l.line}
    76. if ch == '"' {
    77. quoted = true
    78. continue
    79. }
    80. }
    81. val = append(val, ch)
    82. }
    83. }

    理解了 next 函数,就很容易知道如何分析一块选项的 token 了,不过都是 next() 的包装函数罢了。

    excuteDirective

    1. func executeDirectives(inst *Instance, filename string,
    2. directives []string, sblocks []caddyfile.ServerBlock, justValidate bool) error {
    3. // map of server block ID to map of directive name to whatever.
    4. storages := make(map[int]map[string]interface{})
    5. // It is crucial that directives are executed in the proper order.
    6. // We loop with the directives on the outer loop so we execute
    7. // a directive for all server blocks before going to the next directive.
    8. // This is important mainly due to the parsing callbacks (below).
    9. for _, dir := range directives {
    10. for i, sb := range sblocks {
    11. var once sync.Once
    12. if _, ok := storages[i]; !ok {
    13. storages[i] = make(map[string]interface{})
    14. }
    15. for j, key := range sb.Keys {
    16. // Execute directive if it is in the server block
    17. if tokens, ok := sb.Tokens[dir]; ok {
    18. controller := &Controller{
    19. instance: inst,
    20. Key: key,
    21. Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
    22. OncePerServerBlock: func(f func() error) error {
    23. var err error
    24. once.Do(func() {
    25. err = f()
    26. })
    27. return err
    28. },
    29. ServerBlockIndex: i,
    30. ServerBlockKeyIndex: j,
    31. ServerBlockKeys: sb.Keys,
    32. ServerBlockStorage: storages[i][dir],
    33. }
    34. setup, err := DirectiveAction(inst.serverType, dir)
    35. if err != nil {
    36. return err
    37. }
    38. err = setup(controller)
    39. if err != nil {
    40. return err
    41. }
    42. storages[i][dir] = controller.ServerBlockStorage // persist for this server block
    43. }
    44. }
    45. }
    46. if !justValidate {
    47. // See if there are any callbacks to execute after this directive
    48. if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok {
    49. callbacks := allCallbacks[dir]
    50. for _, callback := range callbacks {
    51. if err := callback(inst.context); err != nil {
    52. return err
    53. }
    54. }
    55. }
    56. }
    57. }
    58. return nil
    59. }

    caddyfile 既然被解析完毕,那么就要开始执行配置更改了,这里实际上是 caddy.go 中的 函数,最后在 caddy 的 main.go 中调用来执行更改。

    DirectiveAction

    Caddy 源码全解析 - 图17

    很容易发现,这里是通过 操作 Controller 来实现的,此时可以再返回最上文查看上一次提到 Controller 的时候。

    1. // DirectiveAction gets the action for directive dir of
    2. // server type serverType.
    3. func DirectiveAction(serverType, dir string) (SetupFunc, error) {
    4. if stypePlugins, ok := plugins[serverType]; ok {
    5. if plugin, ok := stypePlugins[dir]; ok {
    6. return plugin.Action, nil
    7. }
    8. }
    9. if genericPlugins, ok := plugins[""]; ok {
    10. if plugin, ok := genericPlugins[dir]; ok {
    11. return plugin.Action, nil
    12. }
    13. }
    14. return nil, fmt.Errorf("no action found for directive '%s' with server type '%s' (missing a plugin?)",
    15. dir, serverType)
    16. }

    了解完这些,我们注意到有一个 叫做 Action 的东西,它又是怎么来的?别急,他就在 Plugin 包中。我们知道了,配置文件实际上是配置各种 plugin 作为插件安装在 caddy 服务器上,而 caddyfile 正是被转化为了 Token,Dispenser 来执行配置更改,即不同的插件安装。那么 Action 就是 Plugin 的 SetupFunc啦,来看看吧。

    Plugin

    你会注意到,在目录中有一个 叫 caddyhttp 的文件夹中的文件夹特别多,不用问,这就是 http 的可选 Plugin 啦

    Overview

    这里概览了 Plugin 是如何注册的。Caddy 源码全解析 - 图18可以在这里看到我们之前讲解的很多的熟悉的概念,这是因为我们快要读完 caddy 的架构了,剩下的实际上是具体的 Plugin 的各种扩展实现了。可以看到,Plugin 是注册在不同的 服务器类型 serverType 下的,实际上是在两重 map 映射的结构中,图中可以看出,然后是 Action ,最近的上文才说明了它,用它来进行 Plugin 的安装。然后来到 Controller ,实际进行配置的家伙,看到了之前所说的 Dispenser 和 Token 配置,还记得吗,他们在刚才的词法分析里才出现过。接下来我们看一个 HTTP 的 Plugin 的例子 errors 的实现

    caddyHTTP

    errors

    Caddy 源码全解析 - 图19这里我们从下看,caddy.Listener 定义在 caddy.go 中,用来支持 零停机时间加载。往上看到 Middleware 调用,我们来看看 errorsHandle 的结构

    1. // ErrorHandler handles HTTP errors (and errors from other middleware).
    2. type ErrorHandler struct {
    3. Next httpserver.Handler
    4. GenericErrorPage string // default error page filename
    5. ErrorPages map[int]string // map of status code to filename
    6. Log *httpserver.Logger
    7. Debug bool // if true, errors are written out to client rather than to a log
    8. }

    可以看到,Next 字段明显是 Chain 调用的下一个 Handler 处理。事实上,每一个 Plugin 或者算是 HTTP 服务中的中间件都有这个字段用于 构建链式调用。每一个 Plugin 值得注意的两个,一个是他们会实现 ServeHTTP 接口进行 HTTP 请求处理。

    1. func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
    2. defer h.recovery(w, r)
    3. status, err := h.Next.ServeHTTP(w, r)
    4. if err != nil {
    5. errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
    6. if h.Debug {
    7. // Write error to response instead of to log
    8. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    9. w.WriteHeader(status)
    10. fmt.Fprintln(w, errMsg)
    11. return 0, err // returning 0 signals that a response has been written
    12. }
    13. h.Log.Println(errMsg)
    14. }
    15. if status >= 400 {
    16. h.errorPage(w, r, status)
    17. return 0, err
    18. }
    19. return status, err
    20. }

    另一个是安装到 caddy 中的 setup.go 文件,我们看一下 Plugin 安装的全流程。

    Directives

    前面提到过很多次 Directives 这里做一个它的整个流程概览。上文中提到,这些注册实际上都是 Controller 执行的。下半部分是 关于 HTTP 的服务配置这里的重点在 errors.serup() 可以看到,它创建了 errors.ErrHandler 并注册到了 httpserver 的一对中间件中

    1. // setup configures a new errors middleware instance.
    2. func setup(c *caddy.Controller) error {
    3. handler, err := errorsParse(c)
    4. ···
    5. httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
    6. handler.Next = next
    7. return handler
    8. })
    9. return nil
    10. }

    实际上这里还有一个关于 caddy.Controller 到 ErrorHandler 的一个转换 通过 errorsParse 函数Caddy 源码全解析 - 图20谢谢阅读,如果有不对的地方欢迎指正。参考:https://www.cnblogs.com/abser/p/11337662.html参考:https://github.com/caddyserver/caddy

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  12