跳转至

Clash 源码详解

Clash 源码详解系列集锦

众所周知,Clash for Windows 衫裤跑路了,让我默哀一分钟,For Freedom!!!

所以一个奇怪的想法出现了,我能否扛起大旗走下去!...我瞎说的,第一步就先读懂源码吧。

【第一弹】Clash 中缓存管理

程序初始化会读取用户目录下的 ~/.config/clash/* 配置文件

执行 tree ~/.config/clash/* 可以查看 clash 配置文件结构如下:

├── cache.db
├── config.ini
├── config.yaml
├── Country.mmdb
├── logs
   ├── 2024-02-28-144228.log
   ├── 2024-03-04-175206.log
   ├── 2024-03-04-175247.log
   └── 2024-03-06-121729.log
└── profiles
    ├── 1703153138868.yml
    ├── 1709629664192.yml
    └── list.yml

2 directories, 11 files

源码解析

constant/config.go 包含了初始化代码:

1. 获取用户目录,初始化 HomeDir

这是一段很标准的获取用户 HOME 目录的代码,我们可以灵活的应用在其他项目中。

currentUser, err := user.Current()
if err != nil {
    dir := os.Getenv("HOME")
    if dir == "" {
        log.Fatalf("Can't get current user: %s", err.Error())
    }
    HomeDir = dir
} else {
    HomeDir = currentUser.HomeDir
}

2. 初始化 ~/.config 文件;

dirPath := path.Join(HomeDir, ".config", Name)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
    if err := os.MkdirAll(dirPath, 0777); err != nil {
        log.Fatalf("Can't create config directory %s: %s", dirPath, err.Error())
    }
}

3. 初始化 ~/.config/config.ini 文件;

ConfigPath = path.Join(dirPath, "config.ini")
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
    log.Info("Can't find config, create a empty file")
    os.OpenFile(ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
}

有趣的是,作者没有使用 func Create(name string) (*File, error) 方法, 定义如下:

// Create creates or truncates the named file. If the file already exists,
// it is truncated. If the file does not exist, it is created with mode 0666
// (before umask). If successful, methods on the returned File can
// be used for I/O; the associated file descriptor has mode O_RDWR.
// If there is an error, it will be of type *PathError.
func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

作者使用了 O_WRONLY0644 权限更加合适。

这些数字和权限的映射关系如下:

4:读权限(Read) 2:写权限(Write) 1:执行权限(eXecute) 数字的意义是各个权限的相加。例如,0666 表示 4 (读) + 2 (写) + 2 (写) + 1 (执行) + 1 (执行) + 1 (执行)。

4. 初始化 Country.mmdb

MMDBPath = path.Join(dirPath, "Country.mmdb")
if _, err := os.Stat(MMDBPath); os.IsNotExist(err) {
    log.Info("Can't find MMDB, start download")
    err := downloadMMDB(MMDBPath)
    if err != nil {
        log.Fatalf("Can't download MMDB: %s", err.Error())
    }
}

这个真是太重要了,属于是 Clash 本地存储方案的核心。

在项目方案设计是,我们可以借鉴这个思想。

不过为什么没有用 sqllite 我们可以探讨一下:

"MMDB" 和 "SQLite" 是两种完全不同的数据库系统,用于不同的用途,并有一些关键的区别。

  1. 类型:
  2. MMDB(MaxMind DB): 是MaxMind公司创建的数据库格式,主要用于存储地理位置信息,特别是IP地址与地理位置的映射关系。它不是通用的数据库管理系统,而是专注于提供对地理位置信息的高效检索。
  3. SQLite: 是一种嵌入式数据库引擎,它是一种关系型数据库管理系统(RDBMS)。SQLite是一款轻量级、自包含的数据库,适用于嵌入式系统和移动应用等场景。

  4. 用途:

  5. MMDB: 主要用于 IP 地址定位,以确定访问者的地理位置信息。通常在网络分析、广告定向和一些安全性检查中使用。
  6. SQLite: 可以用于各种通用的数据库需求,包括嵌入式系统、移动应用、桌面应用和小型服务等。它支持 SQL 查询语言,可以存储和检索多种类型的数据。

  7. 数据模型:

  8. MMDB: 使用特定的树状结构,适用于高效的IP地址到地理位置信息的查找。
  9. SQLite: 遵循关系型数据库模型,支持表、行和列的概念,以及 SQL 查询语言。

  10. 性能和用途限制:

  11. MMDB: 针对特定的地理位置查询优化,对于其他类型的数据存储和查询并不适用。
  12. SQLite: 虽然可以处理各种数据类型,但在大规模高并发读写的情况下可能不如一些专门设计的数据库系统。

总的来说,MMDB 和 SQLite 面向不同的应用场景。MMDB 更适合处理与地理位置信息相关的数据,而 SQLite 是一种通用的关系型数据库引擎,适用于各种数据存储和检索需求。

func downloadMMDB(path string) (err error) {
    resp, err := http.Get("http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz")
    if err != nil {
        return
    }
    defer resp.Body.Close()

    gr, err := gzip.NewReader(resp.Body)
    if err != nil {
        return
    }
    defer gr.Close()

    tr := tar.NewReader(gr)
    for {
        h, err := tr.Next()
        if err == io.EOF {
            break
        } else if err != nil {
            return err
        }

        if !strings.HasSuffix(h.Name, "GeoLite2-Country.mmdb") {
            continue
        }

        f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            return err
        }
        defer f.Close()
        _, err = io.Copy(f, tr)
        if err != nil {
            return err
        }
    }

    return nil
}

总览

var (
    HomeDir    string
    ConfigPath string
    MMDBPath   string
)


func init() {
    currentUser, err := user.Current()
    if err != nil {
        dir := os.Getenv("HOME")
        if dir == "" {
            log.Fatalf("Can't get current user: %s", err.Error())
        }
        HomeDir = dir
    } else {
        HomeDir = currentUser.HomeDir
    }

    dirPath := path.Join(HomeDir, ".config", Name)
    if _, err := os.Stat(dirPath); os.IsNotExist(err) {
        if err := os.MkdirAll(dirPath, 0777); err != nil {
            log.Fatalf("Can't create config directory %s: %s", dirPath, err.Error())
        }
    }

    ConfigPath = path.Join(dirPath, "config.ini")
    if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
        log.Info("Can't find config, create a empty file")
        os.OpenFile(ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
    }

    MMDBPath = path.Join(dirPath, "Country.mmdb")
    if _, err := os.Stat(MMDBPath); os.IsNotExist(err) {
        log.Info("Can't find MMDB, start download")
        err := downloadMMDB(MMDBPath)
        if err != nil {
            log.Fatalf("Can't download MMDB: %s", err.Error())
        }
    }
}

func downloadMMDB(path string) (err error) {
    resp, err := http.Get("http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz")
    if err != nil {
        return
    }
    defer resp.Body.Close()

    gr, err := gzip.NewReader(resp.Body)
    if err != nil {
        return
    }
    defer gr.Close()

    tr := tar.NewReader(gr)
    for {
        h, err := tr.Next()
        if err == io.EOF {
            break
        } else if err != nil {
            return err
        }

        if !strings.HasSuffix(h.Name, "GeoLite2-Country.mmdb") {
            continue
        }

        f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
        if err != nil {
            return err
        }
        defer f.Close()
        _, err = io.Copy(f, tr)
        if err != nil {
            return err
        }
    }

    return nil
}