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的exporter做更好。
└── upgrade.go           // 热更新

Caddy 启动过程

main 入口

caddy/main.go

package mainimport "github.com/caddyserver/caddy/caddy/caddymain"var run = caddymain.Run // replaced for testsfunc main() {run()
}

main函数很简单,引用了caddmain包,调用了caddy/caddymain/run.go的run函数。下面主要看run函数怎么去启动服务器的:

run函数

run函数代码比较长,首先大致看下run.go文件里有哪些东西。包内函数(首字母小写的)先忽略,因为最能提现一个包的主要职责的是它的import、包内变量、init函数、导出函数(首字母大写的函数)、导出结构体(首字母大写的结构体)。

package caddymainimport (..._ "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      stringconf            stringcpu             stringenvFile         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包做了什么操作。

    可以看到caddyhttp包都是在import其他的包, 这些被caddyhttp import的包,就是caddy官方自带的相关插件,和httpserver这个server plugin。
    稍后再看plugin的具体实现。
  2. 初始化caddymain包中的包内变量。
  3. 执行caddymain包的init函数。
    不管什么包,init函数的作用都很明确:初始化相关包内变量,读取相关配置、参数等等。caddymain的init函数也不例外。

    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))}
    
  4. 调用Run函数。
    在caddy这个项目中,caddymain.Run才是程序的"main"函数。
    这里忽略了一些无关紧要的逻辑和某些执行一次就退出的命令,如 caddy -plugins 、 caddy -validate只关注caddy服务器启动过程相关的步骤。
func Run() {// 1. 解析命令行参数,不调用这个的话,init里面绑定的变量就都是默认值了。flag.Parse()// 2. log怎么输出,输出到哪里,日志文件怎么平滑滚动。// Set up process log before anything bad happensswitch logfile {case "stdout":log.SetOutput(os.Stdout)case "stderr":log.SetOutput(os.Stderr)case "":...}// 3. 加载环境变量并设置。   // load all additional envs as soon as possibleif err := LoadEnvFromFile(envFile); err != nil {mustLogFatalf("%v", err)}// 4. 初始化遥测相关逻辑// initialize telemetry clientif EnableTelemetry {err := initTelemetry()...}...// 5. 可以把caddyfile从json和普通模式互相转换// Check if we just need to do a Caddyfile Convert and exitcheckJSONCaddyfile()// 6. 设置cpu使用,最小不能小于1// Set CPU caperr := setCPU(cpu)if err != nil {mustLogFatalf("%v", err)}// 7. 发送Startup事件,然后调用EventHook去处理这个事件。//    EventHook处理过程要用goroutine去处理,防止阻塞。// Executes Startup eventscaddy.EmitEvent(caddy.StartupEvent, nil)// 8. 去加载caddyfile文件,根据插件定义的loader去加载。// 详细内容可以看LoadCaddyfile的函数备注// Get Caddyfile inputcaddyfileinput, err := caddy.LoadCaddyfile(serverType)if err != nil {mustLogFatalf("%v", err)}// 9. 启动服务器!!!// Start your enginesinstance, err := caddy.Start(caddyfileinput)if err != nil {mustLogFatalf("%v", err)}// 10. 阻塞主进程,防止main goroutine退出//     内部就是调用了sync.WaitGroup的Wait方法。// Twiddle your thumbsinstance.Wait()
}

Run函数的逻辑也很清楚: 1.处理参数,读取配置 2. 调用Start方法 3. Wait阻塞main goroutine.

Start 函数

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 eventsEmitEvent(InstanceStartupEvent, inst)return inst, nil
}

到这一步整个服务器还没有运作起来,还无法监听端口,处理请求。startWithListenerFds函数里面开始用Caddyfile文件定义的配置启动相关的Services(注意是复数,有多少个Server取决于Caddyfile里面的定义)。

startWithListenerFds 函数
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 errordefer 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 = truemu.Unlock()return nil
}

startWithListenerFds调用了MakeServers() 产生若干个(具体有多少个,看Caddyfile怎么定义)服务的实例,startServers又把这些服务实例启动起来。 而且在启动服务前后,还会执行一些callback函数。
执行逻辑顺序如下:

  1. MakeServers()
    MakeServers是plugins.go文件内Context接口的一个方法,放在接口里的作用,自然是方便扩展。plugins.go被放在根目录下,说明caddy可以很支持外部自定义其他ServerType的Instance。
    因为golang的接口和实现是松耦合的,很难从接口定义去找到实现它的实例,反过来也是。这里想找到MakeServers的实现,一可以通过逻辑判断,二可以通过IDE的全局搜索功能。
    Caddy项目自带了http这种ServerType的实现,于是去caddyhttp/httpserver里面找,果不其然在caddyhttp/httpserver/plugin.go里面找到了。
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.Serverfor 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
}
  1. run startup callbacks
    (和hook、callback相关的先不分析。)
  2. startServers()
    MakeServer函数被抽象成接口了,为什么startServer没有呢? 这是因为Server本身就是一个接口, startServers的主要逻辑其实就是调用Listen 和 Serve。
type Server interface {TCPServerUDPServer
}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.Listenerpc  net.PacketConnerr 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/4373log.Println(err)}}case <-stopChan:return}}}()// 这个goroutine用来控制,当所有server都退出后,停止上面那个记录错误日志的goroutine.go func() {stopWg.Wait()stopChan <- struct{}{}}()return nil
}
  1. run any AfterStartup callbacks
    (和hook、callback相关的先不分析。)

以上就是Caddy服务启动的过程。

TODO: 有些详细的实现还没有具体看完,之后再其他文章里面详细讲解。

Caddy 源码阅读相关推荐

  1. Caddy源码阅读(一)Run详解

    Caddy源码阅读(一)Run详解 前言 本次系列会讲解 caddy 整个生命周期涉及到的源码. 平时我们使用 caddy 都是使用 它的 二进制 分发文件,现在来分析 caddy 的 Run 函数. ...

  2. 应用监控CAT之cat-client源码阅读(一)

    CAT 由大众点评开发的,基于 Java 的实时应用监控平台,包括实时应用监控,业务监控.对于及时发现线上问题非常有用.(不知道大家有没有在用) 应用自然是最初级的,用完之后,还想了解下其背后的原理, ...

  3. centos下将vim配置为强大的源码阅读器

    每日杂事缠身,让自己在不断得烦扰之后终于有了自己的清静时光来熟悉一下我的工具,每次熟悉源码都需要先在windows端改好,拖到linux端,再编译.出现问题,还得重新回到windows端,这个过程太耗 ...

  4. 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking

    该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...

  5. 源码阅读:SDWebImage(六)——SDWebImageCoderHelper

    该文章阅读的SDWebImage的版本为4.3.3. 这个类提供了四个方法,这四个方法可分为两类,一类是动图处理,一类是图像方向处理. 1.私有函数 先来看一下这个类里的两个函数 /**这个函数是计算 ...

  6. mybatis源码阅读

    说下mybatis执行一个sql语句的流程 执行语句,事务等SqlSession都交给了excutor,excutor又委托给statementHandler SimpleExecutor:每执行一次 ...

  7. 24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment

    24 UsageEnvironment使用环境抽象基类--Live555源码阅读(三)UsageEnvironment 24 UsageEnvironment使用环境抽象基类--Live555源码阅读 ...

  8. Transformers包tokenizer.encode()方法源码阅读笔记

    Transformers包tokenizer.encode()方法源码阅读笔记_天才小呵呵的博客-CSDN博客_tokenizer.encode

  9. 源码阅读笔记 BiLSTM+CRF做NER任务 流程图

    源码阅读笔记 BiLSTM+CRF做NER任务(二) 源码地址:https://github.com/ZhixiuYe/NER-pytorch 本篇正式进入源码的阅读,按照流程顺序,一一解剖. 一.流 ...

最新文章

  1. 一个简单的struts的例子
  2. php post请求后端拿不到值_[精选] uniapp实现多端开发,与PHP是如何结合的
  3. 多个Spring Boot项目部署在一个Tomcat容器无法启动
  4. iOS H264,H265视频编码(Video encode)
  5. 高效终端设备视觉系统开发与优化
  6. python学到什么程度算是会-Python 必须学到什么程度?
  7. iOS-数据持久化-第三方框架FMDB的使用
  8. ImageView、ImageButton、Button三者比较
  9. 我对java的理解(二)——反射是小偷的万能钥匙
  10. hnu 暑期实训之挖掘机技术哪家强
  11. 腾讯回应“暴力裁员”;小米否认常程与联想签有竞业禁止条款;NumPy 1.16.6 发布 | 极客头条...
  12. python自学行吗-没有编程基础,可以自学Python吗?
  13. iOS App打包上架超详细流程1
  14. python map对象
  15. python集合操作班级干部竞选演讲稿_【热门】竞选班干部演讲稿集合8篇
  16. Word中使用表格排版公式时,表格内序号纵向居中的问题。
  17. [英语竞赛] 知识整理
  18. 国产处理器服务器操作系统安装(海之舟服务器操作系统安装说明)
  19. 幂律分布参数估计幂律分布公式计算
  20. 软件测试之linux环境搭建与操作Xshell、Xftp

热门文章

  1. quartz报错:Couldn‘t retrieve trigger: No record found for selection of Trigger with key—————————————
  2. 数据库的数据存储文件
  3. 查看/data/data下的数据库文件
  4. 台大李宏毅课程笔记3——New Optimization for Deep Learning深度学习新优化
  5. python 识别人名_HanLP中人名识别分析
  6. hystrix熔断器之配置
  7. Uos统信系统本地apt及基础网络,主机名时区配置
  8. Android 简单音乐播放器开发
  9. 链游知识4:以太坊浏览器的使用
  10. CSS禅意花园 —— 设计