frpc 启动流程

背景

Go 语言的几个特性:

  • 没有构造函数
  • 一个文件就是一个模块
  • 每个模块存在一个 func init(),初次调用模块时调用

fpr 整个代码的组织是比较清晰的,使用 ls 可以看到这样的目录结构结构

frpc 如何停 frpc启动_运维

其中在这里需要比较注意的两个文件夹分别是:

  • cmd : 存放 frpc 和 frps 程序的源代码
  • pkg : frp 开发的基础组件库

frpc 如何停 frpc启动_frpc 如何停_02

可以看到 pkg 下都是一些基础的组件库,跟 frp 项目本身关系不大,是一些基础组件(也就是开发其他项目也可以使用);可以看到像是验证、配置、协议这些基础公共组件,它们没有与 frp 的核心业务内网穿透强相关。

frpc 如何停 frpc启动_运维_03

然后 cmd 下则存储着 frpsfrpc 的入口 main.go,本次先分析 frpc 的原理,在分析的时候可以先忽略 frpc 调用 pkg 下组件的细节,先关心 cmd/frpc 下的内容。

frpc-启动流程

frpc 如何停 frpc启动_启动流程_04

完整流程

可以看到 frpc 的启动流程是很简单的,整个启动的流程的启动依靠于 Go 的 init 机制,在 root.go 里可以看到:

func init() {
    rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
    rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")

    kcpDoneCh = make(chan struct{})
}

在这个函数调用完之后, proc.go 下的 doInit 将会被调用,完成整个 frpc 的启动。

func init() 里可以看到一个重要的全局变量 rootCmd,它的初始化位置如下:

var rootCmd = &cobra.Command{
    Use:   "frpc",
    Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
    RunE: func(cmd *cobra.Command, args []string) error {
        if showVersion {
            fmt.Println(version.Full())
            return nil
        }

        // Do not show command usage here.
        err := runClient(cfgFile)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
        return nil
    },
}

这是由 command.go 模块提供的支持,这里不展开,但是注意到 RunE 变量,它指定了要运行的任务,在 command.go 中存在对其的描述 RunE: Run but returns an error.

观察 runClient 的实现,如下:

func runClient(cfgFilePath string) error {
    cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
    if err != nil {
        return err
    }
    return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath)
}

可以看到启动服务器可以分两部分:

  • 参数解析 : 将 ini 配置反序列化为内存对象
  • frpc 启动: 根据配置对象启动客户端

参数解析

runClient 中的参数解析的语句是:

cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath)
    if err != nil {
        return err
    }

实现依靠于 pkg/config 组件,最大的特点是将 ini 配置文件拆分成三个配置对象 cfgpxyCfgsvisitorCfgs

它们的原型对象分别为:

cfg -> ClientCommonConf
pxyCfgs -> map[string]ProxyConf
visitorCfgs -> map[string]VisitorConf

它们的职责可以用一个案例给出来,如下是我的 frpc.ini 配置文件:

[common]
server_addr = x.x.x.x
server_port = 8012
login_fail_exit = false
tls_enable = true
authentication_method = token
token = 082116ea-1fcd-4737-944e-56e5078fa8fb
admin_port = 20000
admin_user = root
admin_pwd = 123456

[netdata2]
type = tcp
local_ip = 127.0.0.1
local_port = 19999
remote_port = 24999

而使用 vscode 去看一下这三个对象的值,可以看到:

ClientCommonConf:

frpc 如何停 frpc启动_启动流程_05

map[string]ProxyConf:

frpc 如何停 frpc启动_内网穿透_06

然后 visitorCfgs 没有值,因为我的 ini 里面没有配置,目前没有用到,所以先不了解,但是可以发现 frp 按照 ini 中的 section 对配置对象进行了分配,common 对应于 ClientCommonConf ,而代理实例则对应于 map[string]ProxyConf

frpc-启动

服务器启动的实现是在 startService 中实现的,它需要三个配置对象以及配文件作为输入参数,其中核心代码为:

// 创建一个新的服务实例
svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile)
// 运行
err = svr.Run()

心得

为了看得懂 go 的代码,抽空花了一点时间走马观花浏览了一下 go 的基本使用方式,感觉挺独特的一门语言,主要如下:

  • 一个文件即为一个模块,一个类
  • 访问权限不使用 publicprivate ,而是使用标识符的名称,大写即为 public ,小写即为 private
  • 定义对象的方式比较特别,比如 bool swap(int* num1, int* num2) 在 go 中写成 func swap(num1 *int, sum2 *int) bool
  • chan 管道操作以及协程的使用

总而言之,还是得过一遍 go 的基本语法,要不然看代码都有点似懂非懂的感觉。