Prometheus 的大粉丝。 我在家庭和工作中都使用了很多,并且非常喜欢深入" />
062017
 

我是 Prometheus 的大粉丝。 我在家庭和工作中都使用了很多,并且非常喜欢深入了解我的系统在任何时刻的工作情况。 最广泛使用的 Prometheus 商家之一是 node_exporter:可以从类 UNIX 机器中提取各种指标的守护进程。

在我浏览仓库时,我注意到 open issue,要求向 node_exporter 添加 WiFi 指标。 这个想法吸引了我,我意识到我一定会在我的 Linux 笔记本电脑上使用这样一个功能。 我开始探索在 Linux 上检索WiFi 设备信息的选项。

经过几周的实验(包括旧版 ioctl() 无线扩展 API),我创作了两个 Go 包,它们可以共同工作,在 Linux 上与 WiFi 设备进行交互:

* netlink:提供对 Linux netlink 套接字的底层访问。
* wifi:提供对 IEEE 802.11 WiFi 设备操作和统计信息的访问。

这一系列的文章将描述在 Go 中实现这些包时学到的一些经验教训,希望能够为选择这两个库进行试验的人提供一个很好的参考。

本系列的伪代码将使用 Go 的 x/sys/unix 包和我的 netlink 和 wifi 包的类型。 我打算把这个系列分解如下(链接来自更多的文章):

* 第 1 部分:netlink(这篇文章):netlink 的介绍。
* 第 2 部分:通用 netlink:通用 netlink 简介,一个 netlink 家族,旨在简化新家庭的创建。
* 第 3 部分:包 netlink,genetlink 和 wifi:使用 Go 驱动与 netlink,通用 netlink 和 nl80211 的交互。

什么是 netlink?

Netlink 是一个 Linux 内核进程间通信机制,可实现用户空间进程与内核之间的通信,或多个用户空间进程通讯。 Netlink 套接字是启用此通信的原语。

这篇文章将提供关于 netlink 套接字,消息,多播组和属性的基本知识。 此外,这篇文章将重点介绍用户空间和内核之间的通信,而不是两个用户空间进程之间的通信。

创建 netlink 套接字

Netlink 使用标准 BSD 套接字 API。 对于在 C 中进行网络编程的人来说,这应该是非常熟悉的。如果您不熟悉 BSD 套接字,我建议您阅读优秀的“Beej 网络编程指南”来了解本主题。

重要的是要注意,netlink 通信永远不会遍历本地主机(local host)。 记住这一点,让我们开始深入了解 netlink 套接字的工作原理!

要与 netlink 进行通信,必须打开 netlink 套接字。 这是使用 socket() 系统调用完成的:

fd, err := unix.Socket(
    // Always used when opening netlink sockets.
    // 打开 netlink 套接字时始终使用。
    unix.AF_NETLINK,
    // Seemingly used interchangeably with SOCK_DGRAM,
    // but it appears not to matter which is used.
    // 似乎与 SOCK_DGRAM 可互换使用,使用哪个并不重要。
    unix.SOCK_RAW,
    // The netlink family that the socket will communicate
    // with, such as NETLINK_ROUTE or NETLINK_GENERIC.
    // 套接字与之通信的 netlink 系列,如 NETLINK_ROUTE 或 NETLINK_GENERIC。
    family,
)

family 参数指定一个特定的 netlink 系列:本质上是可以使用 netlink 套接字进行通信的内核子系统。 这些系列可能会提供诸如此类的功能。

* NETLINK_ROUTE:操纵 Linux 的网络接口,路由,IP地址等
* NETLINK_GENERIC:简化添加新的 netlink 系列的构建块,如 nl80211,Open vSwitch 等。

一旦创建套接字,必须调用 bind() 来准备发送和接收消息。

err := unix.Bind(fd, &unix.SockaddrNetlink{
    // Always used when binding netlink sockets.
    // 绑定 netlink 套接字时始终使用。
    Family: unix.AF_NETLINK,
    // A bitmask of multicast groups to join on bind.
    // Typically set to zero.
    // 绑定的多播组加入的位掩码。通常设置为零。
    Groups: 0,
    // If you'd like, you can assign a PID for this socket
    // here, but in my experience, it's easier to leave
    // this set to zero and let netlink assign and manage
    // PIDs on its own.
    // 如果你愿意,你可以在这里给这个套接字分配一个PID,但是根据我的经验,将这个设置保留为零是很容易的,让 netlink 自己分配和管理 PID。
    Pid: 0,
})

此时,netlink 套接字现在可以发送和接收来自内核的消息。

Netlink 消息格式

Netlink 消息遵循非常特殊的格式。 所有消息必须与 4 字节边界对齐。 例如,16 字节的消息必须按原样发送,但是 17 字节的消息必须被填充到 20 个字节。

非常重要的是要注意,与典型的网络通信不同,netlink 使用主机字节顺序来编码和解码整数,而不是普通的网络字节顺序(大端)。 因此,必须在数据的字节和整数表示之间转换的代码必须牢记这一点。

Netlink 消息头使用以下格式:(来自 RFC 3549 的图):

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type | Flags |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Process ID (PID) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这些字段包含以下信息:

* 长度(32位):整个消息的长度,包括报头和有效载荷(消息体)。
* 类型(16位):消息包含什么样的信息,如错误,multi-part 消息的结束等。
* 标志(16位):指示消息是请求的位标志,如 multi-part ,请求确认等。
* 序列号(32位):用于关联请求和响应的数字;每个请求递增。
* 进程ID(PID)(32位):有时称为端口号;用于唯一标识特定 netlink 套接字的数字; 可能是也可能不是进程的ID。

最后,有效载荷可能会立即跟随 netlink 消息头。 再次注意,有效载荷必须填充到 4 字节的边界。

向内核发送请求的示例 netlink 消息可能类似于 Go 中的以下内容:

msg := netlink.Message{
    Header: netlink.Header{
        // Length of header, plus payload.
        Length: 16 + 4,
        // Set to zero on requests.
        Type: 0,
        // Indicate that message is a request to the kernel.
        Flags: netlink.HeaderFlagsRequest,
        // Sequence number selected at random.
        Sequence: 1,
        // PID set to process's ID.
        PID: uint32(os.Getpid()),
    },
    // An arbitrary byte payload. May be in a variety of formats.
    Data: []byte{0x01, 0x02, 0x03, 0x04},
}

发送和接收 netlink 消息

现在我们熟悉了netlink 套接字的一些基础知识,我们可以使用套接字发送和接收数据。

一旦一个消息已经准备好,它可以使用 sendto() 发送到内核:

// Assume messageBytes produces a netlink request message (like the
// one shown above) with the specified payload.
b := messageBytes([]byte{0x01, 0x02, 0x03, 0x04})
err := unix.Sendto(b, 0, &unix.SockaddrNetlink{
    // Always used when sending on netlink sockets.
    Family: unix.AF_NETLINK,
})

对 netlink 的只读请求通常不需要任何特殊权限。 使用 netlink 修改子系统状态或需要锁定其内部状态的操作通常需要提升权限。 这可能意味着以 root 身份运行该程序或使用 CAP_NET_ADMIN 来执行以下操作:

* 发送写请求,使用 netlink 更改子系统。
* 使用 NLM_F_ATOMIC 标志发送读取请求,来从 netlink 接收数据的原子快照。

使用 recvfrom() 从 netlink socket 接收消息可能会稍微复杂一些,具体取决于各种因素。Netlink 可能如下回复:

* 非常小或非常大的消息。
* Mulit-part 消息,分为多个部分。
* 显示错误号,当消息头类型为“error”时。

此外,每个消息的序列号和 PID 也应该被验证。使用原始系统调用时,由套接字的用户处理这些情况。

大消息

为了处理大消息,我采用了一种分配单页内存的技术:窥探缓冲区(读取数据但不清除),然后将缓冲区的大小加倍,如果太小,无法读取整个消息。 谢谢 Dominik Honnef 你对这个问题的见解。

为了简便起见,我们省略了错误处理。实际中,请检查你的错误!

b := make([]byte, os.Getpagesize())
for {
    // Peek at the buffer to see how many bytes are available.
    n, _, _ := unix.Recvfrom(fd, b, unix.MSG_PEEK)
    // Break when we can read all messages.
    if n < len(b) {
        break
    }
    // Double in size if not enough bytes.
    b = make([]byte, len(b)*2)
}
// Read out all available messages.
n, _, _ := unix.Recvfrom(fd, b, 0)

理论上说,一个 netlink 消息的大小可能高达~4GiB(32位无符号整数的最大值),但实践中消息要小得多。

Multi-part 消息

对于某些类型的消息,netlink 可以使用“Multi-part 消息”来回复。在这种情况下,最后一条之前的每条消息都将设置“multi”标志。 最后的消息将有一个“done”的类型。

返回 Multi-part 消息时,第一个 recvfrom() 将返回所有设置了“multi”标志的消息。 接下来,必须再次调用 recvfrom() 来检索头文件类型为“done”的最终消息。 这是非常重要的,否则 netlink 将会直接 hang 在后续的请求上,等待调用者取出最后的头部类型为“done”的消息。

Netlink 错误号

如果 netlink 由于任何原因不能满足请求,它将在包含消息头类型为“error”的消息的有效载荷中返回明确的错误号。 这些错误号与 Linux 的经典错误号相同,例如“不存在这样的文件或目录”的ENOENT,“被拒绝的权限”的 EPERM。

如果消息的报头类型指示错误,则错误编号将被编码为消息有效载荷的前4个字节中的带符号的32位整数(注意:也使用系统字节顺序)。

const name = "foo0"
_, err := rtnetlink.InterfaceByName(name)
if err != nil && os.IsNotExist(err) {
    // Error is result of a netlink error number, and can be
    // checked in the usual Go fashion.
    log.Printf("no such device: %q", name)
    return
}

序列号和 PID 验证

为了确保内核的 netlink 回复是响应我们的某个请求,我们还必须在每个接收的消息上验证序列号和 PID。在大多数情况下,这些都应该与发送到内核的请求完全一致。后续请求应该在向 netlink 发送另一个消息之前增加序列号。

PID 验证可能会有所不同,具体取决于几个条件。

* 如果在用户空间中代表组播组接收到消息,则它的 PID 为0,表示消息始发于内核。
* 如果请求被发送到 PID 为 0 的内核,netlink 将在第一个响应中为给定的套接字分配一个PID。该PID应在随后的通信中使用(并被验证)。

假设您没有在 bind() 中指定 PID,则在单个应用程序中打开多个 netlink 套接字时,将该进程的 ID 作为第一个 netlink 套接字的 PID。后续的将由 netlink 从随机数选择。根据我的经验,只要让 netlink 分配所有自身的 PID,并确保跟踪每个套接字分配的数字,这一点要容易得多。

多播组

除了传统的请求/响应套接字范例之外,netlink 套接字还提供多播组,以便在发生某些事件时启用订阅。

可以使用两种不同的方法来连接多播组:

* 在 bind() 期间指定组位掩码。这被认为是 “legacy” 的方法。
* 使用 setsockopt() 连接和断开组。 这是首选,现代的方法。

使用 setsockopt() 连接和断开组是交换单个常量的问题。在Go中,这是使用 uint32 “group” 值完成的。

// Can also specify unix.NETLINK_DROP_MEMBERSHIP to leave
// a group.
const joinLeave = unix.NETLINK_ADD_MEMBERSHIP
// Multicast group ID. Typically assigned using predefined
// constants for various netlink families.
const group = 1
err := syscall.SetSockoptInt(
    fd,
    unix.SOL_NETLINK,
    joinLeave,
    group,
)

一旦加入了一个组,你可以像往常一样使用 recvfrom() 来收听消息。断开组将不会给出给定多播组的更多消息。

Netlink 属性

要在 netlink 套接字上包含我们的引用,我们将讨论 netlink 消息有效载荷的一个非常常见的数据格式:属性。

Netlink 属性是不寻常的,因为它们是LTV(长度,类型,值)格式,而不是典型的TLV(类型,长度,值)。 与 netlink 套接字中的其他整数一样,类型和长度值也以主机字节编码。 最后,netlink 属性也必须填充到 4 字节边界,就像 netlink 消息一样。

每个字段包含以下信息:

* 长度(16位):整个属性的长度,包括长度,类型和值字段。不能设置为4字节边界。 例如,如果长度为17字节,则该属性将被填充到20个字节,但是不应该将3个字节的填充解释为有意义的。
* 类型(16位):属性的类型,通常定义为某些 netlink 系列或标题中的常量。
* 值(可变字节):属性的原始有效负载。可能包含以相同格式存储的嵌套属性。这些嵌套属性可能包含更多的嵌套属性!

netlink 属性中可能有两个特殊的标志,虽然我还没有在我的工作中遇到他们。

* NLA_F_NESTED:指定嵌套属性;用作解析的提示。即使存在嵌套属性,似乎并不总是被使用。
* NLA_F_NET_BYTEORDER:属性数据以网络字节顺序(大端)存储,而不是主机字节顺序。

请参阅给定的 netlink 系列的文档,以确定是否应检查其中一个标志。

概要

现在我们熟悉使用 netlink 套接字和消息,系列中的下一篇文章将基于这些知识来深入通用 netlink。
希望你喜欢这篇文章! 如果您有任何疑问或意见,请随时通过评论,Twitter或Gophers Slack(用户名:mdlayher)进行联系。

更新

2/22/2017:将有关BSD套接字API的背景信息移动到“创建netlink套接字”部分。
2/22/2017:注意到需要root或CAP_NET_ADMIN用于许多netlink写操作,以及使用NLM_F_ATOMIC时。
2/23/2017:注意到在 bind() 中为套接字指定 PID 的能力。
2/27/2017:由于 syscall 被冻结,因此将伪代码更改为使用x/sys/unix而不是syscall。

参考

以下链接经常用作参考,因为我建立了包 netlink,并撰写了这篇文章:
维基百科:Netlink:https://en.wikipedia.org/wiki/Netlink
使用Netlink套接字在Linux内核和用户空间之间进行通信:https://pdfs.semanticscholar.org/6efd/e161a2582ba5846e4b8fea5a53bc305a64f3.pdf
使用Netlink套接字了解和编程:https://people.redhat.com/nhorman/papers/netlink.pdf
Netlink [C] 库(libnl):https://www.infradead.org/~tgr/libnl/doc/core.html

182017
 

Go 标准库有一个 expvar 包。 该软件包可以通过 JSON 格式的 HTTP API 公开您的应用程序和 Go 运行时的指标。 我认为这个软件包对于每个人 gopher 来说都是有用的。 但是,来自 godoc.org 的数据表明,没有多少人知道这个包。截止目前(2017-6-18),该包被公开的项目 import 2207 次,相比较而言,连 image 包被 import 3491 次之多。这篇文章,我想展示 expvar 包的工作原理,以及它的使用。

包简介

包 expvar 为公共变量提供了一个标准化的接口,如服务器中的操作计数器。它以 JSON 格式通过 /debug/vars 接口以 HTTP 的方式公开这些公共变量。

设置或修改这些公共变量的操作是原子的。

除了为程序增加 HTTP handler,此包还注册以下变量:

    cmdline   os.Args
    memstats  runtime.Memstats

导入该包有时只是为注册其 HTTP handler 和上述变量。 要以这种方式使用,请将此包通过如下形式引入到程序中:

    import _ "expvar"

例子

在浏览此包的详细信息之前,我想演示使用 expvar 包可以做什么。以下代码创建一个在监听 1818端口的 HTTP 服务器。每个请求 hander() 后,在向访问者发送响应消息之前增加计数器。

    package main

    import (
        "expvar"
        "fmt"
        "net/http"
    )

    var visits = expvar.NewInt("visits")

    func handler(w http.ResponseWriter, r *http.Request) {
        visits.Add(1)
        fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
    }

    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":1818", nil)
    }

导入时,expvar 包将为 http.DefaultServeMux 上的模式 “/debug /vars” 注册一个处理函数。此处理程序返回已在 expvar 包中注册的所有指标。运行代码并访问 http://localhost:1818/debug/vars,您将看到如下所示的内容。输出被截断以增加可读性:

    {
      "cmdline": [
        "/tmp/go-build872151671/command-line-arguments/_obj/exe/main"
      ],
      "memstats": {
        "Alloc": 397576,
        "TotalAlloc": 397576,
        "Sys": 3084288,
        "Lookups": 7,
        "Mallocs": 5119,
        "Frees": 167,
        "HeapAlloc": 397576,
        "HeapSys": 1769472,
        "HeapIdle": 1015808,
        "HeapInuse": 753664,
        "HeapReleased": 0,
        "HeapObjects": 4952,
        "StackInuse": 327680,
        "StackSys": 327680,
        "MSpanInuse": 14240,
        "MSpanSys": 16384,
        "MCacheInuse": 4800,
        "MCacheSys": 16384,
        "BuckHashSys": 2380,
        "GCSys": 131072,
        "OtherSys": 820916,
        "NextGC": 4194304,
        "LastGC": 0,
        "PauseTotalNs": 0,
        "PauseNs": [
          0,
          0,
        ],
        "PauseEnd": [
          0,
          0
        ],
        "GCCPUFraction": 0,
        "EnableGC": true,
        "DebugGC": false,
        "BySize": [
          {
            "Size": 16640,
            "Mallocs": 0,
            "Frees": 0
          },
          {
            "Size": 17664,
            "Mallocs": 0,
            "Frees": 0
          }
        ]
      },
      "visits": 0
    }

信息真不少。这是因为默认情况下该包注册了os.Args 和 runtime.Memstats 两个指标。 我想在这个 JSON 响应结束时关注访问计数器。 因为计数器还没有增加,它的值仍然为0。现在通过访问http:// localhost:1818/golang 来增加计数器,然后返回。计数器不再为0。

expvar.Publish

expvar 包相当小且容易理解。它主要由两个部分组成。第一个是函数 expvar.Publish(name string,v expvar.Var)。该函数可用于在未导出的全局注册表中注册具有特定名称的 v。以下代码段显示了具体实现。接下来的 3 个代码段是从 expvar 包的源代码中截取的。

    // Publish declares a named exported variable. This should be called from a
    // package's init function when it creates its Vars. If the name is already
    // registered then this will log.Panic.
    func Publish(name string, v Var) {
        mutex.Lock()
        defer mutex.Unlock()

        // Check if name has been taken already. If so, panic.
        if _, existing := vars[name]; existing {
            log.Panicln("Reuse of exported var name:", name)
        }

         // vars is the global registry. It is defined somewhere else in the
         // expvar package like this:
         //
         //  vars = make(map[string]Var)
        vars[name] = v
        // 一方面,该包中所有公共变量,放在 vars 中,同时,通过 varKeys 保存了所有变量名,并且按字母序排序,即实现了一个有序的、线程安全的哈希表
        varKeys = append(varKeys, name)
        sort.Strings(varKeys)
    }

expvar.Var

另一个重要的组件是 expvar.Var 接口。 这个接口只有一个方法:

    // Var is an abstract type for all exported variables.
    type Var interface {
            // String returns a valid JSON value for the variable.
            // Types with String methods that do not return valid JSON
            // (such as time.Time) must not be used as a Var.
            String() string
    }

所以你可以在有 String() string 方法的所有类型上调用 Publish() 函数。

expvar.Int

expvar 包附带了其他几个类型,它们实现了 expvar.Var 接口。其中一个是 expvar.Int,我们已经在演示代码中通过 expvar.NewInt(“visits”) 方式使用它了,它会创建一个新的 expvar.Int,并使用 expvar.Publish 注册它,然后返回一个指向新创建的 expvar.Int 的指针。

    func NewInt(name string) *Int {
        v := new(Int)
        Publish(name, v)
        return v
    }

expvar.Int 包装一个 int64,并有两个函数 Add(delta int64) 和 Set(value int64),它们以线程安全的方式修改整数。

Other types

除了expvar.Int,该包还提供了一些实现 expvar.Var 接口的其他类型:

* expvar.Float
* expvar.String
* expvar.Map
* expvar.Func

前两个类型包装了 float64 和 string。后两种类型需要稍微解释下。

expvar.Map 类型可用于使指标出现在某些名称空间下。我经常这样用:

    var stats = expvar.NewMap("tcp")
    var requests, requestsFailed expvar.Int

    func init() {
        stats.Set("requests", &requests)
        stats.Set("requests_failed", &requestsFailed)
    }

这段代码使用名称空间 tcp 注册了两个指标 requests 和 requests_failed。它将显示在 JSON 响应中,如下所示:

    {
        "tcp": {
            "request": 18,
            "requests_failed": 21
        }
    }

当要使用某个函数的执行结果时,您可以使用 expvar.Func。假设您希望计算应用程序的正常运行时间,每次有人访问 http://localhost:1818/debug/vars 时,都必须重新计算此值。

    var start = time.Now()

    func calculateUptime() interface {
        return time.Since(start).String()
    }

    expvar.Publish("uptime", expvar.Func(calculateUptime))

关于 Handler 函数

本文开始时提到,可以简单的导入 expvar 包,然后使用其副作用,导出 /debug/vars 模式。然而,如果我们使用了一些框架,并非使用 http.DefaultServeMux,而是框架自己定义的 Mux,这时直接导入使用副作用可能会不生效。这时我们可以按照使用的框架,定义自己的路径,比如也叫:/debug/vars,然后,这对应的处理程序中,有两种处理方式:

1)将处理直接交给 expvar.Handler,比如:

    handler := expvar.Handler()
    handler.ServeHTTP(w, req)

2)自己遍历 expvar 中的公共变量,构造输出,甚至可以过滤 expvar 默认提供的 cmdline 和 memstats,我们看下 expvarHandler 的源码就明白了:(通过 expvar.Do 函数来遍历)

    func expvarHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        fmt.Fprintf(w, "{\n")
        first := true
        Do(func(kv KeyValue) {
            if !first {
                fmt.Fprintf(w, ",\n")
            }
            first = false
            fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
        })
        fmt.Fprintf(w, "\n}\n")
    }

Go 语言中文网 就是用了第1种方式来处理。

总结

通过 expvar 包,使得展示应用程序指标非常容易。我几乎在我写的每个应用程序中使用它来展示一些指示应用程序运行状况的指标。InfluxDB 和 Grafana,加上一个自定义的聚合器,我可以很容易监控我的应用程序。

在英文 expvar in action 的基础上有增减。

222014
 

1、cron 表达式的基本格式

用过 linux 的应该对 cron 有所了解。linux 中可以通过 crontab -e 来配置定时任务。不过,linux 中的 cron 只能精确到分钟。而我们这里要讨论的 Go 实现的 cron 可以精确到秒,除了这点比较大的区别外,cron 表达式的基本语法是类似的。(如果使用过 Java 中的 Quartz,对 cron 表达式应该比较了解,而且它和这里我们将要讨论的 Go 版 cron 很像,也都精确到秒)

cron(计划任务),顾名思义,按照约定的时间,定时的执行特定的任务(job)。cron 表达式 表达了这种约定。

cron 表达式代表了一个时间集合,使用 6 个空格分隔的字段表示。

字段名 是否必须 允许的值 允许的特定字符
秒(Seconds) 0-59
* / , -
分(Minutes) 0-59
* / , -
时(Hours) 0-23
* / , -
日(Day of month) 1-31
* / , – ?
月(Month) 1-12 or JAN-DEC
* / , -
星期(Day of week) 0-6 or SUM-SAT
* / , – ?

注:
1)月(Month)和星期(Day of week)字段的值不区分大小写,如:SUN、Sun 和 sun 是一样的。
2)星期
(Day of week)字段如果没提供,相当于是 *

2、特殊字符说明

1)星号(*)

表示 cron 表达式能匹配该字段的所有值。如在第5个字段使用星号(month),表示每个月

2)斜线(/)

表示增长间隔,如第1个字段(minutes) 值是 3-59/15,表示每小时的第3分钟开始执行一次,之后每隔 15 分钟执行一次(即 3、18、33、48 这些时间点执行),这里也可以表示为:3/15

3)逗号(,)

用于枚举值,如第6个字段值是 MON,WED,FRI,表示 星期一、三、五 执行

4)连字号(-)

表示一个范围,如第3个字段的值为 9-17 表示 9am 到 5pm 直接每个小时(包括9和17)

5)问号(?)

只用于 日(Day of month) 和 星期(Day of week),表示不指定值,可以用于代替 *

3、主要类型或接口说明

1)Cron:包含一系列要执行的实体;支持暂停【stop】;添加实体等

type Cron struct {
    entries  []*Entry
    stop     chan struct{}   // 控制 Cron 实例暂停
    add      chan *Entry     // 当 Cron 已经运行了,增加新的 Entity 是通过 add 这个 channel 实现的
    snapshot chan []*Entry   // 获取当前所有 entity 的快照
    running  bool            // 当已经运行时为true;否则为false
}

注意,Cron 结构没有导出任何成员。

注意:有一个成员 stop,类型是 struct{},即空结构体。

2)Entry:调度实体

type Entry struct {
    // The schedule on which this job should be run.
    // 负责调度当前 Entity 中的 Job 执行
    Schedule Schedule

    // The next time the job will run. This is the zero time if Cron has not been
    // started or this entry's schedule is unsatisfiable
    // Job 下一次执行的时间
    Next time.Time

    // The last time this job was run. This is the zero time if the job has never
    // been run.
    // 上一次执行时间
    Prev time.Time

    // The Job to run.
    // 要执行的 Job
    Job Job
}

3)Job:每一个实体包含一个需要运行的Job

这是一个接口,只有一个方法:run

type Job interface {
    Run()
}

由于 Entity 中需要 Job 类型,因此,我们希望定期运行的任务,就需要实现 Job 接口。同时,由于 Job 接口只有一个无参数无返回值的方法,为了使用方便,作者提供了一个类型:

type FuncJob func()

它通过简单的实现 Run() 方法来实现 Job 接口:

func (f FuncJob) Run() { f() }

这样,任何无参数无返回值的函数,通过强制类型转换为 FuncJob,就可以当作 Job 来使用了,AddFunc 方法 就是这么做的。

4)Schedule:每个实体包含一个调度器(Schedule)

负责调度 Job 的执行。它也是一个接口。

type Schedule interface {
    // Return the next activation time, later than the given time.
    // Next is invoked initially, and then each time the job is run.
    // 返回同一 Entity 中的 Job 下一次执行的时间
    Next(time.Time) time.Time
}

Schedule 的具体实现通过解析 Cron 表达式得到。

库中提供了 Schedule 的两个具体实现,分别是 SpecSchedule 和 ConstantDelaySchedule。

① SpecSchedule

type SpecSchedule struct {
    Second, Minute, Hour, Dom, Month, Dow uint64
}

从开始介绍的 Cron 表达式可以容易得知各个字段的意思,同时,对各种表达式的解析也会最终得到一个 SpecSchedule 的实例。库中的 Parse 返回的其实就是 SpecSchedule 的实例(当然也就实现了 Schedule 接口)。

② ConstantDelaySchedule

type ConstantDelaySchedule struct {
    Delay time.Duration // 循环的时间间隔
}

这是一个简单的循环调度器,如:每 5 分钟。注意,最小单位是秒,不能比秒还小,比如 毫秒。

通过 Every 函数可以获取该类型的实例,如:

constDelaySchedule := Every(5e9)

得到的是一个每 5 秒执行一次的调度器。

4、主要实例化方法

1) 函数

① 实例化 Cron

func New() *Cron {
    return &Cron{
        entries:  nil,
        add:      make(chan *Entry),
        stop:     make(chan struct{}),
        snapshot: make(chan []*Entry),
        running:  false,
    }
}

可见实例化时,成员使用的基本是默认值;

② 解析 Cron 表达式

func Parse(spec string) (_ Schedule, err error)

spec 可以是:

  • Full crontab specs, e.g. “* * * * * ?”
  • Descriptors, e.g. “@midnight”, “@every 1h30m”

② 成员方法

// 将 job 加入 Cron 中
// 如上所述,该方法只是简单的通过 FuncJob 类型强制转换 cmd,然后调用 AddJob 方法
func (c *Cron) AddFunc(spec string, cmd func()) error

// 将 job 加入 Cron 中
// 通过 Parse 函数解析 cron 表达式 spec 的到调度器实例(Schedule),之后调用 c.Schedule 方法
func (c *Cron) AddJob(spec string, cmd Job) error

// 获取当前 Cron 总所有 Entities 的快照
func (c *Cron) Entries() []*Entry

// 通过两个参数实例化一个 Entity,然后加入当前 Cron 中
// 注意:如果当前 Cron 未运行,则直接将该 entity 加入 Cron 中;
// 否则,通过 add 这个成员 channel 将 entity 加入正在运行的 Cron 中
func (c *Cron) Schedule(schedule Schedule, cmd Job)

// 新启动一个 goroutine 运行当前 Cron
func (c *Cron) Start()

// 通过给 stop 成员发送一个 struct{}{} 来停止当前 Cron,同时将 running 置为 false
// 从这里知道,stop 只是通知 Cron 停止,因此往 channel 发一个值即可,而不关心值是多少
// 所以,成员 stop 定义为空 struct
func (c *Cron) Stop()

5、使用示例

package main

import (
    "github.com/robfig/cron"
    "log"
)

func main() {
    i := 0
    c := cron.New()
    spec := "*/5 * * * * ?"
    c.AddFunc(spec, func() {
        i++
        log.Println("cron running:", i)
    })
    c.Start()

    select{}
}

这是一个简单示例。

输出类似这样:

2014/02/22 21:23:40 cron running: 1
2014/02/22 21:23:45 cron running: 2
2014/02/22 21:23:50 cron running: 3
2014/02/22 21:23:55 cron running: 4
2014/02/22 21:24:00 cron running: 5

可见 cron 的使用还是挺简单的。

6、Go 版 cron 源码地址

查看源码

082014
 

在公司内部,经常会有域名是需要绑定host才能访问的,如果是通过浏览器访问,我们会修改本机的hosts文件;然而,如果是要通过程序访问这样的域名,我们是否依然必须绑定host呢?答案当然是否定的,而且,依赖本地绑定的host,程序到其他机器部署,也必须在那台机器绑定host,如果机器很多呢?

本文示例:
IP:192.168.1.102,也就是说需要访问这台机器上的资源
域名:www.studygolang.com,nginx 配置的虚拟主机
url path:/testhost.txt,内容是:Welcome to studygolang.com

需求:需要请求服务器上的 testhost.txt 资源

1、Linux Shell的解决方案

Linux下的curl程序可以绑定host,因此,在 shell 中可以很简单的实现,如: curl -H “Host:www.studygolang.com” http://192.168.1.102/testhost.txt

2、PHP 的解决方案

1)通过 curl 扩展实现

     $ch = curl_init();
     curl_setopt($ch, CURLOPT_HTTPHEADER, array('Host:www.studygolang.com'));
     curl_setopt($ch, CURLOPT_URL, 'http://192.168.1.102/testhost.txt');
     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
     $ret = curl_exec($ch);
     var_dump($ret);
    

2)不依赖 curl 扩展的方式

     // Create a stream
     $opts = array(
         'http'=>array(
             'method'=>"GET",
             'header'=>"Host:www.studygolang.com"
         )
     );

     $context = stream_context_create($opts);

     // Open the file using the HTTP headers set above
     $ret = file_get_contents('http://192.168.1.102/testhost.txt', false, $context);
     var_dump($ret);
 

3、Golang 的解决方案

由于 Go 标准库实现了 http 协议,在 net/http 包中寻找解决方案。

一般的,请求一个 url,我们通过以下代码实现:

http.Get(url)

然而,针对本文说到的这种情况,无论 url = “http://192.168.1.102/testhost.txt” 还是 url = “http://www.studygolang.com/testhost.txt”,都无法请求到资源(没有绑定 host 的情况)。

在 http 包中的 Request 结构中,有一个字段:Host,我们可以参考上面两种解决方案,设置 Host 的值。方法如下:

     package main

     import (
         "net/http"
         "io/ioutil"
         "fmt"
     )

     func main() {
         req, err := http.NewRequest("GET", "http://192.168.1.102/testhost.txt", nil)
         if err != nil {
             panic(err)
         }
         req.Host = "www.studygolang.com"
         resp, err := http.DefaultClient.Do(req)
         if err != nil {
             panic(err)
         }
         defer resp.Body.Close()
         body, err := ioutil.ReadAll(resp.Body)
         if err != nil {
             panic(err)
         }
         fmt.Println(string(body))
     }

4、总结

不管是什么方式、什么语言,归根结底,需要告知服务器请求的是哪个 Host,这个是 HTTP 协议的 Host 头。如果不手动设置 Host 头,则会从请求的 url 中获取。

232013
 

在写命令行程序(工具、server)时,对命令参数进行解析是常见的需求。各种语言一般都会提供解析命令行参数的方法或库,以方便程序员使用。如果命令行参数纯粹自己写代码解析,对于比较复杂的,还是挺费劲的。在go标准库中提供了一个包:flag,方便进行命令行解析。

注:区分几个概念
1)命令行参数(或参数):是指运行程序提供的参数
2)已定义命令行参数:是指程序中通过flag.Xxx等这种形式定义了的参数
3)非flag(non-flag)命令行参数(或保留的命令行参数):后文解释

一、flag包概述

flag包实现了命令行参数的解析。

定义flags有两种方式:

1)flag.Xxx(),其中Xxx可以是Int、String等;返回一个相应类型的指针,如:

var ip = flag.Int("flagname", 1234, "help message for flagname")

2)flag.XxxVar(),将flag绑定到一个变量上,如:

var flagvar int
flag.IntVar(&amp;flagvar, "flagname", 1234, "help message for flagname")

另外,还可以创建自定义flag,只要实现flag.Value接口即可(要求receiver是指针),这时候可以通过如下方式定义该flag:

flag.Var(&amp;flagVal, "name", "help message for flagname")

注意到,这种方式是没有提供默认值的,所以默认值就是类型的零值。

在所有的flag定义完成之后,可以通过调用flag.Parse()进行解析。

命令行flag的语法有如下三种形式:
-flag // 只支持bool类型
-flag=x
-flag x // 只支持非bool类型

其中第三种形式只能用于非bool类型的flag,原因是:如果支持,那么对于这样的命令 cmd -x *,如果有一个文件名字是:0或false等,则命令的愿意会改变(之所以这样,是因为bool类型支持-flag这种形式,如果bool类型不支持-flag这种形式,则bool类型可以和其他类型一样处理。也正因为这样,Parse()中,对bool类型进行了特殊处理)。默认的,提供了-flag,则对应的值为true,否则为flag.Bool/BoolVar中指定的默认值;如果希望显示设置为false则使用-flag=false。

int类型可以是十进制、十六进制、八进制甚至是负数;bool类型可以是1, 0, t, f, true, false, TRUE, FALSE, True, False。Duration可以接受任何time.ParseDuration能解析的类型

二、类型和函数

在看类型和函数之前,先看一下变量
ErrHelp:该错误类型用于当命令行指定了-help参数但没有定义时。
Usage:这是一个函数,用户输出所有定义了的命令行参数和帮助信息(usage message)。一般,当命令行参数解析出错时,该函数会被调用。我们可以指定自己的Usage函数,即:flag.Usage = func(){}

1、函数

go标准库中,经常这么做:

定义了一个类型,提供了很多方法;为了方便使用,会实例化一个该类型的实例(通用),这样便可以直接使用该实例调用方法。比如:encoding/base64中提供了StdEncoding和URLEncoding实例,使用时:base64.StdEncoding.Encode()

在flag包中,进行了进一步封装:将FlagSet的方法都重新定义了一遍,也就是提供了一序列函数,而函数中只是简单的调用已经实例化好了的FlagSet实例:commandLine 的方法,这样commandLine实例便不需要export。这样,使用者是这么调用:flag.Parse()而不是flag.commandLine.Parse()

这里不详细介绍各个函数,在类型方法中介绍。

2、类型(数据结构)

1)ErrorHandling

type ErrorHandling int

该类型定义了在参数解析出错时错误处理方式定义了三个该类型的常量:

const (
	ContinueOnError ErrorHandling = iota
	ExitOnError
	PanicOnError
)

三个常量在源码的FlagSet方法parseOne()中使用了。

2)Flag

// A Flag represents the state of a flag.
type Flag struct {
	Name     string // name as it appears on command line
	Usage    string // help message
	Value    Value  // value as set
	DefValue string // default value (as text); for usage message
}

Flag类型代表一个flag的状态。

比如:autogo -f abc.txt,代码flag.String(“f”, “a.txt”, “usage”),则该Flag实例(可以通过flag.Lookup(“f”)获得)相应的值为:f, usage, abc.txt, a.txt。

3)FlagSet

// A FlagSet represents a set of defined flags.
type FlagSet struct {
	// Usage is the function called when an error occurs while parsing flags.
	// The field is a function (not a method) that may be changed to point to
	// a custom error handler.
	Usage func()

	name string // FlagSet的名字。commandLine给的是os.Args[0]
	parsed bool // 是否执行过Parse()
	actual map[string]*Flag // 存放实际传递了的参数(即命令行参数)
	formal map[string]*Flag // 存放所有已定义命令行参数
	args []string // arguments after flags // 存放非flag(non-flag)参数
	exitOnError bool // does the program exit if there's an error?
	errorHandling ErrorHandling // 当解析出错时,处理错误的方式
	output io.Writer // nil means stderr; use out() accessor
}

4)Value接口

// Value is the interface to the dynamic value stored in a flag.
// (The default value is represented as a string.)
type Value interface {
	String() string
	Set(string) error
}

所有参数类型需要实现Value接口,flag包中,为int、float、bool等实现了该接口。借助该接口,我们可以自定义flag

三、主要类型的方法(包括类型实例化)

flag包中主要是FlagSet类型。

1、实例化方式

NewFlagSet()用于实例化FlagSet。预定义的FlagSet实例commandLine的定义方式:

// The default set of command-line flags, parsed from os.Args.
var commandLine = NewFlagSet(os.Args[0], ExitOnError)

可见,默认的FlagSet实例在解析出错时会提出程序。

由于FlagSet中的字段没有export,其他方式获得FlagSet实例后,比如:FlagSet{}或new(FlagSet),应该调用Init()方法,初始化name和errorHandling。

2、定义flag参数方法

这一序列的方法都有两种形式,在一开始已经说了两种方式的区别。这些方法用于定义某一类型的flag参数。

3、解析参数(Parse)

func (f *FlagSet) Parse(arguments []string) error

从参数列表中解析定义的flag。参数arguments不包括命令名,即应该是os.Args[1:]。事实上,flag.Parse()函数就是这么做的:

// Parse parses the command-line flags from os.Args[1:].  Must be called
// after all flags are defined and before flags are accessed by the program.
func Parse() {
	// Ignore errors; commandLine is set for ExitOnError.
	commandLine.Parse(os.Args[1:])
}

该方法应该在flag参数定义后而具体参数值被访问前调用。

如果提供了-help参数(命令中给了)但没有定义(代码中),该方法返回ErrHelp错误。默认的commandLine,在Parse出错时会退出(ExitOnError)

为了更深入的理解,我们看一下Parse(arguments []string)的源码:

func (f *FlagSet) Parse(arguments []string) error {
	f.parsed = true
	f.args = arguments
	for {
		seen, err := f.parseOne()
		if seen {
			continue
		}
		if err == nil {
			break
		}
		switch f.errorHandling {
		case ContinueOnError:
			return err
		case ExitOnError:
			os.Exit(2)
		case PanicOnError:
			panic(err)
		}
	}
	return nil
}

真正解析参数的方法是非导出方法parseOne。

结合parseOne方法,我们来解释non-flag以及包文档中的这句话:

Flag parsing stops just before the first non-flag argument (“-” is a non-flag argument) or after the terminator “–”.

我们需要了解解析什么时候停止

根据Parse()中for循环终止的条件(不考虑解析出错),我们知道,当parseOne返回false, nil时,Parse解析终止。正常解析完成我们不考虑。看一下parseOne的源码发现,有两处会返回false, nil。

1)第一个non-flag参数

s := f.args[0]
if len(s) == 0 || s[0] != '-' || len(s) == 1 {
	return false, nil
}

也就是,当遇到单独的一个”-”或不是”-”开始时,会停止解析。比如:

./autogo – -f或./autogo build -f

这两种情况,-f都不会被正确解析。像该例子中的”-”或build(以及之后的参数),我们称之为non-flag参数

2)两个连续的”–”

if s[1] == '-' {
	num_minuses++
	if len(s) == 2 { // "--" terminates the flags
		f.args = f.args[1:]
		return false, nil
	}
}

也就是,当遇到联系的两个”-”时,解析停止。

说明:这里说的”-”和”–”,位置和”-f”这种的一样。也就是说,下面这种情况并不是这里说的:

./autogo -f –

这里的”–”会被当成是f的值

parseOne方法中接下来是处理-flag=x,然后是-flag(bool类型)(这里对bool进行了特殊处理),接着是-flag x这种形式,最后,将解析成功的Flag实例存入FlagSet的actual map中。

另外,在parseOne中有这么一句:

f.args = f.args[1:]

也就是说,没执行成功一次parseOne,f.args会少一个。所以,FlagSet中的args最后留下来的就是所有non-flag参数。

4、Arg(i int)和Args()、NArg()、NFlag()

Arg(i int)和Args()这两个方法就是获取non-flag参数的;NArg()获得non-flag个数;NFlag()获得FlagSet中actual长度(即被设置了的参数个数)。

5、Visit/VisitAll

这两个函数分别用户访问FlatSet的actual和formal中的Flag,而具体的访问方式由调用者决定。

6、PrintDefaults()

打印所有已定义参数的默认值(调用VisitAll),默认输出到标准错误,除非指定了FlagSet的output(通过SetOutput()设置)

7、Set(name, value string)

设置某个flag的值(通过Flag的name)

四、总结

使用建议:虽然上面讲了那么多,一般来说,我们只简单的定义flag,然后parse,就如同开始的例子一样。

如果项目需要复杂或更高级的命令行解析方式,可以试试goptions

如果想要像go工具那样的多命令(子命令)处理方式,可以试试command

十一 292012
 

学习一门语言,熟悉语言语法、规范等之后,应该学习语言的标准库。在Python中,会有一些函数来探究包的内容。在Go中,更多的是通过查看Go标准库文档来学习。

不仅要知其然,更要知其所以然。

实际写代码中,肯定需要用到很多标准库中的包,在学习阶段,可以在需要用某个包时,彻底学习这个包,掌握它。标准库中每个包的文档是学习包最好的资料,一定要仔细看明白。

Go包具体该怎么学了?以下是我自己的学习方法,仅供参考(以time包为例)

1、看文档中的Overview,整体上对该包有一个了解

从这知道,该包用于处理和显示日期和时间。日期都是公历。

2、看Index,关注函数和类型(函数是指不属于某种类型的func),不需要关注类型的方法(Variable和Constants需要大概关注)

1)定义了一些常量,表示时间格式
2)定义了Duration、Location、Month、ParseError、Ticker、Time、Timer和Weekday等类型
从名字可以很容易的知道这些类型所代表的含义
3)有After、Sleep和Tick三个全局函数

其实全局函数还有其他的,但是其他全局函数在文档中放在了某种类型的下面,表示该全局函数会生成一个该类型的实例,这个可以当做类型的方法来研究

这一步,我们可以不关心这些类型该怎么使用,我们关心全局函数怎么用
①func After(d Duration) 在【Index】视图 点击该函数,跳转到该函数的说明处。
函数注释已经说的很清楚:指定的时间过去之后,将当前发送到chanel中返回,和NewTimer(d).C功能相同。另外,还提供了使用示例。
②func Sleep(d Duration)
很明显,暂停当前goroutine一段时间
③func Tick(d Duration) 这是包设计中 设计函数 常用的方式:方便包的使用者,直接使用Tiker类型,而不需要另外实例化再调用其方法。Tick方法适用于那些需要使用Tiker,但是永远不需要停止的场景。

以上三个函数文档都提供了对应的例子

3、关注类型及其方法 (以Duration为例)

1)看类型的定义(结构)

type Duration int64
类型注释说,Duration表示两个时间的间隔,单位是纳秒(nanosecond)
该类型预定义了一些常量,这样方便类型转换、计算以及阅读。另外还提供了一些示例。

2)实例化方式

由于Duration是int64的别名,整型字面值常量可以直接赋值给Duration。对于整型类型的变量,则需要类型转换
比如,Sleep函数接收Duration类型的参数。如果想Sleep 2秒,这样写:Sleep(2 * time.Second)或者Sleep(2e9)。或者 i := 2; Sleep(time.Duration(i) * time.Second)

另外,ParseDuration可以将字符串转为Duration;Since()也可以获取Duration的实例

3)提供的方法

分别转为时、分、纳秒、秒。除了纳秒,其他都是float64。
这里说一下String()这个方法:
△如果某种类型定义了String()方法,那么,fmt包格式化时,直接传入类型,内部会调用类型的String()方法进行格式化,类似于Java中toString方法

4、自己写几个例子练练手

比如,想要格式化输出当前时间,格式为:2012-11-27 15:10:00
time.Now().Format(“2006-01-02 15:04:05″)

注意,2006这样的必须固定,不能为2007什么的。(不知道为什么这么设计)

设计了一个包实现类似PHP中date()和strtotime()的功能

https://github.com/polaris1119/times

5、学习第三方包(几种生成文档的方式)

1)将包放入$GOROOT/src/pkg/ 目录下,godoc 时会生成该包的文档(当做标准库包了)
2)设定GOPATH,将第三方包放在GOPATH中
3)使用godoc命令时,通过path指定第三方包的路径
4)直接通过godoc命令在命令行看起文档(不直观)
5)http://go.pkgdoc.org/在这个网站搜索包,有的话,会提供文档(这个网站会搜索github上的资源)

十一 292012
 

这里以第三方包goptions为例说明:(只说核心数据结构)

1、定义数据结构(struct等),如FlagSet、Flag,以及数据结构对应的方法。(这里一般会提供实例化数据结构的方法,比如:NewFlagSet())

一般地,依赖这些就可以供外部使用(一般会就该包的功能提供一个外部可用的入口)
比如,外部可以这么使用goptions这个包:
options := struct{}{}
flagSet := goptions.NewFlagSet(filepath.Base(os.Args[0]), &options)
flagSet.Parse(os.Args[1:])

相当于自己实现了goptions包中的Parse()或ParseAndFail()

这样,每个调用者都得这么实现一次。

2、定义函数,提供给外部方便调用的接口;

即:将上面的封装成函数供使用者直接使用。
goptions提供了两种封装:
1)普通封装->Parse(),错误需要自己处理;
2)完全封装->ParseAndFail(),错误都处理好,比显示出友好的错误提示
当然,这里之所以处理错误,正如文档中说的,出错应该是编译期错误,而不是在运行时报出来

3、另外,为了区分不同的错误提示不一样,包中经常会自定义自己的错误类型,比如:

var ErrHelpRequest = errors.New(“Request for Help”)

☆包的其他方面

1、测试用例

以_test.go为后缀,测试方法包含唯一参数:t *testing.T

2、跨平台

以 _$GOOS.go为后缀。如
windows平台以 _windows.go为后缀
Linux平台以 _linux.go为后缀
Mac平台以_darwin.go

3、为包提供例子

以example_test.go为文件名,包名为:要包名加上_test,函数名为Example加上实际的函数。如:
为time包中的After()函数提供例子
文件名:example_test.go,包名:time_test,函数:ExampleAfter()

上面是建议的命名规则,实际上,两处是必须的,其他的地方无所谓,但建议标准库中的方式写。必须的两处是:
①文件名必须以_test.go为后缀(这和测试用例一样)
②为哪个函数提供例子,必须以Example开头,后接函数名
另外,如果函数名最后加上_xxx,则文档中,xxx会变为(xxx)
在 Example 函数尾部用”// Output: ” 来标注正确输出结果