但是如果我们面对的是一个像比原这样比较庞大的项目,身边又没有人指导,只能靠自己去看,这时应该怎么来阅读呢?也许每个人也都能找到自己的办法,或高效,或低效,或放弃。
我在这次阅读比原源代码的过程中,尝试的是这样一种方法:从外部入手,通过与比原节点进行数据交互,来一步步了解比原的内部原理。就像剥石榴一样,一点点小心翼翼的下手,最后才能吃到鲜美的果肉。
所以这个文章系列叫作“剥开比原看代码”。
说明
在系列中的每一章,我通常都会由一个或者几个相关的问题入手,然后通过对源代码进行分析,来说明比原的代码是如何实现的。对于与当前问题关系不大的代码,则会简单带过,等真正需要它们出场的时候再详细解说。
为了保证文章中引用代码的稳定性,我将基于比原的v1.0.1代码进行分析。随着时间推移,比原的代码也将快速更新,但是我觉得,只要把这个版本的代码理解了,再去看新的代码,应该是一件很容易的事情。, q3 \ ^ a* b! `2 g+ t# J
在文章中,将会有一些直接指向github上bytom源代码的链接。为了方便,我专门将bytom v1.0.1的代码放到了一个新的仓库中,这样就不容易与比原官方的最新代码混淆。该仓库地址为:https://github.com/freewind/bytom-v1.0.19 I" p: h% l; m. d1 a- c, n0 v
当然,你不必clone这个仓库(clone官方仓库http://github.com/Bytom/bytom就够了),然后在必要的时候,使用以下命令将代码切换到v1.0.1的tag,以便与本系列引用的代码一致:. ~4 k3 }7 |3 \: Y/ Y
git fetch! f4 f% L* E' l: N
git checkout -b v1.0.1
不论采用哪种阅读方法,我想第一步都应该先在本地把比原节点跑起来,试试各种功能。+ J2 g9 v: I; ]1 E1 y
对于如何下载、配置和安装的问题,请直接参看官方文档https://github.com/Bytom/bytom/tree/v1.0.1(注意我这里给出的是v1.0.1的文档),这里不多说。
本篇问题
当我们本地使用make bytomd编译完比原后,我们可以使用下面的命令来进行初始化:( @# B% K, r$ h- i6 l4 Z" w
./bytomd init --chain_id testnet
这里指定了使用的chain是testnet(还有别的选项,如mainnet等等)。运行成功后,它将会在本地文件系统生成一些配置文件,供比原启动时使用。
所以我的问题是:: k: v7 ~1 ?, {8 L. V' r
比原初始化时,产生了什么样的配置文件,放在了哪个目录下?' f7 | `: {/ h% I, P
下面我将结合源代码,来回答这个问题。
目录位置4 a( I1 Z" P- u$ @( G* T
首先比原在本地会有一个目录专门用于放置各种数据,比如密钥、配置文件、数据库文件等。这个目录对应的代码位于config/config.go#L190-L205:
func DefaultDataDir() string {7 v c k v6 B7 d* H
// Try to place the data folder in the user's home dir$ w7 p {6 j! D+ C( ?% Z, ^
home := homeDir()
dataDir := "./.bytom"
if home != "" {* t. P5 q9 z! F3 O/ u
switch runtime.GOOS {7 r% X' \; [ @% l* `) T5 c% }
case "darwin":
dataDir = filepath.Join(home, "Library", "Bytom")
case "windows":
dataDir = filepath.Join(home, "AppData", "Roaming", "Bytom")* e% M' p# e5 y1 p j- M7 f1 l
default:
dataDir = filepath.Join(home, ".bytom")0 @& P9 K8 B3 K2 Y7 j4 k- y0 `9 f
}
}
return dataDir
}- Z3 f# T3 f9 `4 r
可以看到,在不同的操作系统上,数据目录的位置也不同:# i; ^1 h4 i; l* T
: @8 ?: E! C1 e y$ N
配置文件内容8 d' n; F. w/ d: B" h$ `5 T) Q
我们根据自己的操作系统打开相应的目录(我的是~/AppData/Roaming/Bytom),可以看到有一个config.toml,内容大约如下:3 S u/ _5 k* {
$ cat config.toml
# This is a TOML config file.! g! @: a# Q3 t8 u4 X
# For more information, see https://github.com/toml-lang/toml2 ?0 A/ J P. w) ]0 t
fast_sync = true
db_backend = "leveldb"8 Z& E1 C2 z @1 {
api_addr = "0.0.0.0:9888"# v, O1 ~$ w0 Z5 Y0 P# ?4 ?: [ ?
chain_id = "testnet"
[p2p]
laddr = "tcp://0.0.0.0:46656"8 S9 Q. k, x# V3 n
seeds = "47.96.42.1:46656,172.104.224.219:46656,45.118.132.164:46656"
它已经把一些基本信息告诉我们了,比如:7 z# ^, R, p: J2 {: |
内容模板/ f' J% B/ s+ a7 R0 ?8 J- r& \
使用不同的chain_id去初始化时,会生成不同内容的配置文件,那么这些内容来自于哪里呢?
原来在config/toml.go#L22-L45,预定义了不同的模板内容:
var defaultConfigTmpl = `# This is a TOML config file.
# For more information, see https://github.com/toml-lang/toml8 f# }) a7 [& n5 x9 a! ?8 Q# @
fast_sync = true- O6 G% k( `( @* _3 V1 F- D
db_backend = "leveldb"
api_addr = "0.0.0.0:9888"5 {# R) f, m3 A5 P
`+ } s( d* r! W8 _0 [) Q4 y
var mainNetConfigTmpl = `chain_id = "mainnet"
[p2p]
laddr = "tcp://0.0.0.0:46657"
seeds = "45.79.213.28:46657,198.74.61.131:46657,212.111.41.245:46657,47.100.214.154:46657,47.100.109.199:46657,47.100.105.165:46657"+ _# {$ d5 i; ]9 v" O7 T
`
var testNetConfigTmpl = `chain_id = "testnet"8 p- t+ c& e ^: y+ \
[p2p]% S. j( z3 ?& T3 U% H. e
laddr = "tcp://0.0.0.0:46656"; A, J; f6 L5 F* ]/ N
seeds = "47.96.42.1:46656,172.104.224.219:46656,45.118.132.164:46656"
`& ~6 N9 D: ?! ]2 M& }( E
var soloNetConfigTmpl = `chain_id = "solonet"1 p% v) d. u% C0 F
[p2p]
laddr = "tcp://0.0.0.0:46658". O; p" ]7 ?4 D0 l; a. y
seeds = ""
`
可以看到,原来这些端口号和seed的地址,都是事先写好在模板里的。7 o3 X* i9 f6 `8 \6 e. a5 A: C) F- Y
而且,通过观察这些配置,我们可以发现,如果chain_id不同,则监听的端口和连接的种子都不同:, t2 `. }' v0 j# o q8 n6 z$ k
) W+ T$ S6 C+ t2 m3 D+ x( x9 F4 }* G
写入文件
这里我们需要快速的把bytomd init的执行流程过一遍,才能清楚配置文件的写入时机,也同时把前面的内容串在了一起。
首先,当我们运行bytomd init时,它对应的代码入口为cmd/bytomd/main.go#L54:
func main() {
cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir()))
cmd.Execute()
} Z& a; D. `+ t+ u0 d
其中的config.DefaultDataDir()就对应于前面提到数据目录位置。
然后执行cmd.Execute(),将根据传入的参数init,选择下面的函数来执行:cmd/bytomd/commands/init.go#L25-L246 a; P( {$ p( \' D# N9 w2 V: J
func initFiles(cmd *cobra.Command, args []string) {8 \' E1 y5 V8 v- Q
configFilePath := path.Join(config.RootDir, "config.toml")
if _, err := os.Stat(configFilePath); !os.IsNotExist(err) {
log.WithField("config", configFilePath).Info("Already exists config file.")
return9 y" Z1 k9 U' L0 f
}6 e" C+ c' ?! @8 D
if config.ChainID == "mainnet" {5 ?+ q ^: y/ v7 \) C" H
cfg.EnsureRoot(config.RootDir, "mainnet")- n+ [' F9 @* m3 p, \# m9 v- o
} else if config.ChainID == "testnet" {4 X, ?, x2 O( Q7 L) Y( P6 A
cfg.EnsureRoot(config.RootDir, "testnet")6 r, q- P, Z/ [. o$ D
} else {# ?& H; z6 u, a$ @- S
cfg.EnsureRoot(config.RootDir, "solonet")& `2 y I \6 `/ {$ e# s4 Z* K
}
log.WithField("config", configFilePath).Info("Initialized bytom")
}
其中的configFilePath,就是config.toml的写入地址,即我们前面所说的数据目录下的config.toml文件。
cfg.EnsureRoot将用来确认数据目录是有效的,并且将根据传入的chain_id不同,来生成不同的内容写入到配置文件中。
它对应的代码是config/toml.go#L10
func EnsureRoot(rootDir string, network string) {5 w+ }4 W p) ~
cmn.EnsureDir(rootDir, 0700)/ `( G0 P3 t% ]* F) ~8 V' V
cmn.EnsureDir(rootDir+"/data", 0700)2 Z. t9 M9 v. ?. k( e* |
configFilePath := path.Join(rootDir, "config.toml")
// Write default config file if missing.7 E& V3 {- b; T# Q
if !cmn.FileExists(configFilePath) {: c* w; c# c5 \4 Q& Z
cmn.MustWriteFile(configFilePath, []byte(selectNetwork(network)), 0644)7 M: V$ q4 U9 g- z% Z. _
}
}/ ], N7 @% Q* z' A" t# u7 l
可以看到,它对数据目录进行了权限上的确认,并且发现当配置文件存在的时候,不会做任何更改。所以如果我们需要生成新的配置文件,就需要把旧的删除(或改名)。
其中的selectNetwork(network)函数,实现了根据chain_id的不同来组装不同的配置文件内容,它对应于master/config/toml.go#L48:" E2 X1 p. }. Y. c: D
func selectNetwork(network string) string {& {2 e1 |; S1 R) Q1 B, Z
if network == "testnet" {
return defaultConfigTmpl + testNetConfigTmpl
} else if network == "mainnet" {
return defaultConfigTmpl + mainNetConfigTmpl
} else {' k+ ~' O1 s9 }7 g H6 A5 S; f
return defaultConfigTmpl + soloNetConfigTmpl6 ^& }, g6 l( X) H! {6 k. Q
}% F( `4 n; t9 k/ E
}' R9 S' }; A! ?; `/ i: k
果然就是一个简单的字符串拼接,其中的defaultConfigTmpl和*NetConfgTmpl在前面已经出现,这里不重复。! v) y9 |4 R* L
最后调用第三方函数cmn.MustWriteFile(configFilePath, []byte(selectNetwork(network)), 0644),把拼接出来的配置文件内容以权限0644写入到指定的文件地址。: w4 a9 O& y i* H
到这里,我们这个问题就算回答完毕了。
感谢社区用户Freewind的辛勤写作