精通IPFS:IPFS 啓動之 init 函數

上一篇文章中,我們瞭解了 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章