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
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项目源码的目录,去除了相关文档文件、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的exporter做更好。
└── upgrade.go // 热更新
caddy/main.go
package main
import "github.com/caddyserver/caddy/caddy/caddymain"
var run = caddymain.Run // replaced for tests
func main() {
run()
}
main函数很简单,引用了caddmain包,调用了caddy/caddymain/run.go的run函数。下面主要看run函数怎么去启动服务器的:
run函数代码比较长,首先大致看下run.go文件里有哪些东西。包内函数(首字母小写的)先忽略,因为最能提现一个包的主要职责的是它的import、包内变量、init函数、导出函数(首字母大写的函数)、导出结构体(首字母大写的结构体)。
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" // 这里定义了app的名字。
// Flags that control program flow or startup
// 定义了程序启动的一些参数,这些参数从运行时指定(从os.Args中解析)。
// 这里定义的是程序启动后,就不能更改的。 配置文件中的参数是程序启动后,还可以热更新的。
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。那么执行步骤如下:
func init() {
caddy.TrapSignals() // 捕捉信号量,处理
// 解析启动参数,flag包是从os.Args中解析的。
flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", sable")
...
// 注册加载caddyfile的loader.
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
}
caddy -plugins 、 caddy -validate
只关注caddy服务器启动过程相关的步骤。func Run() {
// 1. 解析命令行参数,不调用这个的话,init里面绑定的变量就都是默认值了。
flag.Parse()
// 2. log怎么输出,输出到哪里,日志文件怎么平滑滚动。
// Set up process log before anything bad happens
switch logfile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
...
}
// 3. 加载环境变量并设置。
// load all additional envs as soon as possible
if err := LoadEnvFromFile(envFile); err != nil {
mustLogFatalf("%v", err)
}
// 4. 初始化遥测相关逻辑
// initialize telemetry client
if EnableTelemetry {
err := initTelemetry()
...
}
...
// 5. 可以把caddyfile从json和普通模式互相转换
// Check if we just need to do a Caddyfile Convert and exit
checkJSONCaddyfile()
// 6. 设置cpu使用,最小不能小于1
// Set CPU cap
err := setCPU(cpu)
if err != nil {
mustLogFatalf("%v", err)
}
// 7. 发送Startup事件,然后调用EventHook去处理这个事件。
// EventHook处理过程要用goroutine去处理,防止阻塞。
// Executes Startup events
caddy.EmitEvent(caddy.StartupEvent, nil)
// 8. 去加载caddyfile文件,根据插件定义的loader去加载。
// 详细内容可以看LoadCaddyfile的函数备注
// Get Caddyfile input
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
if err != nil {
mustLogFatalf("%v", err)
}
// 9. 启动服务器!!!
// Start your engines
instance, err := caddy.Start(caddyfileinput)
if err != nil {
mustLogFatalf("%v", err)
}
// 10. 阻塞主进程,防止main goroutine退出
// 内部就是调用了sync.WaitGroup的Wait方法。
// Twiddle your thumbs
instance.Wait()
}
Run函数的逻辑也很清楚: 1.处理参数,读取配置 2. 调用Start方法 3. Wait阻塞main goroutine.
Start函数位于 项目根目录下的 caddy.go文件中。
为什么要这样组织文件结构呢? 为什么不把start函数也放在caddymain包里面呢?
这是为了方便别的项目来引用caddy。如果放在caddymain,别的项目引入的时候就只需要import "github.com/caddyserver/caddy"
从这里可以看出caddy这个项目的定位,不仅仅是一个web server, 还可以是一个lib库。
// Start starts Caddy with the given Caddyfile.
//
// This function blocks until all the servers are listening.
func Start(cdyfile Input) (*Instance, error) {
// 1. 初始化一个instance, Run函数末尾调用的instance.Wait()就是调用的这个instance里面的wg。
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
// 2. 根据这个instance和caddyfile的配置启动服务器
err := startWithListenerFds(cdyfile, inst, nil)
if err != nil {
return inst, err
}
// 3. 给父进程发送成功启动信号
// 这里只要在upgrade的时候才有用,upgrade的时候父进程fork子进程,子进程成功执行完startWithListenerFds后,通过管道发送success给父进程,父进程再kill self。
signalSuccessToParent()
if pidErr := writePidFile(); pidErr != nil {
log.Printf("[ERROR] Could not write pidfile: %v", pidErr)
}
// 4. 发送instance start up 事件,调用对此事件感兴趣的hook函数。
// Execute instantiation events
EmitEvent(InstanceStartupEvent, inst)
return inst, nil
}
到这一步整个服务器还没有运作起来,还无法监听端口,处理请求。startWithListenerFds函数里面开始用Caddyfile文件定义的配置启动相关的Services(注意是复数,有多少个Server取决于Caddyfile里面的定义)。
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
// 1. 这里把instance保存到了一个包内变量instances的slice里面,
// 一个instance代表服务的实例,如http服务器 dns服务器等。
// 对instances的操作都加上锁了,防止并发问题。
instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()
var err error
defer func() {
// 当instance处理失败了,需要从instances 这个slice中移除。
if err != nil {
instancesMu.Lock()
for i, otherInst := range instances {
if otherInst == inst {
instances = append(instances[:i], instances[i+1:]...)
break
}
}
instancesMu.Unlock()
}
}()
// 2. 这里处理Caddyfile, 验证Caddyfile里面的directives(指令)是否有效可用。
if cdyfile == nil {
cdyfile = CaddyfileInput{}
}
err = ValidateAndExecuteDirectives(cdyfile, inst, false)
if err != nil {
return err
}
// 3. 这里是Make Servers,就是产生instance里面所有的server
// 比如instance代表了一个http服务器, serverA就是其中监听在8080端口的一个http服务,
// serverB是另一个监听在8020的http服务。 这里同时被make出来。
slist, err := inst.context.MakeServers()
if err != nil {
return err
}
// 4. 处理start up的相关callback, 先不关注其中的细节。
...
// 5. 上面创建了servers 这里统一启动起来
err = startServers(slist, inst, restartFds)
if err != nil {
return err
}
// 6. 处理after start up的相关callback, 先不关注其中的细节。
...
mu.Lock()
started = true
mu.Unlock()
return nil
}
startWithListenerFds调用了MakeServers() 产生若干个(具体有多少个,看Caddyfile怎么定义)服务的实例,startServers又把这些服务实例启动起来。 而且在启动服务前后,还会执行一些callback函数。
执行逻辑顺序如下:
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
// 这里用到"github.com/mholt/certmagic"这个包来做tls和https相关的事情,
// 因为没有实际用过certmagic, 这里的代码也跳过。
// CertMagic - 利用Go程序管理TLS证书的颁发和续订,自动化添加HTTPS
...
// 前面讲过每个server实例绑定了一个端口,这里是把配置分组,按照端口不同来分组
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
if err != nil {
return nil, err
}
// 根据每个端口,和定义在这个端口上的相关配置来生成一个Server实例
var servers []caddy.Server
for addr, group := range groups {
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
// 判断是dev还是prod环境
deploymentGuess := "dev"
if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction {
deploymentGuess = "prod"
}
telemetry.Set("http_deployment_guess", deploymentGuess)
telemetry.Set("http_num_sites", len(h.siteConfigs))
return servers, nil
}
type Server interface {
TCPServer
UDPServer
}
type TCPServer interface {
Listen() (net.Listener, error)
Serve(net.Listener) error
}
type UDPServer interface {
ListenPacket() (net.PacketConn, error)
ServePacket(net.PacketConn) error
}
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
// 服务启动或处理过程产生的错误就往这个channel中塞
errChan := make(chan error, len(serverList))
// 用来控制记录错误日志的goroutine在记录完日志后能退出
stopChan := make(chan struct{})
// 保证控制server异常退出后,所有错误日志能被记录到
stopWg := &sync.WaitGroup{}
// 根据传入的server list 遍历处理
// 每个server绑定相应的tcp listener和udp listener。并将server添加到instance里面
// TODO: upgrade和reload过程中文件描述符的操作没有看懂, 先省略,后续文章补上
for _, s := range serverList {
var (
ln net.Listener
pc net.PacketConn
err error
)
...
if ln == nil {
ln, err = s.Listen()
if err != nil {
return fmt.Errorf("Listen: %v", err)
}
}
if pc == nil {
pc, err = s.ListenPacket()
if err != nil {
return fmt.Errorf("ListenPacket: %v", err)
}
}
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
}
// 遍历instance的server, 调用server的Serve方法监听。出错的话就把错误塞入errChan这个管道里面
// 每个server都起了两个goroutine,分别监听tcp和udp.
// 这里使用WaitGroup来同步goroutine,
// instance的wg用来防止main goroutine退出。
// stopWg用来挂起最下面那个goroutine。
// 这样能保证,只要有一个server还在监听着,就不会导致main goroutine退出,也不会导致记录错误日志的goroutine退出。
for _, s := range inst.servers {
inst.wg.Add(2)
stopWg.Add(2)
func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
go func() {
defer func() {
inst.wg.Done()
stopWg.Done()
}()
errChan <- s.Serve(ln)
}()
go func() {
defer func() {
inst.wg.Done()
stopWg.Done()
}()
errChan <- s.ServePacket(pc)
}()
}(s.server, s.listener, s.packet, inst)
}
// 这个goroutine用来记录从errChan来的错误
go func() {
for {
select {
case err := <-errChan:
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") {
// this error is normal when closing the listener; see https://github.com/golang/go/issues/4373
log.Println(err)
}
}
case <-stopChan:
return
}
}
}()
// 这个goroutine用来控制,当所有server都退出后,停止上面那个记录错误日志的goroutine.
go func() {
stopWg.Wait()
stopChan <- struct{}{}
}()
return nil
}
以上就是Caddy服务启动的过程。
TODO: 有些详细的实现还没有具体看完,之后再其他文章里面详细讲解。