夏溪辰的博客

xiaxichen's blog

Caddy 源码分析 Plugin & Controller 安装插件

2023-12-03

Caddy 源码分析 Plugin & Controller 安装插件

Caddy 在日常软件开发中,Controller 是 MVC 的 逻辑处理部分,在 Caddy 中,它意味这 Plugin 的安装逻辑。

Caddy 自身提供了一个 总的可扩展的房子一样的框架,Controller 把不同的 Plugin 像积木一样搭建成一个服务器程序提供服务。

Context

80F5F012-F4D9-4B2E-B4B7-31C10A30E86A.png

前文提到, Caddyfile 通过 Loader ,lexer 等进行读取。

读取出来的 token directives 最终是用来配置 Plugin 的,

在本文中会详细探索 Plugin 如何读取配置安装,Controller 是完成这一行为的执行者。

同时探索 Plugin 的设计是如何解耦 Caddy 的 Plugin 开发者和 Caddy 的 maintainer 的。

Plugin 插件的巧妙设计解耦扩展

变量

在 caddy/plugin.go 中保存着整个 caddy 的插件,当使用插件的时候,导入某个插件包即可。在包里的 init 函数会帮助你注册好 插件

// plugins is a map of server type to map of plugin name to
// Plugin. These are the "general" plugins that may or may
// not be associated with a specific server type. If it's
// applicable to multiple server types or the server type is
// irrelevant, the key is empty string (""). But all plugins
// must have a name.
plugins = make(map[string]map[string]Plugin)

这里使用的 两层 map 第一层是 ServerType 的 name ,第二层是 plugin 的名字,如果第一个为空,说明这个插件所有的 ServerType 都可以使用

注册

使用 caddy.RegisterPlugin() 注册,dup 是 duplicate 的简写

// RegisterPlugin plugs in plugin. All plugins should register
// themselves, even if they do not perform an action associated
// with a directive. It is important for the process to know
// which plugins are available.
//
// The plugin MUST have a name: lower case and one word.
// If this plugin has an action, it must be the name of
// the directive that invokes it. A name is always required
// and must be unique for the server type.
func RegisterPlugin(name string, plugin Plugin) {
    if name == "" {
        panic("plugin must have a name")
    }
    if _, ok := plugins[plugin.ServerType]; !ok {
        plugins[plugin.ServerType] = make(map[string]Plugin)
    }
    if _, dup := plugins[plugin.ServerType][name]; dup {
        panic("plugin named " + name + " already registered for server type " + plugin.ServerType)
    }
    plugins[plugin.ServerType][name] = plugin
}

实现

逻辑解耦使得我们实现 Plugin 只需要关注如何将我们的 Plugin 注册到 Caddy Server 中了。

如何实现 一个 Plugin 呢

Overview

这里概览了 Plugin 是如何注册的。

4A91F390-44F1-4368-B5F7-D9018DBD03A3.jpg

可以在这里看到我们之前讲解的很多的熟悉的概念,这是因为我们快要读完 caddy 的架构了,剩下的是具体的 Plugin 的各种扩展实现了。

可以看到,Plugin 是注册在不同的 服务器类型 serverType 下的,是在两重 map 映射的结构中,图中可以看出,然后是 Action ,最近的上文才说明了它,用它来进行 Plugin 的安装。

然后来到 Controller ,实际进行配置的家伙,看到了之前所说的 Dispenser 和 Token 配置

还记得吗,他们在刚才的词法分析里才出现过。

你会注意到,在目录中有一个 叫 caddyhttp 的文件夹中的文件夹特别多, 这就是 一系列 http 服务器 的 Plugin 实现 接下来我们看一个 HTTP 的 Plugin 的例子 errors 的实现

caddyHTTP

errors

overview

7E488040-B6EC-4328-84C6-60E5C3DB69A2.jpg

这里我们从下看,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 安装的全流程。

这里还有一个关于 caddy.Controller 到 ErrorHandler 的一个转换 通过 errorsParse 函数

317DC9B3-4B45-4777-B3CC-9ADF4C2B1121.jpg

DirectiveAction