frpc 启动流程
背景
Go 语言的几个特性:
- 没有构造函数
- 一个文件就是一个模块
- 每个模块存在一个
func init()
,初次调用模块时调用
fpr
整个代码的组织是比较清晰的,使用 ls
可以看到这样的目录结构结构
其中在这里需要比较注意的两个文件夹分别是:
- cmd : 存放 frpc 和 frps 程序的源代码
- pkg : frp 开发的基础组件库
可以看到 pkg
下都是一些基础的组件库,跟 frp
项目本身关系不大,是一些基础组件(也就是开发其他项目也可以使用);可以看到像是验证、配置、协议这些基础公共组件,它们没有与 frp 的核心业务内网穿透强相关。
然后 cmd
下则存储着 frps
及 frpc
的入口 main.go
,本次先分析 frpc
的原理,在分析的时候可以先忽略 frpc
调用 pkg
下组件的细节,先关心 cmd/frpc
下的内容。
frpc-启动流程
完整流程
可以看到 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
配置文件拆分成三个配置对象 cfg
、 pxyCfgs
及 visitorCfgs
。
它们的原型对象分别为:
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:
map[string]ProxyConf:
然后 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
的基本使用方式,感觉挺独特的一门语言,主要如下:
- 一个文件即为一个模块,一个类
- 访问权限不使用
public
、private
,而是使用标识符的名称,大写即为public
,小写即为private
- 定义对象的方式比较特别,比如
bool swap(int* num1, int* num2)
在 go 中写成func swap(num1 *int, sum2 *int) bool
-
chan
管道操作以及协程的使用 - …
总而言之,还是得过一遍 go 的基本语法,要不然看代码都有点似懂非懂的感觉。