BitTorrent協議的種子文件(英語:Torrent file)能夠保存一組文件的元數據。這種格式的文件被BitTorrent協議所定義。擴展名通常爲「.torrent」。java
.torrent種子文件本質上是文本文件,包含Tracker信息和文件信息兩部分。Tracker信息主要是BT下載中須要用到的Tracker服務器的地址和針對Tracker服務器的設置,文件信息是根據對目標文件的計算生成的,計算結果根據BitTorrent協議內的Bencode規則進行編碼。它的主要原理是須要把提供下載的文件虛擬分紅大小相等的塊,塊大小必須爲2k的整數次方(因爲是虛擬分塊,硬盤上並不產生各個塊文件),並把每一個塊的索引信息和Hash驗證碼寫入種子文件中;因此,種子文件就是被下載文件的「索引」。git
Torrent文件內容都已Bencoding編碼類型進行存儲,總體上是一個字典結構,見下:github
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
announce | string | required | Tracker的Url |
info | dictionary | required | 該條映射到一個字典,該字典的鍵將取決於共享的一個或多個文件 |
announce-list | array[] | optional | 備用Tracker的Url,以列表形式存在 |
comment | string | optional | 備註 |
created by | string | optional | 建立人或建立程序的信息 |
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
name | string | required | 建議保存到的文件名稱 |
piceces | byte[] | required | 每一個文件塊的SHA-1的集成Hash。 |
piece length | long | required | 每一個文件塊的字節數 |
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
name | string | required | 建議保存到的目錄名稱 |
piceces | byte[] | required | 每一個文件塊的SHA-1的集成Hash。 |
piece length | long | required | 每一個文件塊的字節數 |
files | array[] | required | 文件列表,列表存儲的內容是字典結構 |
files字典結構:json
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
path | array[] | required | 一個對應子目錄名的字符串列表,最後一項是實際的文件名稱 |
length | long | required | 文件的大小(以字節爲單位) |
以JSON
序列化整個字典後,單文件和多文件的結構大體以下,注意:JSON內容省略了pieces摘要大部份內容,僅展現了開頭部分,另外因爲本人序列化工具設置所致,全部的整型都會序列化成字符串類型。數組
單文件結構服務器
{ "creation date": "1581674765", "comment": "dynamic metainfo from client", "announce-list": [ [ "udp://tracker.leechers-paradise.org:6969/announce" ], [ "udp://tracker.internetwarriors.net:1337/announce" ], [ "udp://tracker.opentrackr.org:1337/announce" ], [ "udp://tracker.coppersurfer.tk:6969/announce" ], [ "udp://tracker.pirateparty.gr:6969/announce" ] ], "created by": "go.torrent", "announce": "udp://tracker.leechers-paradise.org:6969/announce", "info": { "pieces": "レJᅯ\ufff4ᅯ*f\nᄍ\ufff0... ...", "length": "54358058387", "name": "Frozen.II.2019.BDREMUX.2160p.HDR.seleZen.mkv", "piece length": "16777216" } }
多文件結構函數
{ "creation date": "1604347014", "comment": "Torrent downloaded from https://YTS.MX", "announce-list": [ [ "udp://tracker.coppersurfer.tk:6969/announce" ], [ "udp://9.rarbg.com:2710/announce" ], [ "udp://p4p.arenabg.com:1337" ], [ "udp://tracker.internetwarriors.net:1337" ], [ "udp://tracker.opentrackr.org:1337/announce" ] ], "created by": "YTS.AG", "announce": "udp://tracker.coppersurfer.tk:6969/announce", "info": { "pieces": "ᆲimᅬヒ\u000b*゚ᆲト... ...", "name": "Love And Monsters (2020) [2160p] [4K] [WEB] [5.1] [YTS.MX]", "files": [ { "path": [ "Love.And.Monsters.2020.2160p.4K.WEB.x265.10bit.mkv" ], "length": "5215702961" }, { "path": [ "www.YTS.MX.jpg" ], "length": "53226" } ], "piece length": "524288" } }
根據上文所說,Torrent文件均以Bencoding編碼進行存儲,故咱們須要大體瞭解一下Bencoding編碼。工具
Bencoding以四種基本類型數據構成:ui
字符串類型由如下結構表示:字符串長度:字符串原文
,例如:42:udp://tracker.pirateparty.gr:6969/announce
。編碼
整型類型由如下結構表示:i<整形數據>e
,例如i1234e
,則代表的整形數據爲1234。
列表類型由如下結構表示:l<列表數據>e
,即列表以字母l
開頭,以字母e
結束,中間的均爲列表中的數據,中間的值能夠爲任意的四種類型之一。
字典類型由如下結構表示:d<字典數據>e
,即字典由字母d
開頭,以字母e
結束,中間的均爲字典中的數據,中間的值能夠爲任意的四種類型之一。
根據上述描述來看看實際的內容解析,咱們如下方的數據爲例:
d8:announce49:udp://tracker.leechers-paradise.org:6969/announce13:announce-listll49:udp://tracker.leechers-paradise.org:6969/announceel48:udp://tracker.internetwarriors.net:1337/announceeee
你們能夠先嚐試根據上面的內容對這一串內容進行解析,我將這一串數據拆分開來方便你們理解和查看,能夠明顯看出其由一個擁有兩個鍵值的字典,其中一個鍵爲announce
,另外一個鍵爲announce-list
,二者的值一個爲udp://tracker.leechers-paradise.org:6969/announce
,一個爲列表,列表內還嵌套了一層列表。
d 8:announce 49:udp://tracker.leechers-paradise.org:6969/announce 13:announce-list l l 49:udp://tracker.leechers-paradise.org:6969/announce e l 48:udp://tracker.internetwarriors.net:1337/announce e e e
根據上文對Torrent文件編碼的瞭解,那麼咱們使用代碼對Torrent文件就很簡單了。咱們只須要讀取種子字節流,判斷具體是哪一種類型並進行相應轉換便可。
即:讀取文件字節,判斷字節屬於哪種類型:0-9 : 字符串類型、i:整形數據、l:列表數據、d:字典數據
再根據每一個數據具體類型獲取該數據的內容,再讀取下一個文件字節獲取下一個數據類型便可,根據這個分析,僞代碼以下:
// 當讀取到字節對應的內容爲0-9時進入該方法 String readString(byte[] info,int offset) { // 讀取‘:’之前的數據,即字符串長度 int length = readLength(info,offset); // 根據字符串長度,獲取實際字符串內容 string data = readData(info,length,offset); // 返回讀取到的字符串內容,整個讀取過程當中讀過的偏移量要累加到offset return data; }
這裏有一個注意項,考慮到數據邊界問題,例如java
等語言,推薦使用Long
類型,以防數據越界。
// 當讀取到的字節對應的內容爲i時,進入該方法 Long readInt(byte[] info,int offset) { // 讀取第一個'e'以前的數據,包括'e' string data = readInt(info,offset) return Long.valueOf(data); }
由於列表類型中能夠夾雜全部四種類型中任意要給即須要用到上面兩個方法。
// 當讀取到的字節對應的內容爲l時,進入該方法 List readList(byte[] info,int offset){ List list = new List(); // 讀取到第一個'e'爲止 while(info[offset] != 'e'){ swtich(info[offset]){ // 若是是列表,讀取列表並向列表添加 case 'l': list.add(readList(info,offset)); break; // 若是是字典,讀取字典並向列表添加 case 'd': list.add(readDictionary(info,offset)); break; // 若是是整形數據,讀取數據並向列表添加 case 'i': list.add(readInt(info,offset)); break; // 若是是字符串,讀取字符串數據並向列表添加 case '0-9': list.add(readString(info,offset)); } } // offset向前移一位,把列表的結束符'e'移動爲已讀 offset++; return list; }
讀取字典類型與列表十分類似,惟一不一樣的就是須要區分鍵值,字典的鍵只可能爲字符串,故依次來判斷。
// 當讀取到的字節對應的內容爲d時,進入該方法 Dictionary readDictionary(byte[] info,int offset){ Dictionary dic = new Dictionary(); // key爲null時,字符串爲鍵,不然爲值 String key = null; // 讀取到第一個'e'爲止 while(info[offset] != 'e'){ swtich(info[offset]){ // 若是是列表,讀取列表並向字典添加,添加列表時確定存在鍵,直接添加並將鍵置空 case 'l': dic.put(key,readList(info,offset)); key = null; break; // 若是是字典,讀取字典並向字典添加,添加字典時確定存在鍵,直接添加並將鍵置空 case 'd': dic.put(key,readDictionary(info,offset)); key = null; break; // 若是是整形數據,讀取數據並向字典添加,添加整形數據時確定存在鍵,直接添加並將鍵置空 case 'i': dic.put(key,readInt(info,offset)); key = null; break; // 若是是字符串 case '0-9': string data = readString(info,offset); // key爲null時,字符串爲鍵,不然爲值 if(key == null){ key = data; }else{ dic.put(key,data); key = null; } } } // offset向前移一位,把列表的結束符'e'移動爲已讀 offset++; return dic; }
磁力連接與Torrent文件是能夠相互轉換的,此文只討論根據Torrent文件如何轉換爲Magnet磁力連接。
磁力連接由一組參數組成,參數間的順序沒有講究,其格式與在HTTP連接末尾的查詢字符串相同。最多見的參數是"xt",是"exact topic"的縮寫,一般是一個特定文件的內容散列函數值造成的URN,例如:
magnet:?xt=urn:bith:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C
注意,雖然這個連接指向一個特定文件,可是客戶端應用程序仍然必須進行搜索來肯定哪裏,若是有,可以獲取那個文件(即經過DHT進行搜索,這樣就實現了Magnet到Torrent的轉換,本文不討論)。
部分字段名見下方表格:
字段名 | 含義 |
---|---|
magnet | 協議名 |
xt | exact topic的縮寫,包含文件哈希值的統一資源名稱。BTIH(BitTorrent Info Hash)表示哈希方法名,這裏還可使用ED2K,AICH,SHA1和MD5等。這個值是文件的標識符,是不可缺乏的。 |
dn | display name的縮寫,表示向用戶顯示的文件名。這一項是選填的。 |
tr | tracker的縮寫,表示tracker服務器的地址。這一項也是選填的。 |
bith | BitTorrent info hash,種子散列函數 |
即爲Torrent文件中,Info字典下的name鍵所對應的值
即爲Torrent文件中,announce以及announce-list兩個鍵所對應的值
即爲Torrent文件中,info對應的字典的SHA1哈希值(Hex)
根據下圖,爲4:infod
,以d
的地址做爲哈希原文的起始索引,則爲Adress:00 01A3
到整個info結束,以e
的地址做爲哈希原文的終止索引地址,則爲Adress:03 0BE7
根據上述可知:
magnet = 'magnet:?xt=urn:btih:'+Hex(Sha1(info))+'&dn='+encode(name)+'&tr='+encode(announce)
結合上一部分的實現,咱們能夠在讀取info時記錄startindex和endindex,即:
Dictionary readDictionary(byte[] info,int offset){ //... case 'd': bool record = key == 'info'; if(record){ startindex = offset; } readDictoinary(info,offset); if(record){ endindex = offset } } string getBith(byte[] info,int start,int end){ // 獲取info中從start到end的字節數組,並對其進行摘要計算 byte[] infoByte = new byte[infoEnd - infoStart + 1]; System.arraycopy(torrentBytes, infoStart, infoByte, 0, infoEnd - infoStart + 1); return Hex.toHex(Sha1.toSha1(infoByte)); }
本人經過Java實現了以上部分邏輯(Torrent文件解析以及Magnet連接生成),如有須要參考的讀者能夠到如下網址獲取相關內容:
工具類目錄:https://github.com/Rekent/common-utils/tree/master/src/main/java/com/rekent/tools/utils/torrent
依賴jar包:https://github.com/Rekent/common-utils/releases/tag/v0.0.3
調用方式:
public void testResolve() throws Exception { String path = "C:\\Users\\Refkent\\Downloads\\Test.torrent"; TorrentFile torrentFile = TorrentFileUtils.resolve(path); System.out.println(torrentFile.print()); System.out.println(torrentFile.getHash()); System.out.println(torrentFile.getMagnetUri()); }