bt協議有早起的tracker版本(俗稱bt1.0)和現在常用的基於DHT的版本(俗稱bt2.0),下文爲整理的協議相關資料。
tracker版本
bittorrent是一個文件分發協議,它使用url來定位文件而且跟web服務無縫集成。當有多個人同時下載同一個文件時,下載者之間可以互相上傳自己已有的那部分文件,讓一個文件支持很多人同時下載卻只增加小量的帶寬負擔變成可能,這就是bt協議相比http協議的優勢。
1. bt文件分享由下列內容組成:
傳統的文件服務器
種子文件(.torrent文件)
bt tracker服務器
文件分享者
web瀏覽器
web瀏覽器用戶(多個)
2. 一個服務器按照下面的步驟開始文件分享過程
啓動一個bt tracker服務器
啓動一個普通的web服務器,如apache
在web服務器上配置多媒體類型‘application/x-bittorrent’關聯到.torrent文件
生成一個.torrent文件,在文件中添加bt tracker服務器的地址
上傳torrent文件到web服務器
發佈torrent文件下載頁面
等待用戶下載
3. 一個用戶按照下面的步驟開始文件下載
安裝bt客戶端
瀏覽web頁面
下載torrent文件
保存torrent文件到本地
使用bt客戶端打開torrent文件,開始下載
等待文件下載完成
4. bencoding編碼
strings(字符串)編碼爲:<字符串長度>:<字符串> 例如: 4:test 表示爲字符串"test",4:例子 表示爲字符串“例子”,字符串長度單位爲字節,沒開始或結束標記
integers(整數)編碼爲:i<整數>e,開始標記i,結束標記爲e,例如:i1234e 表示爲整數1234,i-1234e 表示爲整數-1234,整數沒有大小限制,i0e 表示爲整數0,i-0e 爲非法,以0開頭的爲非法如: i01234e 爲非法
lists(列表)編碼爲:le,開始標記爲l,結束標記爲e,列表裏可以包含任何bencoding編碼類型,包括整數,字符串,列表,字典。例如: l4:test5abcdee 表示爲二個字符串[“test”,“abcde”]
dictionaries(字典)編碼爲de,開始標記爲d,結束標記爲e,關鍵字必須爲bencoding字符串,值可以爲任何bencoding編碼類型,例如: d3:agei20ee 表示爲{“age”=20},d4:path3:C:\8:filename8:test.txte 表示爲{“path”=“C:”,“filename”=“test.txt”}
5. metainfo files
metainfo files(俗稱torrent文件、bt種子文件),是使用bencoding進行編碼的一個dictionaries數據類型,包括以下key
- info(dictionary): 必選, 表示該bt種子文件的文件信息
- 文件信息包括文件的公共部分
1.1) piece length(integer): 必選, 每一數據塊的長度
1.2) pieces(string): 必選, 所有數據塊的 SHA1 校驗值
1.3) publisher(string): 可選, 發佈者
1.4) publisher.utf-8(string): 可選, 發佈者的 UTF-8 編碼
1.5) publisher-url(string): 可選, 發佈者的 URL
1.6) publisher-url.utf-8(string): 可選, 發佈者的 URL 的 UTF-8 編碼 - 如果 bt 種子包含的是單個文件,包含以下內容
2.1) name(string): 必選, 推薦的文件名稱
2.2) name.utf-8(string): 可選, 推薦的文件名稱的 UTF-8 編碼
2.3) length(int): 必選,文件的長度單位是字節 - 如果是多文件,則包含以下部分:
3.1) name(string): 必選, 推薦的文件夾名稱
3.2) name.utf-8(string): 可選, 推薦的文件名稱的 UTF-8 編碼
3.3) files(list): 必選, 文件列表,每個文件列表下面是包括每一個文件的信息,文件信息是個字典 - 文件字典
4.1) length(int): 必選,文件的長度單位是字節
4.2) path(string): 必選,文件名稱,包含文件夾在內
4.3) path.utf-8(string): 必選,文件名稱 UTF-8 表示,包含文件夾在內
4.4) filehas(string): 可選,文件hash
4.5) ed2k(string): 可選, ed2k 信息
- 文件信息包括文件的公共部分
- announce(string): 必選, tracker 服務器的地址
- announce-list(list): 可選, 可選的 tracker 服務器地址
- creation date(interger): 必選, 文件創建時間
- comment(string): 可選, bt 文件註釋
- created by(string): 可選,文件創建者
6. trackers
tracker服務器接收get請求,一個get請求由下列字段組成
• info_hash 20字節的sha1哈希值,是bencoding編碼之後的torrent文件內容的hash。
• peer_id: 長度爲20的字符串,代表下載者的id,每一個下載者開始下載之前會隨機生成自己的id。
• ip : 可選參數,表示文件下載者的id
• port: 文件下載者監聽的端口,默認從6881開始,最大的6889
• uploaded: 十進制表示的上傳字節總數
• downloaded: 十進制表示的下載字節總數
• left: 十進制表示的剩餘字節總數,注意這個值不能通過downloaded和length進行算數計算得到,因爲當一些下載文件塊的數據的完整性校驗失敗的,這些文件塊必須被重新下載。
• event : 可選參數,有四個可能的至 started,completed,stopped,empty。
tracker返回的內容是一個bencoded dictionaries數據類型,如果返回的內容包含failure reason字段,表示請求失敗,failure reason包含失敗的理由。如果沒有failure reason字段,則返回內容必須包含interval和peers字段。interval代表客戶端發起下一次請求的間隔,peers包含一個peer列表。一個peer由peer、id、ip、port組成。
7. peer protocol
bt peer協議是基於tcp或者utp協議。peer連接是對稱的,雙方可以同時發送數據,而且數據的形式是一樣的。一個peer代表一個bt下載用戶。
peer協議會使用torrent文件中的pieces塊,下標從0開始。當一個peer下載完一個文件塊,檢查文件塊的hash值匹配正確的時候,它就發送一個announce請求給擁有該piece塊的peer列表,聲明自己擁有該piece塊,這樣其它的peer就可以向這個peer發送下載該piece的請求。
peer鏈接兩端包含2位的狀態:choked,unchoked和interested,uninterested。Choking表示不會發送數據知道一個unchoking動作發生。後面將會解釋爲什麼會存在choking這個狀態。
當peer連接的雙方有一個是interested狀態,而且另外一個不是choking狀態,這個連接就可以進行數據傳輸。無論何時當一個下載者向一個unchoked狀態的peer發送下載請求時,interest狀態必須每次更新。這個屬性的實現比較困難,但是這讓下載者知道當它是unchoked的狀態時哪些peers將會立刻開始下載成爲可能。
peer連接一開始是choked和uninterested狀態。當數據開始傳輸時下載者必須把下載塊放進請求隊列來獲取更高的tcp性能(這就是所謂的管道技術)。另一方面,不能馬上寫入tcp緩存的請求必須放進隊列,而不是放在應用層的網絡緩衝區,這樣當choke動作發生時,他們才能被丟掉。
peer連線協議由握手和緊跟着的無窮的用長度做前綴的字符串流。握手由19(十進制字符串)跟着字符串’BitTorrent protocol’開始。開始的字符串是長度。後面所有的整數都是使用4字節big-endian(高字節序)固定的頭部之後跟着是8個保留字節,在目實現的協議版本中值都是0。如果你想擴展協議,可以使用這8個字節,請和Bram Cohen(bt協議的作者)聯繫確保所有擴展的兼容性。再接下來是20個字節的sha1哈希值,來自torrent文件中的info字段。如果peer連接的兩端發送的哈希值不一致,則連接被關閉。一個例外的情形是當一個下載者想在一個端口開始多線程下載,他們等待進來的連接發送hash,如果這個hash在它的維護的列表裏面,就把這個hash返回給另一端。20字節hash的後面是20字節的peer id,這個id會發送給bt tracker服務器,而且會出現在bt tracker返回的peer列表裏面。
上面就是一次握手所有的內容。接下來是可選一系類的長度做前綴的消息。長度爲0表示keepalives(保持連接),被忽略。保持連接消息每兩分鐘發送一次。
8. peer messages
所有的非保持連接的消息由一個表示類型的字節開始。
類型列表如下:
0 - choke
1 - unchoke
2 - interested
3 - not interested
4 - have
5 - bitfield
6 - reques5t
7 - piece
8 - cancel
‘choke’, ‘unchoke’, ‘interested’, 和 ‘not interested’ 這四個消息沒有消息體。bitfield消息只作爲第一個消息發送一次。它的消息體是一個bitfield數據類型(參考c語言),下載者發送過的塊的下標對應的位置1,其餘的置0。下載者沒有任何內容的時候可以跳過bitfield消息。bitfield的第一個字節對應下標0 - 7,第二個對應 8 - 15,等等。多餘的位置0。
have消息的消息體是一個數字,表示下載者最近下載完成和檢驗正確的文件塊的下標。
request消息包含index、begin、length三個字段。最後兩個是字節偏移。length通常是2的指數除非是文件的最後一塊。當前所有bt協議的實現版本中length的值是16kiB,關閉連接的request中length字段的值要大於16kiB。
cancel消息和request有一樣的消息體。用來取消下載某一個文件塊。
piece消息包含index、begin、piece字段。它們的值和request消息是相關的。下載者通常用隨機的順序下載文件塊,這樣能提高效率。
choking發生的原因有幾個。一個是當一次發起過多的連接時tcp擁塞控制表現極差。另外choking可以讓peer連接的雙方使用 tit-for-tat-ish算法來保證下載速度的一致。
一個好的choking算法必須具備幾個好的特性。它應該能控制併發數量來獲取更高的tcp性能。它應該避免過快的choking和unchoking。最後它應該每過一段時間就嘗試空閒連接來找到更好的連接,這就是所謂的unchoking優化。
基於DHT的bt協議
由於很多國家、地區通過封禁tracker的方式試圖封停bt下載以保護版權、控制網絡傳輸等,因此tracker式的bt受到了極大的打擊。而基於DHT的去中心化網絡則慢慢興起。
基於DHT的bt下載流程包括:藉助於DHT和KRPC完成Node節點尋址、資源對應的Peer獲取,底層uTP以及Peer Wire握手,獲取到目標資源的"種子信息(infohash/filename/pieces分塊sha1)",完成下載。
1. DHT算法
DHT常見算法主要有Kademlia、chord、Pastry等。其中Kademlia最爲流行,BT及BT的衍生派(Mainline, Btspilits, Btcomet, uTorrent…),eMule及eMule各類Mods(verycd, easy emules, xtreme…)等P2P文件分享軟件都是基於該算法來實現DHT網絡的。
關於Kademlia算法的詳細內容在此不做介紹,因爲篇幅較長。核心思想可以用如下的例子來表明:
常見的tracker好比是學校圖書館,裏面存滿了書,學生需要統一去圖書館進行登記借閱。而分佈式則是,拆掉圖書館(不設立中心化的服務器),將圖書館裏所有的書都分發到每位學生手上(所有的文件分散存儲在各個節點上)。即是所有的學生,共同組成了一個分佈式的圖書館。爲了方便查找,Kademlia作了下面這種安排:
假設某本書的書名的hash值是 00010000,那麼這本書就會被要求存在學號爲00010000的同學手上。但還得考慮到會有同學缺勤。算法要求這本書不能只存在一個同學手上,而是被要求同時存儲在學號最接近00010000的k位同學手上,即00010001、00010010、00010011…等同學手上都會有這本書。
同樣地,當你需要找這本書時,將書名hash一下,得到 00010000,這個便是索書號,你就知道該找哪(幾)位同學了。剩下的問題,就是找到這(幾)位同學的手機號。
一個可行的思路就是在你的通訊錄裏找到一位擁有目標同學的聯繫方式的同學。前面提到,每位同學手上的通訊錄都是按距離分層的。算法的設計是,如果一個同學離你越近,你手上的通訊錄裏存有ta的手機號碼的概率越大。而算法的核心的思路就可以是:當你知道目標同學Z與你之間的距離,你可以在你的通訊錄上先找到一個你認爲與同學Z最相近的同學B,請同學B再進一步去查找同學Z的手機號。
以0000110爲基礎節點,如果一個節點的ID,前面所有位數都與它相同,只有最後1位不同,這樣的節點只有1個——0000111,與基礎節點的異或值爲0000001,即距離爲1;對於0000110而言,這樣的節點歸爲“k-bucket 1”;
如果一個節點的ID,前面所有位數相同,從倒數第2位開始不同,這樣的節點只有2個:0000101、0000100,與基礎節點的異或值爲0000011和0000010,即距離範圍爲3和2;對於0000110而言,這樣的節點歸爲“k-bucket 2”;
……
如果一個節點的ID,前面所有位數相同,從倒數第n位開始不同,這樣的節點只有2(i-1)個,與基礎節點的距離範圍爲[2(i-1), 2i);對於0000110而言,這樣的節點歸爲“k-bucket i”;
我們現在來闡述一個完整的索書流程。
A同學(學號00000110)想找《分佈式算法》,A首先需要計算書名的哈希值,hash(《分佈式算法》) = 00010000。那麼A就知道ta需要找到00010000號同學(命名爲Z同學)或學號與Z鄰近的同學。
Z的學號00010000與自己的異或距離爲 00010110,距離範圍在[24, 25),所以這個Z同學可能在k-bucket 5中(或者說,Z同學的學號與A同學的學號從第5位開始不同,所以Z同學可能在k-bucket 5中)。
然後A同學看看自己的k-bucket 5有沒有Z同學:
如果有,那就直接聯繫Z同學要書;
如果沒有,在k-bucket 5裏隨便找一個B同學(注意任意B同學,它的學號第5位肯定與Z相同,即它與Z同學的距離會小於24,相當於比Z、A之間的距離縮短了一半以上),請求B同學在它自己的通訊錄裏按同樣的查找方式找一下Z同學:
– 如果B知道Z同學,那就把Z同學的手機號(IP Address)告訴A;
– 如果B也不知道Z同學,那B按同樣的搜索方法,可以在自己的通訊錄裏找到一個離Z更近的C同學(Z、C之間距離小於23),把C同學推薦給A;A同學請求C同學進行下一步查找。
由此,可以將節點的定位變成類似二叉搜索的方式進行。保證對於任意n個學生,最多只需要查詢log2(n)次,即可找到獲得目標同學的聯繫方式
2. peer和node
一個peer是一個實現了bt協議並且開啓了TCP監聽端口的bt客戶端或者服務器。一個node是一個實現了DHT協議並且開啓了UDP監聽端口的bt客戶端或者服務器,這兩者非常容易混淆。
DHT由很多node以及這些node保存的peer地址信息組成,一個bt客戶端包括了一個DHT node節點,通過這些node節點來和DHT網絡中的其它節點通信來獲取peer的信息,然後再通過bt協議從peer下載文件。流程如下。
- 當node要爲 torrent(種子文件) 尋找 peer(保存了目標資源的IP) 時,它將自己路由表中的node ID 和 torrent 的 infohash(資源HASH) 進行"距離對比"(node和目標文件的距離),然後向路由表中離 infohash 最近的node發送請求,問它們正在下載這個 torrent 的 peer 的聯繫信息
- 因爲資源HASH和node HASH都共用一套20bytes的命名空間,所以DHT node節點充當了peer的"代理"的工作,我們不能直接向peer發起資源獲取請求(即使這個peer確實存儲了我們的目標資源),因爲peer本身不具備處理P2P request/response能力的,我們需要藉助DHT的能力,讓DHT告訴我們哪個peer保存了我們想要的資源或者哪個DHT node可能知道從而遞歸地繼續去問那個DHT網絡
- 如果一個被聯繫的node知道下載這個 torrent 的 peer 信息,那個 peer 的聯繫信息將被回覆給當前node。否則,那個被聯繫的node則必須回覆在它的路由表中離該 torrent 的 infohash 最近的node的聯繫信息,
- 最初的node重複地請求比目標 infohash 更近的node,直到不能再找到更近的node爲止
- 查詢完了之後,客戶端把自己作爲一個 peer 插入到所有回覆node中離種子最近的那個node中,這一步背後的含義是: 我之前是請求這個資源的人,我們現在獲取到資源了,我在下載這個文件的同時,我也要充當一個新的peer來向其他的客戶端貢獻自己的文件共享,這樣,當另外的其他客戶端在發起新的請求的時候,DHT節點就有可能把當前客戶端對應的peer返回給新的請求方,這樣不斷發展下去,這個資源的熱度就越來越熱,下載速度也越來越快
- 請求 peer 的返回值包含一個不透明的值,稱之爲"令牌(token)"
- 如果一個node宣佈它所控制的 peer 正在下載一個種子(即該node擁有該文件資源),它必須在回覆請求node的同時,附加上對方向我們發送的最近的"令牌(token)"。這樣當一個node試圖"宣佈"正在下載一個種子時,被請求的node覈對令牌和發出請求的node的 IP 地址。這是爲了防止惡意的主機登記其它主機的種子。由於令牌僅僅由請求node返回給收到令牌的同一個node,所以沒有規定他的具體實現。但是令牌必須在一個規定的時間內被接受,超時後令牌則失效。在 BitTorrent 的實現中,token 是在 IP 地址後面連接一個 secret(通常是一個隨機數),這個 secret 每五分鐘改變一次,其中 token 在十分鐘以內是可接受的。
3. 路由表
- 每個node節點維護一個路由表保存已知的好節點。路由表中的節點是用來作爲在 DHT 中請求的起始點。路由表中的節點是在不斷的向其他節點請求過程中,對方節點回復的。即DHT中的K桶中的節點,當我們請求一個目標資源的時候,我們根據HASH XOR從自己的K桶中選擇最有可能知道該資源的節點發起請求,而被請求的節點也不一定知道目標資源所在的peer,這個時候被請求方會返回一個新的"它認爲可能知道這個peer的節點",請求方收到這個新的節點後,會把這個節點保存進自己的K桶內,然後繼續發起請求,直到找到目標資源所在的peer爲止
- 並不是我們在請求過程中收到的節點都是平等的,有的節點是好的,而另一些則不是。許多使用 DHT 協議的節點都可以發送請求並接收回復,但是不能主動回覆其他節點的請求,這種節點被稱之爲"壞節點"
- 節點的路由表只包含已知的好節點,這很重要。好節點是指在過去的 15 分鐘以內,曾經對我們的某一個請求給出過回覆的節點(存活好節點),或者曾經對我們的請求給出過一個回覆(不用在15分鐘以內),並且在過去的 15 分鐘給我們發送過請求。上述兩種情況都可將節點視爲好節點。在 15 分鐘之後,對方沒有上述 2 種情況發生,這個節點將變爲可疑的。當節點不能給我們的一系列請求給出回覆時,這個節點將變爲壞的。相比那些未知狀態的節點,已知的好節點會被給於更高的優先級。因此如果我們要做DHT嗅探,我們的嗅探器除了要能夠發出FIND_NODE請求及接收返回之外,還需要能夠響應其他節點發來的請求(get_peers/announce_peer),這樣纔不會被其他節點列入"可疑"甚至"壞節點"列表中。
- 路由表覆蓋從 0 到 2^160 全部的節點 ID 空間。路由表又被劃分爲桶(bucket),每個桶包含一部分的 ID 空間。空的路由表只有一個桶,它的 ID 範圍從 min=0 到 max=2^160。當 ID 爲 N 的節點插入到表中時,它將被放到 ID 範圍在 min <= N < max 的桶中
- 空的路由表只有一個桶,所以所有的節點都將被放到這個桶中。每個桶最多隻能保存 K 個節點,當前 K=8。當一個桶放滿了好節點之後,將不再允許新的節點加入,除非我們自身的節點 ID 在這個桶的範圍內。在這樣的情況下,這個桶將被分裂爲 2 個新的桶,每個新桶的範圍都是原來舊桶的一半。原來舊桶中的節點將被重新分配到這兩個新的桶中。如果一個新表只有一個桶,這個包含整個範圍的桶將總被分裂爲 2 個新的桶,每個桶的覆蓋範圍從 0…2^159 和 2159…2160,以log2N的方式不斷分裂,類似於Kademlia中的K桶機制
- 當桶裝滿了好節點,新的節點會被丟棄。一旦桶中的某個節點變爲了壞的節點,那麼我們就用新的節點來替換這個壞的節點。如果桶中有在 15 分鐘內都沒有活躍過的節點,我們將這樣的節點視爲可疑的節點,這時我們向最久沒有聯繫的節點發送 ping。如果被 ping 的節點給出了回覆,那麼我們向下一個可疑的節點發送 ping,不斷這樣循環下去,直到有某一個節點沒有給出 ping 的回覆,或者當前桶中的所有節點都是好的(也就是所有節點都不是可疑節點,他們在過去 15 分鐘內都有活動)。如果桶中的某個節點沒有對我們的 ping 給出回覆,我們最好再試一次(再發送一次 ping,因爲這個節點也許仍然是活躍的,但由於網絡擁塞,所以發生了丟包現象,注意 DHT 的包都是 UDP 的),而不是立即丟棄這個節點或者直接用新節點來替代它。這樣,我們得路由表將充滿穩定的長時間在線的節點
- 每個桶都應該維持一個 lastchange 字段來表明桶中節點的"新鮮"度。當桶中的節點被 ping 並給出了回覆,或者一個節點被加入到了桶,或者一個節點被新的節點所替代,桶的 lastchange 字段都應當被更新。如果一個桶的 lastchange 在過去的 15 分鐘內都沒有變化,那麼我們將更新它。這個更新桶操作是這樣完成的
- 從這個桶所覆蓋的範圍中隨機選擇一個 ID,並對這個 ID 執行 find_nodes 查找操作。常常收到請求的節點通常不需要常常更新自己的桶
- 反之,不常常收到請求的節點常常需要週期性的執行更新所有桶的操作,這樣才能保證當我們用到 DHT 的時候,裏面有足夠多的好的節點
- 在插入第一個節點到路由表並啓動服務後,這個節點應試着查找 DHT 中離自己更近的節點,這個查找工作是通過不斷的發出 find_node 消息給越來越近的節點來完成的,當不能找到更近的節點時,這個擴散工作就結束了
- 路由表應當被啓動工作和客戶端軟件保存(也就是啓動的時候從客戶端中讀取路由表信息,結束的時候客戶端軟件記錄到文件中)
4. bt協議擴展
BitTorrent 協議已經被擴展爲可以在通過 tracker 得到的 peer 之間互相交換節點的 UDP 端口號(也就是告訴對方我們的 DHT 服務端口號),在這樣的方式下,客戶端可以通過下載普通的種子文件來自動擴展 DHT 路由表(我直接知道某個節點有某一個資源)。新安裝的客戶端第一次試着下載一個無 tracker 的種子時,它的路由表中將沒有任何節點,這是它需要在 torrent 文件中找到聯繫信息
- peers 如果支持 DHT 協議就將 BitTorrent 協議握手消息的保留位的第 8 字節的最後一位置爲 1
- 這時如果 peer 收到一個 handshake 表明對方支持 DHT 協議,就應該發送 PORT 消息。它由字節 0x09 開始,payload 的長度是 2 個字節,包含了這個 peer 的 DHT 服務使用的網絡字節序的 UDP 端口號
- 當 peer 收到這樣的消息時應當向對方的 IP 和消息中指定的端口號的節點發送 ping
- 如果收到了 ping 的回覆,那麼應當使用上述的方法將新節點的聯繫信息加入到路由表中
5. torrent文件擴展
一個無 tracker 的 torrent 文件字典不包含 announce 關鍵字,而使用 nodes 關鍵字來替代。這個關鍵字對應的內容應該設置爲 torrent 創建者的路由表中 K 個最接近的節點(可供選擇的),這個關鍵字也可以設置爲一個已知的可用節點(這意味着接收到這個種子文件的客戶端能夠向這些節點發出解析請求,詢問資源的所在位置),比如這個 torrent 文件的創建者
不要自動加入 router.bittorrent.com 到 torrent 文件中或者自動加入這個節點到客戶端路由表中。這麼做還有另一個好處,這個對等網絡可以保持無中心化,對於外部新加入的新節點來說,它可以不用通過"中心引導節點"來加入網絡,隱藏了"中心引導節點"的存在,增強了對等網絡的隱蔽性。
nodes= [["", ], ["",], …]
nodes= [[“127.0.0.1”, 6881], [“your.router.node”,4804]]
6. KRPC協議
KRPC是BitTorrent在Kademlia理論基礎之上定義的一個通信消息格式協議,主要用來支持peer節點的獲取(get_peer)和peer節點的聲明(announce_peer),以及判活心跳(ping)、節點尋址(find_node),它在find_node的原理上和DHT是一樣的,同時增加了get_peer / announce_peer / ping協議的支持。KRPC協議是由B編碼組成的一個簡單的RPC結構,有4種請求:ping、find_node、get_peers 和 announce_peer。
Peers的聯繫信息被編碼爲6字節的字符串。又被稱爲"CompactIP-address/port info",其中前4個字節是網絡字節序的IP地址,後2個字節是網絡字節序的端口。
Nodes的聯繫信息被編碼爲26字節的字符串。又被稱爲"Compactnode info",其中前20字節是網絡字節序的nodeID,後面6個字節是peers的"CompactIP-address/port info"。
KRPC協議框架如下:
- t關鍵字: 每條消息都包含 t 關鍵字,它是一個代表了 transaction ID 的字符串。transaction ID 由請求節點產生,並且回覆中要包含回顯該字段(挑戰-響應模型),所以回覆可能對應一個節點的多個請求。transaction ID 應當被編碼爲一個短的二進制字符串,比如 2 個字節,這樣就可以對應 2^16 個請求
- y關鍵字: 它由一個字節組成,表明這個消息的類型。y 對應的值有三種情況
- q 表示請求(請求Queries): q類型的消息它包含 2 個附加的關鍵字 q 和 a
1.1) 關鍵字 q: 是字符串類型,包含了請求的方法名字(get_peers/announce_peer/ping/find_node)
1.2) 關鍵字 a: 一個字典類型包含了請求所附加的參數(info_hash/id…) - r 表示回覆(回覆 Responses): 包含了返回的值。發送回覆消息是在正確解析了請求消息的基礎上完成的,包含了一個附加的關鍵字 r。關鍵字 r 是字典類型
2.1) id: peer節點id號或者下一跳DHT節點
2.2) nodes": “”
2.3) token: token - e 表示錯誤(錯誤 Errors): 包含一個附加的關鍵字 e,關鍵字 e 是列表類型
3.1) 第一個元素是數字類型,表明了錯誤碼,當一個請求不能解析或出錯時,錯誤包將被髮送。下表描述了可能出現的錯誤碼
201: 一般錯誤
202: 服務錯誤
203: 協議錯誤,比如不規範的包,無效的參數,或者錯誤的 toke
204: 未知方法
3.2) 第二個元素是字符串類型,表明了錯誤信息
錯誤包例子:
一般錯誤={“t”:“aa”, “y”:“e”, “e”:[201,“A Generic Error Ocurred”]}
B編碼=d1:eli201e23:AGenericErrorOcurrede1:t2:aa1:y1:ee
- q 表示請求(請求Queries): q類型的消息它包含 2 個附加的關鍵字 q 和 a
7. DHT請求
所有的請求都包含一個關鍵字id,它包含了請求節點的nodeID。所有的回覆也包含關鍵字id,它包含了回覆節點的nodeID。
(1) ping
最基礎的請求就是ping,用於檢測節點是否可達。這時KPRC協議中的“q”=“ping”。Ping請求包含一個參數id,它是一個20字節的字符串包含了發送者網絡字節序的nodeID。對應的ping回覆也包含一個參數id,包含了回覆者的nodeID。
參數: {“id” : “”}
回覆:{“id” : “”}
報文包例子
ping Query =
{“t”:“aa”,“y”:“q”,“q”:“ping”,“a”:{“id”:“abcdefghij0123456789”}}
bencoded =
d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
Response =
{“t”:“aa”, “y”:“r”, “r”: {“id”:“mnopqrstuvwxyz123456”}}
bencoded =
d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
(2) find_node
Findnode被用來查找給定ID的node的聯繫信息。這時KPRC協議中的q=“find_node”。find_node請求包含2個參數,第一個參數是id,包含了請求node的nodeID。第二個參數是target,包含了請求者正在查找的node的nodeID。當一個node接收到了find_node的請求,他應該給出對應的回覆,回覆中包含2個關鍵字id和nodes,nodes是一個字符串類型,包含了被請求節點的路由表中最接近目標node的K(8)個最接近的nodes的聯繫信息。
參數: {“id” : “”, “target” : “”}
回覆: {“id” : “”, “nodes” : “”}
報文包例子
find_node請求
={“t”:“aa”,“y”:“q”,“q”:“find_node”,“a”:{“id”:“abcdefghij0123456789”,“target”:“mnopqrstuvwxyz123456”}}
B編碼
=d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe
回覆={“t”:“aa”, “y”:“r”, “r”:{“id”:“0123456789abcdefghij”, “nodes”:“def456…”}}
B編碼
=d1:rd2:id20:0123456789abcdefghij5:nodes9:def456…e1:t2:aa1:y1:re
(3) get_peers
Getpeers與torrent文件的info_hash有關。這時KPRC協議中的”q”=”get_peers”。get_peers請求包含2個參數。第一個參數是id,包含了請求node的nodeID。第二個參數是info_hash,它代表torrent文件的infohash。如果被請求的節點有對應info_hash的peers,他將返回一個關鍵字values,這是一個列表類型的字符串。每一個字符串包含了"CompactIP-address/portinfo"格式的peers信息。如果被請求的節點沒有這個infohash的peers,那麼他將返回關鍵字nodes,這個關鍵字包含了被請求節點的路由表中離info_hash最近的K個nodes,使用"Compactnodeinfo"格式回覆。在這兩種情況下,關鍵字token都將被返回。token關鍵字在今後的annouce_peer請求中必須要攜帶。Token是一個短的二進制字符串。
參數: {“id” : “”,“info_hash” : “<20-byte infohash of targettorrent>”}
回覆:{“id” : “”,“token” :"",“values” : ["<peer 1 info string>","<peer 2 info string>"]}
or:{“id” : “”,“token” :"",“nodes” : “”}
報文包例子
get_peers請求
={“t”:“aa”,“y”:“q”,“q”:“get_peers”,“a”:{“id”:“abcdefghij0123456789”,“info_hash”:“mnopqrstuvwxyz123456”}}
B編碼
=d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe
回覆peers
={“t”:“aa”,“y”:“r”,“r”:{“id”:“abcdefghij0123456789”, “token”:“aoeusnth”,“values”: [“axje.u”, “idhtnm”]}}
B編碼
=d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:re
回覆最接近的nodes=
{“t”:“aa”,“y”:“r”,“r”:{“id”:“abcdefghij0123456789”,“token”:“aoeusnth”,“nodes”: “def456…”}}
B編碼
=d1:rd2:id20:abcdefghij01234567895:nodes9:def456…5:token8:aoeusnthe1:t2:aa1:y1:re
(4) announce_peer
這個請求用來表明發出announce_peer請求的node,正在某個端口下載torrent文件。announce_peer包含4個參數。第一個參數是id,包含了請求node的nodeID;第二個參數是info_hash,包含了torrent文件的infohash;第三個參數是port包含了整型的端口號,表明peer在哪個端口下載;第四個參數數是token,這是在之前的get_peers請求中收到的回覆中包含的。收到announce_peer請求的node必須檢查這個token與之前我們回覆給這個節點get_peers的token是否相同。如果相同,那麼被請求的節點將記錄發送announce_peer節點的IP和請求中包含的port端口號在peer聯繫信息中對應的infohash下。
參數: {“id”: “”, “info_hash” :"<20-byte infohash of target torrent>", “port”: , “token” : “”}
回覆: {“id”: “”}
報文包例子
announce_peers請求={“t”:“aa”,“y”:“q”,“q”:“announce_peer”, “a”:{“id”:“abcdefghij0123456789”,“info_hash”:“mnopqrstuvwxyz123456”, “port”:6881, “token”: “aoeusnth”}}
B編碼
=d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe
回覆={“t”:“aa”, “y”:“r”, “r”:{“id”:“mnopqrstuvwxyz123456”}}
B編碼=d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
8. utp
uTP協議是一個基於UDP的開放的BT點對點文件共享協議。在uTP協議出現之前,BT下載會佔用網絡中大量的鏈接,直接導致其它網絡應用服務質量下載和網絡的擁堵,因此有很多ISP都開始限制BT的下載。uTP減輕了網絡延遲並解決了傳統的基於TCP的BT協議所遇到的擁塞控制問題,提供可靠的有序的傳送。
9. Peer Wire協議
在BitTorrent中,節點的尋址是通過DHT實現的,而實際的資源共享和傳輸則需要通過uTP以及Peer Wire協議來配合完成
(1)握手
Peer Wire協議是Peer之間的通信協議,通常由一個握手消息開始。握手是一個必需的報文,並且必須是客戶端發送的第一個報文。該握手報文的長度是(49+len(pstr))字節。握手消息的格式如下:
<pstrlen><pstr><reserved><info_hash><peer_id>
pstrlen: 字符串長度,單個字節。
pstr: 協議的標識符,字符串類型。
reserved: 8個保留字節。當前的所有實現都使用0.這些字節裏面的每一個字節都可以用來改變協議的行爲。來自Bram的郵件建議應該首先使用後面的位,以便可以使用前面的位來改變後面位的意義。
info_hash: 元信息文件中info鍵(key)對應值的20字節SHA1哈希。這個info_hash和在tracker請求中info_hash是同一個。
peer_id: 用於唯一標識客戶端的20字節字符串。這個peer_id通常跟在tracker請求中傳送的peer_id相同(但也不盡然,例如在Azureus,就有一個匿名選項)。
在BitTorrent協議1.0版本,pstrlen = 19, pstr = “BitTorrent protocol”。
連接的發起者應該立即發送握手報文。如果接收方能夠同時地服務多個torrent,它會等待發起者的握手報文(torrent由infohash唯一標識)。儘管如此,一旦接收方看到握手報文中的info_hash部分,接收方必須儘快響應。tracker的NAT-checking特性不會發送握手報文的peer_id字段。
如果一個客戶端接收到一個握手報文,並且該客戶端沒有服務這個報文的info_hash,那麼該客戶端必須丟棄該連接。
如果一個連接發起者接收到一個握手報文,並且該報文中peer_id與期望的peer_id不匹配,那麼連接發起者應該丟棄該連接。注意發起者可能接收來自tracker的peer信息,該信息包含peer註冊的peer_id。來自於tracker的peer_id需要匹配握手報文中的peer_id。
peer_id長20個字節。至於怎麼將客戶端和客戶端版本信息編碼成peer_id,現在主要有兩種慣例:Azureus風格和Shadow風格。
Azureus風格使用如下編碼方式:’-’, 緊接着是2個字符的client id,再接着是4個數字的版本號,’-’,後面跟着隨機數。
例如:’-AZ2060-’…
Shadow風格使用如下編碼方式:一個用於客戶端標識的ASCII字母數字,多達五個字符的版本號(如果少於5個,則以’-’填充),緊接着是3個字符(通常是’—’,但也不總是這樣),最後跟着隨機數。版本字符串中的每一個字符表示一個0到63的數字。‘0’=0, …, ‘9’=9, ‘A’=10, …, ‘Z’=35, ‘a’=36, …, ‘z’=61, ‘.’=62, ‘-’=63。
(2)狀態信息
一個客戶端(client)必須維持其與每一個遠程peer(端)連接的狀態信息:
choked: 遠程peer(端)是否已經choke本客戶端。當一個peer(端) choke本客戶端後,它是在通知本客戶端,除非它unchoke本客戶端,否則它不會應答該客戶端所發出的任何請求。本客戶端也不應該試圖向遠程peer發送數據請求,並且應該認爲所有沒有應答的請求已經被遠程peer丟棄。
interested: 遠程peer(端)是否對本客戶端提供的數據感興趣。這是遠程peer在通知本客戶端,當本客戶端unchoke他們時,遠程客戶端將開始請求塊(block)。注意這也意味着本客戶端需要記錄它是否對遠程 peer(端)感興趣,以及它是否choke/unchoke遠程peer。因此真正的列表看起來像這樣:
am_choking: 本客戶端正在choke遠程peer。
am_interested: 本客戶端對遠程peer感興趣。
peer_choking: 遠程peer正choke本客戶端。
peer_interested: 遠程peer對本客戶端感興趣。
客戶端連接開始時狀態是choke和not interested(不感興趣)。換句話就是:
am_choking = 1
am_interested = 0
peer_choking = 1
peer_interested = 0
當一個客戶端對一個遠程peer感興趣並且那個遠程peer沒有choke這個客戶端,那麼這個客戶端就可以從遠程peer下載塊(block)。當一個客戶端沒有choke一個peer,並且那個peer對這個客戶端這個感興趣時,這個客戶端就會上傳塊(block)。
客戶端必須不斷通知它的peers,它是否對它們感興趣,這一點是很重要的。客戶端和每個端的狀態信息必須保持最新,即使本客戶端被choke。這允許所有的peer知道,當它們unchoke該客戶端後,該客戶端是否開始下載(反之亦然)。
10. 獲取資源完成下載
磁力鏈是爲了簡化BT種子文件的分發,封裝了一個簡化版的magnet url,客戶端解析這個magnet磁力鏈之後,需要在DHT網絡中尋找infohash對應的peer節點,獲取節點成功後,向目標peer節點獲取真正的BitTorrent種子(torrent文件)信息(包含了完整的pieces SHA1雜湊信息),另一個渠道就是傳統的Bt種子論壇會分發BT種子文件。
magnet URL的格式如下:
magnet:?xt=urn:btih:&dn=&tr=
- : Infohash的16進制編碼,共40字符。爲了與其它的編碼兼容,客戶端應當也支持32字符的infohash base32編碼
- Xt是唯一強制的參數
- dn是在等待metadata時可能供客戶端顯示的名字
- 如果只有一個字段,Tr是tracker的url,如果有很多的tracker,那麼多個tr字段會被包含進去
#dn和tr都是可選的
爲了獲取目標資源的種子信息(infohash/filename/pieces分塊sha1),需要在Peer Wire握手之後發出擴展支持交互消息。這是一個B編碼的字典,包括三類不同的消息。
1 request(請求):
請求消息並不在字典中附加任何關鍵字,這個消息的回覆應當來自支持這個擴展的peer,是一個reject或者data消息,回覆必須和請求所指出的片相同
Peer必須保證它所發送的每個片都通過了infohash的檢測。即直到peer獲得了整個metadata並通過了infohash的驗證,才能夠發送片(即一個peer應該保證自己已經完整從其他peer中拷貝了一份相同的資源文件後,才能繼續響應其他節點的拷貝請求)。Peers沒有獲得整個metadata時,對收到的所有metadata請求都必須直接回復reject消息
#exampel
{'msg_type': 0, 'piece': 0}
d8:msg_typei0e5:piecei0ee
#這代表請求消息在請求metadata的第一片
2 data
這個data消息需要在字典中添加一個新的字段,“total_size”.這個關鍵字段和extension頭的"metadata_size"有相同的含義,這是一個整型
Metadata片被添加到bencode字典後面,他不是字典的一部分,但是是消息的一部分(必須包括長度前綴)。
如果這個片是metadata的最後一個片,他可能小於16KB。如果它不是metadata的最後一片,那大小必須是16KB
# example
{'msg_type': 1, 'piece': 0, 'total_size': 3425}
d8:msg_typei1e5:piecei0e10:total_sizei34256eexxxxxxxx...
# x表示二進制數據(metadata)
3 reject
Reject消息沒有附件的關鍵字。它的意思是peer沒有請求的這個metadata片信息
在客戶端收到收到一定數目的消息後,可以通過拒絕請求消息來進行洪泛攻擊保護。尤其在metadata的數目乘上一個因子時
#
{'msg_type': 2, 'piece': 0}
d8:msg_typei1e5:piecei0ee
4 request請求獲取信息的格式如下:
{
e: 0,
ipv4: xxx,
ipv6: xxx,
complete_ago: 1,
m:
{
upload_only: 3,
lt_donthave: 7,
ut_holepunch: 4,
ut_metadata: 2,
ut_pex: 1,
ut_comment: 6
},
matadata_size: 45377,
p: 33733,
reqq: 255,
v: BitTorrent 7.9.3
yp: 19616,
yourip: xxx
}
m: 是一個字典,表示客戶端支持的所有擴展以及每個擴展的編號
1) ut_pex: 表示該客戶端支持PEX(Peer Exchange)
2) ut_metadata表示支持BEP-009(也就是交換種子文件的metadata)
總結
本文包含了BT下載相關知識的介紹,可能還有很多遺漏,後續再繼續補充。