如何從頭開始構建一個P2P網絡?

分佈式系統的概念由來已久,現在越發火爆。作爲開發人員,分佈式系統一定是避不開的一個課題。分佈式系統一定是由多個節點組成的系統,其中節點指的是計算機服務器,而且這些節點一般不是孤立的,而是互通的。這些連通的節點上部署了我們的節點,並且相互的操作會有協同。分佈式系統對於用戶而言,他們的感覺就像是面對一個服務器,提供用戶需要的服務而已,而實際上這些服務是通過背後的衆多服務器組成的一個分佈式系統,因此分佈式系統看起來想是超級計算機一樣。分佈式系統的核心理念是讓多臺服務器協同工作,完成單臺服務器無法處理的任務,油漆是高併發或者大數據量的任務。但通常情況下,分佈式系統的每個節點一般會不採用高性能的服務器,而是性能相對一般的普通 PC 服務器。也就是通過橫向擴展 (增加更多的服務器),以此來提升分佈式系統的整體性能。所以廉價高效,是分佈式系統最顯著的特點。今年比較火的分佈式系統有 Bitcoin、Blockchain、IPFS 等。今天,爲了讓國內廣大開發人員對分佈式系統的開發有所瞭解,我們翻譯並分享瞭如何使用 Ruby 編程語言從頭開始構建類似 BitTorrent 的P2P 網絡系統:Xorro P2P。Xorro P2P 是由三位軟件工程師使用 Ruby 編程語言開發的迷你型類 BitTorrent 文件分享系統。這三位軟件工程師分別是:陳欣俞 (Ken Chen)、David Kurutz、Terry Lee。

由於我們對 P2P 網絡和分佈式系統感興趣,因此我們決定開始用 Ruby 從頭開始構建自己的類似 BitTorrent(BT)的系統。目前可用的版本是我們發佈的 alpha 測試版本,提供了一個有效的概念驗證:完整的分佈式路由表,節點間的通信和基本的文件傳輸。在本文末列出了未來要新增的功能,我們將會繼續改進 Xorro。

本文記錄了我們構建 Xorro P2P 的過程。希望讀者閱讀本文後能對 P2P 系統有更深一層的認識。

Xorro P2P 運行界面

研究

作爲嚴格意義上的 P2P 系統的最終用戶,我們開始了開發旅程,面臨着非常陡峭的學習曲線。我們必須進行大量的研究來了解 P2P 相關的歷史和內部組成。我們進行了蒐集和閱讀 P2P 相關資料,從舊到新依次有:Napster、Gnutella、Freenet。BitTorrent、IPFS……

集中式系統 vs 去中心化系統

需要理解的一個重要概念是集中式和去中心化系統之間的區別。第一代和第二代網絡架構的比較有助於描述這兩者之間的區別。

Napster:集中式系統

Napster 是一種 P2P 文件共享服務,主要用於傳輸音樂文件,在 1999~2001 年非常流行,據估計,在鼎盛時期大約有 8000 萬註冊用戶。Napster 的工作原理是讓所有節點都連接到中央索引服務器,該服務器包含所有關於誰擁有哪些文件的信息。

集中式 P2P 網絡的一個示例

因爲其本身爲集中式的結構,Napster 中央服務器很容易遭受到攻擊,以及文件傳送的方式備受爭議,營運兩年後在法院的判決下被迫關閉。除此之外,中央索引服務器還意味着存在單點故障,以及缺乏可擴展性。

BitTorrent、Gnutella、Freenet:去中心化系統

下一代 P2P 網絡通過採用去中心化的模式避免了與 Napster 相同的命運。

在像 BitTorrent 這樣的去中心化系統中,每臺計算機 / 節點都充當客戶端和服務器,維護自己的文件查找索引片段。節點可以通過其他節點來查找文件的位置,免除了對中央服務器的依賴。

去中心化 P2P 網絡的一個示例

P2P 文件共享系統的深入介紹

瞭解新一代 P2P 系統的優點後,我們繼續深入研究它們的特性。幸運的是,P2P 網絡是已經發展一段時間的技術,因此網上有許多資源可供我們利用。其中分佈式哈希表(distributed hash tables ,DHT)是 P2P 網絡中最重要的技術,因此分佈式哈希表是當前 P2P 網絡的基礎。我們花了很多時間進行閱讀及研究白皮書、規範文檔、博文和 Stackflow 問答,在開始編程之前,我們要確保對分佈式哈希表有深刻的瞭解。

我們找到的有用資源的列表在這裏:https://xorro-p2p.github.io/resources/

功能的選擇

BitTorrent 主要功能

經過比較許多 P2P 網絡之後,我們最終鎖定了 BitTorrent 的一組功能,作爲我們開發應用的第一個版本的藍本。這些功能將在下文中將進一步詳細描述。

  • 分佈式哈希表(DHT)
  • 文件切分
  • 節點既作爲客戶端又作爲服務器

演示

在深入瞭解 Xorro P2P 的實現細節之前,請先查看下面的動圖,該動圖解說了文件下載過程的實際情況。

首先,清單文件(即 BT 種子,下同)會被下載,然後依據種子中列出的文件切分信息下載各個文件切片。下載完所有的切片後,將它們重新合併回原始文件。

分佈式哈希表的實現

先評估幾種分佈式哈希表的優缺點:Chord、Pastry、Apache Cassandra、Kademlia 等等,再以此爲考量作爲我們選擇的依據。我們最終決定就其普及率、最簡單的遠程過程調用和信息的自動傳播等優點,選擇了 Kademlia。

事實證明,由於大量的新概念:節點、路由表、桶(buckets)、異或距離、路由算法、遠程過程調用(remote procedure calls,RPC)……,使得 Kademlia 的實施極具挑戰性。雖然當時網上已經有一些 Ruby 的實現,但我們只依據規範和白皮書,因爲我們要從頭開始構建分佈式哈希表。

Kademlia

一個 Kademlia 網絡由許多節點組成。

每一個節點都有:

  • 具有唯一的 160 位 ID。
  • 維護包含其他節點聯繫信息的路由表。
  • 維護較大的分佈式哈希表中那些自己的段。
  • 通過 4 個遠程過程調用與其他節點通信。

每個節點的路由表被劃分爲“桶”,每個桶包含與當前節點的特定“距離”的節點的聯繫信息。我們會在後面更詳細介紹關於距離的概念。

每個聯繫信息都包含其他節點的 ID、IP 地址和端口號。

由於 Xorro 是一個文件共享應用程序,因此分佈式哈希表段將包含 key/value 對,其中,每個 key 是一個文件的 ID,對應的 value 是文件的位置。

Kademlia 規範中描述的節點構成。

節點通信

Kademlia 節點發送和響應有四種基本的遠程過程調用。

PING

與互聯網控制消息協議(Internet Control Message Protocol,ICMP)中的 Ping 非常相似,它是用於驗證另一個節點是否仍處於活動狀態。

FIND NODE

發送此遠程過程調用時要找到特定節點的 ID。此遠程過程調用的接收節點在自己的路由表中查找,並返回一組最接近正在查找的 ID 的聯繫節點。

FIND VALUE

發送此遠程過程調用時要帶有要定位的特定文件 ID。如果接收節點在自己的分佈式哈希表段中找到這個 ID,它將返回響應的 Value(URL)。反之,則接收節點返回最接近文件 ID 的聯繫節點列表。

STORE

此遠程過程調用用於在接收節點的分佈式哈希表段中存儲 key/value 對(file_id/location)。

每次成功完成遠程過程調用後,發送節點和接收節點都會在各自的路由表中插入或更新彼此的聯繫信息。

尋找對等點和文件

一個節點如何在 Kademlia 網絡中找到其他節點或者文件呢?我們可以用現實生活中的例子來舉例。

如果某個人想找到另一個不認識的人,他可能會採取以下步驟:

  1. 他可以詢問離目標人物更近的朋友。也許這些朋友和目標人物在同一個行業工作,或者在同一個城市居住。
  2. 如果其中一個朋友知道目標人物在哪裏,那麼就可以提供目標人物的聯繫信息,這樣查找就完成了。
  3. 如果這些朋友都不認識目標人物,他們可以給你提供可能認識目標人物的朋友的聯繫信息。
  4. 然後你再詢問這些人,看看他們是否知道目標人物,重複這一過程,直到找到目標人物,或者達到某種停止查找的條件。

Kademlia 節點在執行查找時就遵循類似的模式。

如果一個節點要從網絡中檢索一條信息(一個文件),它將發送 FIND VALUE 的遠程過程調用到它自己的聯繫節點子集,這些聯繫節點的 ID 與它要查找的文件的 ID“最爲接近”。如果任何接收節點在其分佈式哈希表段中有這個 ID,則它們將返回相應的 value,否則,它們將返回更接近所查詢的 value 的節點列表。

下一個要討論的問題是如何在 Kademlia 網絡中確定“距離”。

距離的計算

Kademlia 將節點之間的距離定義爲節點 ID 的“按位異或”(XOR)。XOR 運算比較兩個輸入值:如果這些輸入相同,則結果爲 false(0);如果輸入不同,則結果爲 true(1)。兩個數字的異或是通過在這兩個數字的二進制表示中找到每一位的 XOR 來計算的。

例如,下圖假設 4 位密鑰空間中的節點 ID 爲 11(只有 0~15 的的 ID 是可能的)。爲證明這個概念,我們使用一些其他 ID 來計算 11 的 XOR。

在第一個例子中,我們計算節點 ID 11 和節點 ID 10 的 XOR 結果。兩個 ID 的前三位是相同的,只有最後一位不同。ID 11 和 ID 10 進行 XOR 運算的結果是二進制的 0001,或者十進制的 1。

接下來我們計算 ID 11 和 ID 12 的 XOR 結果,只有第一位是相同的,而其他部分都是不同的。ID 11 和 ID 12 的 XOR 運算的結果是二進制的 0111,或十進制的 7。

我們最後的一個例子是計算 ID 11 和 ID 4 的 XOR 結果。這裏所有的位都不相同,結果是二進制的 1111,十進制爲 15。

從這些結果中,你會注意到 Kademlia 的 XOR 度量的一個重要特徵:如果節點 ID 和當前節點的 ID 的二進制表示所共有的位相同個數越多,那麼計算得到的 XOR 結果就越小。

Kademlia 網絡和路由表

Kademlia 中的每個節點都可以看做是二叉樹中的葉子。下面我們在 4 位密鑰空間中畫出所有可能的 ID:0~15,從根(root)出發,每一步往左則在該位上新增一個“0”,往右則在該位上新增一個“1”。

與 Kademlia 網絡相關的二叉樹最重要的特性是 O(log n) 查找時間。在 Kademlia 網絡中查找節點或文件是非常有效率的過程。

如前所述,路由表將聯繫節點進行分組並存儲到桶中,每個桶中包含一定距離的節點。這個距離就是“共享位長度”,它是通過節點 ID 與當前 ID 進行 XOR 運算得到結果來得到的。

從下圖我們將看到這些桶是如何在一個 4 位密鑰空間中以 ID 11 的節點中組織的。其 ID 與當前節點共享前 3 位前綴的節點存儲在一個桶中,而 ID 與當前節點共享前 2 位前綴的節點存儲在另一個桶中,以此類推。

文件切分和檢索

文件切分是將文件切分成更小的片段,命名爲切片,並將 files/names 記錄在一個清單文件(即種子)中,這樣它們就可以按照適當的順序檢索和重新合併。

文件切分提高了 P2P 網絡的分佈性和可靠性,因爲多個節點可以存儲共享文件的部分或全部切片。如果包含切片下載信息的節點離線了,此時可以從不同的源檢索該切片信息。

文件切分還可以節省網絡帶寬,共享潛在大文件的負載分佈在許多節點中。

文件切分過程中會生成多個切片和一個清單文件(種子)。

在我們的實現中,文件添加和切分的過程是這樣的:

  • 通過拖放將要共享的文件上傳到網絡中。
  • 通過計算文件內容的 SHA1 哈希值爲文件創建 ID。
  • 這個 ID 被重新用作種子的文件名,擴展名爲“.xro”。這個過程對用戶來說是透明的,用戶只需知道 ID 即可。
  • 原始文件切分成 1 MB 塊,如果小於 1 MB,則切分爲原始大小的 50%。
  • 每個切片都被寫入磁盤,切片的名稱就是其內容的 SHA1 哈希值。
  • 切片名稱是按照順序寫入種子文件的,以及原始文件名和文件大小(以字節爲單位)。
  • 種子和切片位置信息通過 STORE 遠程過程調用廣播給對等節點。對於每個 shard/manifest,宿主節點在自己的路由表中查找與文件 ID 最接近的節點。這些對等節點都接收包含文件的 ID 和位置的 STORE 遠程過程調用。

下面是 JSON 格式的種子內容:


  {
    "file_name":"banana.mp4",
    "length":9137395,
    "pieces":[
      "272610812651008498817059664145444816819140431736",
      "255845820650928817902394043384061703021184974492",
      "709124865584808529999187320247131501825035282844",
      "463972141944555281071361859762050722622562309482",
      "665233928362169136349271011451642022996948352498",
      "460767108478119568061824765684889409150273585314",
      "242856439500459087965632547950882773486858003109",
      "1113118586291233368092664992853829437069513635744",
      "55094692080869844054492088107211106202780121432"
    ]
  }

文件檢索的處理方式也是類似的:

  • 用戶輸入他們想要從網絡中檢索的文件的 ID。這個 ID 是文件內容的哈希值,也恰好是他們首先檢索的清單文件的名稱。
  • 通過向最接近清單文件 ID 的對等節點發出 FIND VALUE 遠程過程調用,從網絡中檢索清單文件。
  • 下載清單文件後,節點爲清單文件中記錄的每個切片重複查找和下載過程(FIND VALUR 遠程過程調用)。
  • 下載完所有切片後,將它們重新合併到原始文件中。
  • 然後,節點向最接近文件各自 ID 的對等節點廣播其獲取的切片和清單的位置信息。

從 Xorro P2P 網絡中檢索文件的例子。

開發策略:從本地環境模擬並擴展網絡應用到真實網絡。

第一階段:測試模式

我們是如何着手構建和測試的?

在項目的早期階段,我們需要測試節點間的痛心,但我們只有類和測試和套件,沒有網絡,沒有遠程過程調用傳輸,甚至也沒有幾臺計算機。

我們經常啓動 Ruby 的交互式 Shell(IRB),將幾個節點進行實例化,並讓它們手動通信。當然,這可以通過腳本來實現。但一旦引入真正的網絡環境,並且節點對象不能在相同的 Ruby 進程中直接使用,否則它就會很快崩潰。

我們需要某種代理對象,它在測試和本地開發期間以一種方式運行,在實際網絡環境中部署時又以另一種方式運行。

我們的解決方案是讓每個節點將所有網絡通信都委託給先前存在的網卡(Network Adapter)對象。

在測試時,在這個網卡對象實際上是一個“Fake Network Adapter”(僞網卡),本質上是一個由其他節點組成的數組,帶有一些用於查找和遠程過程調用代理的方法。這允許我們在沒有遠程過程調用傳輸協議的情況下在本地沙箱中測試節點的交互過程。

典型的工作流程如下圖所示:

  • 節點 A 要求網絡向節點 B 發送遠程過程調用,節點 A 提供聯繫節點的 ID、IP 和端口。
  • 網絡查找節點 B 是否存在於 Fake Network (僞網絡)環境中,這是通過聯繫節點的 ID 來完成的。
  • 網絡直接調用節點 B 上的方法,改變狀態和/或返回一些數據作爲響應。
  • 網絡將該響應傳遞迴節點 A,節點 A 反過來改變狀態或對這些信息做出響應。

第二階段:在本地沙箱中通過 HTTP 進行遠程過程調用

我們的下一步是引入 HTTP 協議作爲構建遠程過程調用方法的基礎協議。

爲此,我們實現了一個 Real Network Adapter (真網卡)對象。它與我們的 Fake Network Adapter 具有相同的接口,但不是通過 ID 查找接收節點並直接調用該方法的,Real Network Adapter 從所提供的聯繫節點信息和對應於遠程過程調用的路由中列出的 IP / 端口處理一個HTTP Post 請求,可能包括請求主體中的相關數據——請求節點的聯繫信息、查詢信息等。

典型工作流程與上圖類似:

  • 節點 A 要求網絡向節點 B 提供遠程過程調用,節點 A 提供聯繫節點的 ID、IP 和端口。
  • 網絡爲 IP、端口和遠程過程調用路徑生成 HTTP POST 請求,並將其與任何有效負載數據一起發送。
  • 接收節點的 HTTP 端點接收 POST 請求,並調用 Node 對象上的接收遠程過程調用的方法。
  • 此接收遠程過程調用方法的返回值通過 HTTP 端點轉換爲 HTTP 響應,併發送回調用節點的網卡。
  • 網絡接收響應,並將其傳遞給節點 A,節點 A 反過來更改狀態或對這些新信息做出響應。

正如你所見到的,從節點的角度來看,並沒有什麼變化:每個節點都發送和接收相同的信息,但是網絡對象和 HTTP 端點抽象出節點之間的通信。

第三階段:RPC/HTTP 在互聯網環境下傳遞

一旦我們知道節點間的通信使用 HTTP 協議,我們的下一步就是將節點部署到互聯網上的多個系統上。如果系統直接在公共互聯網上運行,或者它們位於已配置打開端口的防火牆之後,這種方法就可以很好地工作。

NAT 防火牆後的節點則是另一回事。它們可以很好地加入網絡並檢索文件,但如果沒有端口轉發的話,它們就不能接收傳入的 HTTP 連接,因此不能對網絡做出貢獻。

從長遠來看,目標是基於 TCP / UDP 協議的遠程過程調用和文件傳輸協議,同時支持 STUN 和 TURN 服務器來處理公共 IP / 端口發現和傳入連接。但是,在這個項目中,我們需要一種快速繞過 NAT 和防火牆的方法。

爲此,我們構建了對 Ngrok 的支持,這是一個第三方隧道服務,這樣,防火牆後面的節點就可以成爲我們網絡中完全正常運行、功能齊全的成員。

靈活的節點實例化和啓動腳本

我們意識到現在有許多不同的節點配置/環境需要支持和測試。此外,我們還需要一種能夠在這些配置之間快速、無縫切換的方法。

在每個環境中,一個節點廣播它的 IP / 端口,以及它所託管的每個切片或種子的完整 URL / 路徑。

  • 如果我們只在本地環境中工作的話,那麼所有節點都在不同的 TCP 端口從“本地”進行廣播。
  • 如果節點直接在互聯網上,它應該廣播自己的公共 IP 和端口。
  • 如果節點具有完全限定的域名,那麼該節點應該廣播該域名,而不是數字 IP 地址。
  • 如果節點位於防火牆之後,且防火牆已正確配置用於 NAT 轉換 / 端口轉發,則該節點仍然可以依賴它的公共 IP 和端口。
  • 如果節點位於沒有轉發端口的防火牆後面,則需要:
    • 建立到 Ngrok 服務器的隧道,並注意到它唯一的 Ngrok 地址,這樣它就可以將地址廣播給其他節點。
    • 要知道它是一個 “Leech” 節點,而不是廣播它所託管的任何文件的位置信息。

我們確定了節點可能擁有的許多配置,並創建了環境變量來表示這些選項。我們的節點實例化代碼對這些變量做出反應,我們設計了一系列 Bash 腳本,來爲我們工作的每個環境標準化這些配置:測試、本地、局域網、廣域網等。

系統架構

我們最終的系統架構如下所示:

頂級對象是 XorroNode,它是一個模塊化的 Sinatra 應用程序。

每個 XorroNode 包含:

  • 一個單獨的 Kademlia 節點,負責維護自己的路由表和分佈式哈希表。
  • 用於向其他節點發起遠程過程調用的網卡。
  • 用於從其他節點、Web UI 和文件傳輸接收遠程過程調用的 Sinatra 端點。
  • 用於文件接收、切分和清單文件的創建。

未來的工作

雖然 Xorro P2P 的 Alpha 測試版是功能性的 P2P 軟件,但我們仍有許多需要改進的地方。下面是我們的一些計劃。

將 HTTP 協議替換爲基於 TCP/UDP 協議的遠程過程調用和文件傳輸

使用 HTTP 協議進行遠程過程調用和文件傳輸使我們能夠利用現有工具,並將我們的開發工作重點放在 Kademlia 分佈式哈希表更具體的問題上。但是,基於 TCP / UDP 協議的方法,可以讓我們減少開銷,並可以解決 NAT 遍歷問題。

在 Web UI 之外提交文件

我們非常喜歡我們設計的 Web UI,但當前將文件上傳到網絡的工作流程可能還需要一些改進。

Xorro 網絡上共享的文件通過瀏覽器拖放添加到節點,這一操作方式有點限制。瀏覽器必須向本地計算機上的 HTTP 路由提交 POST 請求,並在請求主體中包含所有文件數據。本地 HTTP 服務器接收數據,將其寫出來,然後對其進行切分成切片。

我們計劃重新設計這一工作流程,以便可以手動將文件添加到本地文件夾中,並自動啓動切分 / 共享過程。此外,本地客戶端應用程序或命令行工具將進一步幫助簡化文件的接收和檢索。

作者介紹:

Ken Chen:來自臺灣的軟件工程師。教育背景是化學和材料工程專業。是 Xorro P2P 的共同創建者。

David Kurutz:美國紐約市的一名軟件工程師,在系統管理、故障排除和項目管理方面擁有 10 年的經驗。同時也是 Xorro P2P 的共同創建者。

Terry Lee:美國舊金山灣區的一名女性軟件工程師。除了編程,在其技術職業生涯中曾擔任過許多角色。在產品管理、項目管理、用戶體驗和網頁設計方面擁有 10 年的經驗,熟悉如何構建產品的整個流程。

原文鏈接:

https://xorro-p2p.github.io/

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