上一篇文章中,我們瞭解了 IPFS 啓動過程中的 boot 函數,它就象一個大總管,控制到 IPFS 系統的啓動整個過程,在那篇文章中,我們簡單的提到了 IPFS 啓動過程分兩個主要步驟,一個是初始化,另一個是啓動。初始化過程要用到的是 init 函數,這個函數初始化系統,只有系統完整初始化之後纔可以啓動系統。init 這個函數位於 core/components/init.js 文件中。下面,進入這個文件來繼續我們的探索之旅。
檢查選項是否爲函數,如果是則重新設置回調函數和選項對象。
if (typeof opts === ‘function’) {
callback = opts
opts = {}
}
在這裏 init 函數的選項參數是我們在前面指定的,默認情況下只有一個 bits: 2048 的屬性,另一個 pass 屬性,依賴於用戶的指定。
接下來,生成 done 函數變量。內容如下,具體執行稍後分析。
const done = (err, res) => {
if (err) {
self.emit('error', err)
return callback(err)
}
self.preStart((err) => {
if (err) {
self.emit('error', err)
return callback(err)
}
self.state.initialized()
self.emit(‘init’)
callback(null, res)
})
}
調用 IPFS 的狀態對象的 init 方法,進行初始化,此處略去不講。
self.state.init()
如果用戶在選項中指定的了具體的倉庫對象,則使用用戶指定的倉庫對象。然後調用 done 函數。
if (opts.repo) {
self._repo = opts.repo
return done(null, true)
}
默認情況下,用戶是不會指定的,所以代碼繼續執行。
設置選項別的一些屬性。
opts.emptyRepo = opts.emptyRepo || false
opts.bits = Number(opts.bits) || 2048
opts.log = opts.log || function () {}
調用 mergeOptions 方法,合併默認配置和用戶指定的配置。這個方法在前面啓動時已經見過,這裏略去不提。
接下來又是一個 waterfall 函數調用。這個函數裏面的流程比較複雜,也比較重要,我們需要一步一步來看。
首先調用倉庫對象的 exists 方法,檢查倉庫是否存在。這個方法內部只檢查倉庫的版本文件是否存在。接下來,調用第二個函數。
接下來,處理第二個函數。首先,檢查前一個函數返回的倉庫是否存在的標識,如果存在,則拋出異常,結束下面的執行。
然後,檢查選項中是否指定有私鑰,如果用戶提供的私鑰是一個對象,則使用私鑰對象直接調用下一個方法;如果用戶提供的私鑰不是一個對象,則調用 peerId.createFromPrivKey 方法,根據私鑰生成一個節點ID,然後再以之爲參數調用下一個方法;如果沒提供私鑰,則調用 peerId.create 方法,生成一個隨機的節點ID,然後再以之爲參數調用下一個方法.
具體代碼如下:
(exists, cb) => {
self.log('repo exists?', exists)
if (exists === true) {
return cb(new Error('repo already exists'))
}
if (opts.privateKey) {
self.log(‘using user-supplied private-key’)
if (typeof opts.privateKey === ‘object’) {
cb(null, opts.privateKey)
} else {
peerId.createFromPrivKey(Buffer.from(opts.privateKey, 'base64'), cb)
}
} else {
// Generate peer identity keypair + transform to desired format + add to config.
opts.log(generating ${opts.bits}-bit RSA keypair…, false)
self.log(‘generating peer id: %s bits’, opts.bits)
peerId.create({ bits: opts.bits }, cb)
}
}
接下來,處理第三個函數。首先,根據生成的節點ID 對象,設置配置對象的 Identity 屬性。
然後,根據是否指定 pass 屬性,決定要不要生成 Keychain。因爲默認情況下,這個配置沒有指定,所以這裏不會生成。
最後,調用倉庫對象的 init 方法來初始化倉庫。
具體代碼如下:
(peerId, cb) => {
self.log('identity generated')
config.Identity = {
PeerID: peerId.toB58String(),
PrivKey: peerId.privKey.bytes.toString('base64')
}
privateKey = peerId.privKey
if (opts.pass) {
config.Keychain = Keychain.generateOptions()
}
// 初始化倉庫
self._repo.init(config, cb)
}
在倉庫的初始化方法中使用了 series 函數,這個函數會依次調用倉庫主對象的 open方法和配置對象、規格對象、版本對象的 set 方法,來真正初始化倉庫,這幾個方法執行完成後,在倉庫目錄下面就會生成 config、datastore_spec、version 等 3個文件。
接下來,處理第四個函數。這個函數就是調用倉庫對象的 open 方法來打開倉庫。
(_, cb) => self._repo.open(cb)
在前面調用這個方法時,因爲倉庫還沒有初始化,所以有很多流程沒有執行到,這次我們來繼續執行這些流程。
這次調用 root 對象的 open 方法和上次沒什麼區別,但是在調用 _isInitialized 方法時,因爲配置對象、規格對象、版本對象都已經存在,所這次這個對象不會生成錯誤對象,從而執行的下一個函數不是最終的回調函數,而是下一個函數,即 _openLock 函數。這個函數執行的結果就是在倉庫目錄中生成了一個 repo.lock 目錄,表明當前進程正在不執行,從而不允許另一個 IPFS 進程同時執行。
下面,我們仔細看下倉庫 open 方法的餘下的流程:
保存文件鎖對象到倉庫對象中。代碼如下,略過不講。
(lck, cb) => {
log('aquired repo.lock')
this.lockfile = lck
cb()
}
處理數據存儲和區塊存儲對象。首先,調用 backends.create 方法生成 datastore 對象,並保存在倉庫對象的同名屬性中,同時在倉庫目錄下面生成 datastore 目錄及對應的文件。這個 create 簡單說一下,它根據第一個參數,從倉庫的選項 storageBackends 中獲得創建某個目錄/文件的方法,再根據第二個參數指定的創建路徑,第三個參數創建的配置參數,通過這幾個參數在指定的路徑下創建指定的目錄/文件。
然後,調用 backends.create 方法生成基礎的 blockstore 對象,同時倉庫目錄下面生成 blocks 目錄。
最後,調用 blockstore 方法,根據配置選項來處理基礎的 blockstore 對象。
具體代碼如下:
(cb) => {
log('creating datastore,類型爲 js-datastore-level')
this.datastore = backends.create('datastore', path.join(this.path, 'datastore'), this.options)
const blocksBaseStore = backends.create('blocks', path.join(this.path, 'blocks'), this.options)
blockstore(
blocksBaseStore,
this.options.storageBackendOptions.blocks,
cb)
}
這裏的配置選項請參考前一篇中提到的生成倉庫對象的過程。
保存區塊存儲對象到倉庫對象中。在前一個函數最後的處理中,會以最終生成的 blockstore 對象爲參數,調用下一個函數。所以,這裏的 blocks 參數即爲最終生成的 blockstore 對象,在倉庫對象中保存這個對象。
(blocks, cb) => {
this.blocks = blocks
cb()
}
生成 keys 對象。這個函數比較簡單,直接調用 backends.create 方法生成 keys 對象,並保存在倉庫對象的同名屬性中,同時在倉庫目錄下面生成 keys 目錄。
(cb) => {
log('creating keystore')
this.keys = backends.create('keys', path.join(this.path, 'keys'), this.options)
cb()
}
設置倉庫關閉屬性。這個函數把倉庫對象的 closed 屬性設置爲假。
最終,倉庫 open 方法的所有業務邏輯都執行完成,所有的目錄及文件也已經存在,終於來到了它的最終回調函數。因爲,在前面執行過程中沒有任何錯誤,所以這個回調函數直接調用最終的回調函數,從而執行流程回到 init 函數中。
接下來,處理第五個函數。在倉庫的 open 方法執行完成後,就開始處理第五個函數。在這裏,根據用戶是否設置 pass 來進行不同處理。如果有設置,則生成 _keychain 對象,並保存到 IPFS 同名屬性中;如果沒有,則直接調用下一個函數。
代碼如下:
(cb) => {
if (opts.pass) {
const keychainOptions = Object.assign({ passPhrase: opts.pass }, config.Keychain)
self._keychain = new Keychain(self._repo.keys, keychainOptions)
self._keychain.importPeer('self', { privKey: privateKey }, cb)
} else {
cb(null, true)
}
}
接下來,處理第六個函數。這個函數主要用來生成 IPNS 對象,這個對象我們後面會涉及到,這裏也略過不提。代碼如下:
(_, cb) => {
const offlineDatastore = new OfflineDatastore(self._repo)
self._ipns = new IPNS(offlineDatastore, self._repo.datastore, self._peerInfo, self._keychain, self._options)
cb(null, true)
}
接下來,處理第七個函數。這個函數主要用來生成一個空的目錄對象,同時把 init-files/init-docs/ 目錄下的所有文件保存到倉庫中。具體如何處理我們下面來看。
(_, cb) => {
if (opts.emptyRepo) {
return cb(null, true)
}
const tasks = [
(cb) => {
waterfall([
(cb) => DAGNode.create(new UnixFs('directory').marshal(), cb),
(node, cb) => self.dag.put(node, {
version: 0,
format: 'dag-pb',
hashAlg: 'sha2-256'
}, cb),
(cid, cb) => self._ipns.initializeKeyspace(privateKey, cid.toBaseEncodedString(), cb)
], cb)
}
]
if (typeof addDefaultAssets === ‘function’) {
tasks.push((cb) => addDefaultAssets(self, opts.log, cb))
}
parallel(tasks, (err) => {
if (err) {
cb(err)
} else {
cb(null, true)
}
})
}
在這段代碼中,第一個要執行的任務是創建一個空的目錄對象,並用這個目錄對象生成一個 DAGNode,然後調用 IPFS 對象的 dag 對象的 put 方法保存生成的 DAG 節點對象,put 方法內部調用 IPFS 對象的 ipld 的同名方法,後者調用 blockservice 對象來保存,這個對象或者調用本地倉庫對象來保存,或者調用 bitswap 來保存,在初始化階段因爲 bitswap 對象還沒有生成,所以會調用本地倉庫對象來保存生成的 DAGNode。
addDefaultAssets 變量是在文件開始的地方定義的,爲一個函數,所以第二個執行的任務就是這個函數。這個函數的主要目的是把 init-files/init-docs/ 目錄下的所有文件保存到倉庫中,所以當我們初始化完成後,可以在倉庫的 blocks 目錄下看到很多文件,這些文件就保存了這裏提到的文件。保存文件這個過程,我們後面會詳細講解,這裏暫且略過。
處理回調。在 waterfall 函數最後一個函數處理完成後,即在 tasks 中的所有任務執行完成後,調用前面指定的 done 回調函數。下面我們看下這個函數的內容。
這個函數有兩個參數,分別表示了前面函數執行的錯誤和結果,當執行成功之後,就會執行 IPFS 對象的 preStart 方法進行預啓動,在預啓動成功之後,調用最終的回調方法,從而執行流程回到 boot 函數,進而開始執行系統的啓動方法,開始真正啓動系統。
預啓動和啓動這兩個方法,我們統一留在下一篇文章進行詳細的說明。
通過上面的梳理,我們可以發現 init 函數顧名思議就是來初始化系統的,包括初始化/生成倉庫,生成節點ID,保存 init-docs 文檔,調用預啓動/啓動方法等啓動,這個方法總的來說就是把系統啓動所需要的一切都準備好,然後正式啓動。
作者:喬瘋,加密貨幣愛好者,ipfs 愛好者,黑螢科技CTO