详细阅读FRP-1-未完待续
写Go好几年,基本没有看过开源项目代码,只是搬砖,说来惭愧,偶然和一个朋友开启Side Project,基于开源项目 FRP 用来做一个内网穿透功能的工具,此项目在Github获得10.7K star,属于是超级硬核Project了。阅读一下大佬的源码,梳理一下思路,顺便写一下文章记录。
前言
由于官方好像并没有文档,所以知乎上找了几篇文章,基于参考文档的思路,往下继续梳理。
基础认知
frp属于C/S架构,实现的功能是将内网某个端口提供的服务通过公网服务器的辅助,穿透绑定到公网IP的某个开放端口,提供给他人使用。Server端要求是放在公网服务器,Client端放在内网环境,能联网,但没有公网IP。所以基础逻辑是,两者在登录完成后 建立长连接,而由于Client的网络环境,导致Server侧是无法向Client直接发送消息的,除非是建连之后,通过长连接发送消息。
详细介绍
有了基础的认知以后,我们来看一下代码。 Client端和Server端 都是使用这个库来封装的,github.com/spf13/cobra
这个库是一个Go做命令行工具比较常用的库。
C端
多个代理配置
抛去cobra框架的代码不看,主要的业务逻辑,是来自 这里: cmd/frpc/sub/root.go:runClient
这个方法接收一个配置,去启动每一个单独的代理配置,传入cfgDir的情况下
使用runMultipleClients
并发去起多个goroutinue,并在协程内部调用 runClient
启动每个配置内的代理,本质上还是调用的一个方法。
1 | func runMultipleClients(cfgDir string) error { |
如果是多个client,那么程序会阻塞在这里,等待进程退出的信号,优雅关闭协程,回到cobra的命令行提示符,或是退出程序。
启动单个代理
从配置文件加载到配置,并且在内部调用了 每个config struct的 Complete方法 设定初始值。每一个 client的配置分为 common_cfg,proxyCfgs, visitorCfgs 三种,每一种都会有不同的用途。
1 | func runClient(cfgFilePath string) error { |
cfg是 client的连接信息,代表这个客户端和服务端的连接配置,一般放在 client配置里,比如 serverAddr, serverPort, TLS, transport
等这些信息,他是长连接的配置,比如说 Server端的地址和端口,传输配置等等。
而 proxyCfgs 则是 每一个代理通道配置,你比如一个TCP服务端口80,另外一个TCP服务端口81,再有一个HTTPS服务端口443,一个SSH服务端口22,等等,每个proxyCfg指向的Client地址端口背后一定要有个对应的服务,这样子才能对外映射,如果这个端口没有监听对应协议的服务,并不会报错,只是你这个通道就没有用。
这里的内容还没有确认,官方也没有文档描述他,所以我暂时只能靠我自己的理解去描述。
visitorCfgs 暂时我还没了解到,就暂时不管,下面放一个我按照他配置文件理解来的含义。
visitorCfgs 的作用按照配置文件来看的话,意思是比如你映射了一个http的网站或服务,这个网站是要求对外提供服务的,那使用你这个服务的人 都叫做visitor,那么每个 visitor 可以配置一个角色 以及用户名密码做鉴权。 不过这个不重要,等真的了解到哪个地步,再说。
startService
传入了 配置,并且把配置路径也给传递给service对象,方便后面使用。
startService方法,判断协议 是 kcp 或是 quic时,注册优雅关闭协程的 channel,订阅 SIGINT/SIGTERM 这两个信号,出现信号 将 当前 ctx 取消,此时当前请求的父子请求 均会被取消,当前程序即可退出。
startService 方法中 注册的 webServer 并不是我们期望的主服务器,后续调用 Run方法时,才是启动主服务器。
1 | func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) { |
Run方法中,比较重要的是 loopLoginUntilSuccess
这个方法,会执行登录操作,并从 Server拿到 runID,进行token校验等操作。这个是个TCP协议的接口。
他是我们Client和 Server建立连接的第一个请求,如果失败则无法建立通信,只有成功才可以建立,作者也知道这个方法很重要,使用了 Backoff 来 循环调用login,直到成功。
当登录成功后,会创建一个 Control 对象,并且执行Run方法,Run方法会启动本地Client端口监听,代表连接正式建立,然后执行 handleWorkConnCb 方法,这个方法会启动一个协程,用来监听本地端口,然后通过conn发送给Server,等待Server返回一个workConn,然后通过workConn和本地端口进行通信。
未完待续…
参考文档
https://jiajunhuang.com/articles/2019_06_11-frpc_source_code_part1.md.html
https://jiajunhuang.com/articles/2019_06_19-frp_source_code_part2.md.html