#844 Go DNS 解析

2022-10-30

A 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    // 方法 1
    // func ResolveIPAddr(network, address string) (*IPAddr, error)
    ipAddr, err := net.ResolveIPAddr("ip", "www.google.com")
    if err != nil {
        fmt.Println("解析IP地址失败:", err)

    } else {
        fmt.Println("IP地址是:", ipAddr.IP)
    }

    // 方法 2
    // func LookupMX(name string) ([]*MX, error)
    // func LookupTXT(name string) ([]string, error)
    // func LookupIP(host string) ([]IP, error)
    // func LookupHost(host string) ([]string, error) // only IPv4
    ips, err := net.LookupHost("www.google.com")
    if err != nil {
        fmt.Println("解析主机名失败:", err)
    } else {
        for _, ip := range ips {
            fmt.Println("IP地址是:", ip)
        }
    }
}

MX 记录

package main

import (
    "fmt"
    "net"
)

func main() {
    records, err := net.LookupMX("qq.com")
    if err != nil {
        fmt.Println("解析MX记录失败:", err)
    } else {
        fmt.Printf("%#v", records)
    }
}

指定 DNS 服务器

func queryMX(dns string, domain string) ([]*net.MX, error) {
    resolver := &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver.LookupMX(context.Background(), domain)
}

完整版本

package main

import (
    "context"
    "fmt"
    "net"
    "strings"
    "time"
)

var dnsServers = []string{
    "223.5.5.5:53",
    "114.114.114.114:53",
    "8.8.8.8:53",
}

type IPType int

const (
    NotIP IPType = iota
    IPv4
    IPv6
)

const (
    DNSLookupTimeout = 500 * time.Millisecond // time.Duration
)

func GetIPType(host string) IPType {
    ip := net.ParseIP(host)
    if ip == nil {
        return NotIP
    }
    if ip.To4() != nil {
        return IPv4
    }
    if ip.To16() != nil {
        return IPv6
    }
    return NotIP
}

type Resolver struct {
    DNS      string
    Resolver *net.Resolver
}

func NewResolver(dns string) Resolver {
    resolver := Resolver{}
    resolver.DNS = dns
    resolver.Resolver = &net.Resolver{
        PreferGo: true,
        Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
            dialer := &net.Dialer{}
            return dialer.DialContext(ctx, "udp", dns)
        },
    }
    return resolver
}

func (r Resolver) QueryA(domain string) ([]string, error) {
    return r.Resolver.LookupHost(context.Background(), domain)
}

func (r Resolver) QueryMX(domain string) ([]*net.MX, error) {
    ctx, cancel := context.WithTimeout(context.Background(), DNSLookupTimeout)
    defer cancel()
    return r.Resolver.LookupMX(ctx, domain)
}

func (r Resolver) Query(domain string) ([]*net.MX, error) {
    addrs, err := r.QueryMX(domain)
    if err != nil {
        return nil, err
    }
    var result []*net.MX
    for _, addr := range addrs {
        // hostname := strings.TrimRight(addr.Host, ".")
        ipType := GetIPType(addr.Host)
        fmt.Printf("%s: %#v\n", addr.Host, ipType)
        if ipType == NotIP {
            ips, err := r.QueryA(addr.Host)
            if err != nil {
                continue
            }
            for _, ip := range ips {
                result = append(result, &net.MX{Host: ip, Pref: addr.Pref})
            }
        }
    }
    return result, nil
}

var resolvers map[string]Resolver

func Query(domain string) ([]*net.MX, error) {
    var addrs []*net.MX
    var err error
    for _, dns := range dnsServers {
        var r Resolver
        if r, ok := resolvers[dns]; !ok {
            r = NewResolver(dns)
            resolvers[dns] = r
        }
        addrs, err = r.Query(domain)
        if err == nil {
            break
        }
        fmt.Printf("Error: %s: %s: %#v\n", dns, domain, err)
    }
    return addrs, err
}

func QueryAsync(domain string, ch chan<- []*net.MX, errCh chan<- error) {
    addrs, err := Query(domain)
    if err != nil {
        errCh <- err
        return
    }
    ch <- addrs
}

func init() {
    resolvers = make(map[string]Resolver)
}

func main() {
    {
        domains := []string{"qq.com", "gmail.com", "google.com"}
        for _, domain := range domains {
            fmt.Println(strings.Repeat("=", 100))
            addrs, err := Query(domain)
            if err != nil {
                fmt.Printf("Error: %#v\n", err)
            } else {
                for _, addr := range addrs {
                    fmt.Printf("%#v\n", addr)
                }
            }
        }
    }

    {
        fmt.Println(strings.Repeat("=", 100))
        ch := make(chan []*net.MX)
        errCh := make(chan error)
        go QueryAsync("google.com", ch, errCh)
        select {
        case addrs := <-ch:
            fmt.Println("MX Records:")
            for _, addr := range addrs {
                fmt.Printf("%#v\n", addr)
            }
        case err := <-errCh:
            fmt.Println("Error:", err)
        }
    }
}

#843 CSDN 免登陆复制

2022-10-27

CSDN 会拦截复制,提示需要登录。

近日在网上学到一招,在控制台输入一行代码就好了:

document.designMode = "on";

#842 ping github.com 得到 127.0.0.1 问题

2022-10-27
git clone git@github.com:tornado/tornado
Cloning into 'tornado'...
ssh: connect to host github.com port 22: Connection refused
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
ping github.com

正在 Ping github.com [::1] 具有 32 字节的数据:
来自 ::1 的回复: 时间<1ms
来自 ::1 的回复: 时间<1ms
来自 ::1 的回复: 时间<1ms

::1 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 0ms,平均 = 0ms
Control-C
ping -4 github.com

正在 Ping github.com [127.0.0.1] 具有 32 字节的数据:
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64
来自 127.0.0.1 的回复: 字节=32 时间<1ms TTL=64

127.0.0.1 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 0ms,最长 = 0ms,平均 = 0ms
Control-C
nslookup github.com
Server:         192.168.31.1
Address:        192.168.31.1#53

Non-authoritative answer:
Name:   github.com
Address: 127.0.0.1
Name:   github.com
Address: ::1

配置 DNS 为 AliDNS 223.5.5.5233.6.6.6

nslookup github.com
Server:         223.5.5.5
Address:        223.5.5.5#53

Non-authoritative answer:
Name:   github.com
Address: 20.205.243.166

问题依旧。

ping 20.205.243.166

正在 Ping 20.205.243.166 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。

20.205.243.166 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 0,丢失 = 3 (100% 丢失),
Control-C
ping baidu.com

正在 Ping baidu.com [39.156.66.10] 具有 32 字节的数据:
来自 39.156.66.10 的回复: 字节=32 时间=23ms TTL=52
来自 39.156.66.10 的回复: 字节=32 时间=24ms TTL=52
来自 39.156.66.10 的回复: 字节=32 时间=23ms TTL=52

39.156.66.10 的 Ping 统计信息:
    数据包: 已发送 = 3,已接收 = 3,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 23ms,最长 = 24ms,平均 = 23ms
Control-C

总结

  1. hosts 没有特殊配置
  2. 改成可用 DNS,问题没有修复
  3. ipconfig /flushdns 也不管用
  4. 重启也没有好

那就奇怪了,这个 ping 里面的本地回环地址是哪里来的呢?

最后,

ipconfig /displaydns 可以看到,还是用的回环地址
ipconfig /all 发现还有一个 DNSv6 配置。。。

DNSv6 设置成 AliDNS 的 IPv6 地址:2400:3200::12400:3200:baba::1 再试,

ping github.com

正在 Ping github.com [20.205.243.166] 具有 32 字节的数据:
请求超时。
请求超时。
请求超时。
请求超时。

20.205.243.166 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),

终于好了。😂

事情的起源是 git clone 失败,最后 git clone 可以了。
ping 不通,可能是网络问题,也可能是 github 网络配置。
这就不管了。

#841 日志平台的设计

2022-10-07

简单的日志系统

我还没有用过 ELK 这样的系统(只实验性使用过 Graylog),使用过这些日志管理方案:

  1. 日志就通过文件存放在服务器上,然后登录服务器进行日志文件分析,排查问题。
  2. 再进一步就是,将日志文件定期 rsync 到一台服务器上,方便日志管理与日志搜索。
  3. 服务直接写 syslog,然后配置好 rsyslog 同步就行。

    graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
    
    A(应用程序):::process -->|写入日志| B(syslog 接口):::process
    B -->|发送日志消息| C(本地 rsyslog 守护进程):::process
    C -->|存储本地日志| D(本地日志文件):::process
    C -->|配置转发规则| E{是否转发到远程?}:::process
    E -->|是| F(通过 UDP/TCP 发送):::process
    F -->|接收日志消息| G(远程 rsyslog 守护进程):::process
    G -->|存储远程日志| H(远程日志文件):::process
    E -->|否| D
    
  4. 不要忘了,日志写入数据库在某些场景下也是一个可选方案,比如内部管理系统的登录日志、操作日志等。

理论上日志可以直接写入远程日志系统,但是我想应该不会有线上服务这样做。网络稳定性问题(可能丢失日志、服务阻塞)、性能消耗、增加系统复杂性等。
syslog 是系统提供的日志接口,rsyslog 提供了 TCP 日志可靠传输、本地日志队列功能,而且是 Linux 世界广泛采用的基础服务,我觉得可以接受。

日志系统

  • ELK / EFK:

    Elasticsearch   # 搜索引擎
    Logstash        # 日志采集、过滤、预处理
    Kabana          # 数据可视化
    
    Filebeat        # Logstash 替代方案,更加轻量级
    
    graph LR
        Logs --> Filebeat --> ES
        Logs --> Logstash --> ES
    
  • FELK:Filebeat 将日志采集到 Logstash,处理之后导入 ES

    graph LR
        Logs --> Filebeat --> Logstash --> ES
    
  • FELK + Kafka

    Filebeat 将日志采集到 Kafka,再由 Logstash 从 Kafka 读取日志,处理完成之后导入 ES

    graph LR
        Logs --> Filebeat --> Kafka --> Logstash --> ES
    
  • Graylog:

    Filebeat
    Graylog Sidebar
    Graylog
    Elasticsearch
    MongoDB
    
  • LPG:

    Loki        # 搜索引擎
    Promtail    # 日志采集、过滤、预处理
    Grafana     # 数据可视化
    

设计

graph TD
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A(服务):::process -->|生成日志| B(rsyslog):::process
    B --> C(Kafka):::process
    C --> D(ELK):::process
  1. 服务写 syslog,通过 rsyslog 同步到

接收,解析,处理,转换,格式化

#840 gob:Golang 二进制编码格式(标准库)

2022-10-01
package main

import (
    "bytes"
    "encoding/gob"
    "fmt"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    // 编码
    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    err := enc.Encode(Person{"John", 30})
    if err != nil {
        panic(err)
    }

    // 解码
    dec := gob.NewDecoder(&buf)
    var p Person
    err = dec.Decode(&p)
    if err != nil {
        panic(err)
    }

    fmt.Println(p)
}

#839 Golang 源码结构

2022-09-16
# tree -L 1 -d .
.
├── api
├── doc
├── lib
├── misc
├── src
└── test

6 directories
# tree -L 1 -d src/
src/
├── archive
├── bufio
├── builtin
├── bytes
├── cmd
├── compress
├── container
├── context
├── crypto
├── database
├── debug
├── embed
├── encoding
├── errors
├── expvar
├── flag
├── fmt
├── go
├── hash
├── html
├── image
├── index
├── internal
├── io
├── log
├── math
├── mime
├── net
├── os
├── path
├── plugin
├── reflect
├── regexp
├── runtime
├── sort
├── strconv
├── strings
├── sync
├── syscall
├── testdata
├── testing
├── text
├── time
├── unicode
├── unsafe
└── vendor

46 directories
tree -L 2 -d src/net/
src/net/
├── http
│   ├── cgi
│   ├── cookiejar
│   ├── fcgi
│   ├── httptest
│   ├── httptrace
│   ├── httputil
│   ├── internal
│   ├── pprof
│   └── testdata
├── internal
│   └── socktest
├── mail
├── rpc
│   └── jsonrpc
├── smtp
├── testdata
├── textproto
└── url

19 directories

#838 Golang UUID

2022-09-13

awesome-go 上面列出来的项目:

  • jakehl/goid  - Generate and Parse RFC4122 compliant V4 UUIDs.
  • twharmon/gouid  - Generate cryptographically secure random string IDs with just one allocation.
  • aidarkhanov/nanoid  - A tiny and efficient Go unique string ID generator.
  • muyo/sno  - Compact, sortable and fast unique IDs with embedded metadata.
  • oklog/ulid  - Go implementation of ULID (Universally Unique Lexicographically Sortable Identifier).
  • uniq - No hassle safe, fast unique identifiers with commands.
  • agext/uuid  - Generate, encode, and decode UUIDs v1 with fast or cryptographic-quality random node identifier.
  • gofrs/uuid  - Implementation of Universally Unique Identifier (UUID). Supports both creation and parsing of UUIDs. Actively maintained fork of satori uuid.
  • google/uuid  - Go package for UUIDs based on RFC 4122 and DCE 1.1: Authentication and Security Services.
  • edwingeng/wuid  - An extremely fast globally unique number generator.
  • rs/xid  - Xid is a globally unique id generator library, ready to be safely used directly in your server code.

我自己又在 GitHub 上搜罗了几个:

package main

import (
    "os/exec"

    guuid "github.com/google/uuid"
    suuid "github.com/satori/go.uuid"
)

func UseGoogle() string {
    id := guuid.New()
    return id.String()
}

func UseSatori() string {
    id := suuid.NewV4()
    return id.String()
}

func UseUuidgen() string {
    id, _ := exec.Command("uuidgen").Output()
    return string(id)
}

func main() {
    fmt.Println("Google UUID:", UseGoogle())
    fmt.Println("Satori UUID:", UseSatori())
    fmt.Println("Uuidgen UUID:", UseUuidgen())
}

#837 Go 谚语

2022-09-13

得知了 Go 谚语这么个东西,搜索一番,找到:

  • https://go-proverbs.github.io/
  • https://github.com/jboursiquot/go-proverbs
  • https://www.youtube.com/watch?v=PAAkCSZUG1c

原来是类似 Python 之禅(import this)一样的东西。

  1. Don't communicate by sharing memory, share memory by communicating. 别通过共享内存通信,要通过通信共享内存。
  2. Concurrency is not parallelism. 并发不是并行。
  3. Channels orchestrate; mutexes serialize.
  4. The bigger the interface, the weaker the abstraction. 接口越大, 抽象越弱。
  5. Make the zero value useful.
  6. interface{} says nothing.
  7. Gofmt's style is no one's favorite, yet gofmt is everyone's favorite. 别争论风格问题,不管喜不喜欢,用 gofmt 就是了。
  8. A little copying is better than a little dependency. 少量的复制好过引入新的依赖。
  9. Syscall must always be guarded with build tags.
  10. Cgo must always be guarded with build tags.
  11. Cgo is not Go. 别用 cgo。
  12. With the unsafe package there are no guarantees. 慎用 unsafe 包(安全无保障)
  13. Clear is better than clever. 清晰的代码比聪明的代码好。
  14. Reflection is never clear. 慎用反射(代码不清晰)。
  15. Errors are values.
  16. Don't just check errors, handle them gracefully.
  17. Design the architecture, name the components, document the details.
  18. Documentation is for users. 文档是给用户的(注意代码本身的可读性)。
  19. Don't panic. 不要滥用 panic

#836 Protobuf 枚举类型的实现

2022-09-12
enum Gender {
    UNKNOWN = 0;
    MALE = 1;
    FEMALE = 2;
}

转换成 Golang 之后是这个样子:

type Person_Gender int32

const (
    Person_UNKNOWN Person_Gender = 0
    Person_MALE    Person_Gender = 1
    Person_FEMALE  Person_Gender = 2
)

// Enum value maps for Person_Gender.
var (
    Person_Gender_name = map[int32]string{
        0: "UNKNOWN",
        1: "MALE",
        2: "FEMALE",
    }
    Person_Gender_value = map[string]int32{
        "UNKNOWN": 0,
        "MALE":    1,
        "FEMALE":  2,
    }
)

func (x Person_Gender) Enum() *Person_Gender {
    p := new(Person_Gender)
    *p = x
    return p
}

func (x Person_Gender) String() string {
    return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}

func (Person_Gender) Descriptor() protoreflect.EnumDescriptor {
    return file_example_proto_enumTypes[0].Descriptor()
}

func (Person_Gender) Type() protoreflect.EnumType {
    return &file_example_proto_enumTypes[0]
}

func (x Person_Gender) Number() protoreflect.EnumNumber {
    return protoreflect.EnumNumber(x)
}

// Deprecated: Use Person_Gender.Descriptor instead.
func (Person_Gender) EnumDescriptor() ([]byte, []int) {
    return file_example_proto_rawDescGZIP(), []int{0, 0}
}

#835 Protobuf 二进制编码

2022-09-12

相较于文本编码的 JSON

  • 速度快(序列化和反序列化)
  • 数据小(二进制数据流更加紧凑)
  • 数据类型丰富(enum,map...)
  • 可读性差

数据类型

Protobuf 类型 描述 Go 对应类型
int32 有符号 32 位整数,采用可变长度编码,适合较小的整数 int32
int64 有符号 64 位整数,采用可变长度编码 int64
uint32 无符号 32 位整数,采用可变长度编码 uint32
uint64 无符号 64 位整数,采用可变长度编码 uint64
sint32 有符号 32 位整数,采用 ZigZag 编码,适合负数较多的情况 int32
sint64 有符号 64 位整数,采用 ZigZag 编码 int64
fixed32 无符号 32 位整数,固定 4 字节编码,对于大整数比 uint32 更高效 uint32
fixed64 无符号 64 位整数,固定 8 字节编码 uint64
sfixed32 有符号 32 位整数,固定 4 字节编码 int32
sfixed64 有符号 64 位整数,固定 8 字节编码 int64
float 单精度 32 位浮点数 float32
double 双精度 64 位浮点数 float64
bool 布尔值,取值为 truefalse bool
string 包含 UTF - 8 编码或 7 位 ASCII 文本的字符串,长度不能超过 2^32 string
bytes 任意的字节序列,长度不能超过 2^32 []byte
message 自定义的数据结构,由多个字段组成,可以嵌套定义 struct
enum 用于定义一组命名的常量值 Go 枚举实现
repeated 在字段前加上 repeated 关键字表示该字段是一个数组 Go 数组
map<KeyType, ValueType> 从 proto3 开始支持的映射类型,用于表示键值对 map
  1. 枚举类型,有另一篇文章可供参考:Protobuf 枚举类型的实现

  2. 数组(Repeated)示例:

    message Person {
        repeated string hobbies = 5;
    }
    
  3. 映射(Map)示例:

    message Person {
        map<string, string> contacts = 6;
    }
    

    这里 contacts 是一个键为字符串、值也为字符串的映射。

基本流程

  1. 定义数据结构(.proto 文件)
  2. 使用编译器生成目标编程语言代码
  3. 在程序中引入生成的代码

  4. Protobuf 编译器: protoc,作用是生成目标语言的数据定义代码
    生成的代码
    需要到 GitHub 下载。

  5. Protobuf 运行时,作用是序列化和反序列化的基础库

    Lang Repo
    C++ https://github.com/protocolbuffers/protobuf/blob/main/src
    Java https://github.com/protocolbuffers/protobuf/blob/main/java
    Python https://github.com/protocolbuffers/protobuf/blob/main/python
    Golang https://github.com/protocolbuffers/protobuf-go

示例

  1. proto 文件

    syntax = "proto3";
    
    package example;
    
    message Person {
        enum Gender {
            UNKNOWN = 0;
            MALE = 1;
            FEMALE = 2;
        }
        string name = 1;
        int32 age = 2;
        Gender gender = 3;
    }
    
  2. 编译:

    $ protoc --go_out=. --go_opt=Mexample.proto=./generated example.proto
    $ find .
    .
    ./example.proto
    ./generated
    ./generated/example.pb.go
    
  3. 调用 proto 生成的 Go 代码

    package main
    
    import (
        "fmt"
        "log"
    
        pb "example/generated" // 导入生成的包
    
        "google.golang.org/protobuf/proto"
    )
    
    func main() {
        // 创建一个 Person 实例
        person := &pb.Person{
            Name:   "Alice",
            Age:    30,
            Gender: pb.Person_FEMALE,
        }
    
        // 序列化
        data, err := proto.Marshal(person)
        if err != nil {
            log.Fatalf("Failed to marshal: %v", err)
        }
    
        // 反序列化
        newPerson := &pb.Person{}
        err = proto.Unmarshal(data, newPerson)
        if err != nil {
            log.Fatalf("Failed to unmarshal: %v", err)
        }
    
        fmt.Printf("Deserialized person: %+v\n", newPerson)
    }
    
    $ go mod init example
    $ go mod tidy
    
    $ go build -o main.exe .
    $ ./main.exe
    Deserialized person: name:"Alice" age:30 gender:FEMALE