Torrent簡介
BitTorrent協議的種子文件(英語:Torrent file)可以保存一組文件的元數據。這種格式的文件被BitTorrent協議所定義。擴展名一般為“.torrent”。
.torrent種子文件本質上是文本文件,包含Tracker信息和文件信息兩部分。Tracker信息主要是BT下載中需要用到的Tracker服務器的地址和針對Tracker服務器的設置,文件信息是根據對目標文件的計算生成的,計算結果根據BitTorrent協議內的Bencode規則進行編碼。它的主要原理是需要把提供下載的文件虛擬分成大小相等的塊,塊大小必須為2k的整數次方(由於是虛擬分塊,硬盤上並不產生各個塊文件),並把每個塊的索引信息和Hash驗證碼寫入種子文件中;所以,種子文件就是被下載文件的“索引”。
Torrent結構
Torrent文件內容都已Bencoding編碼類型進行存儲,整體上是一個字典結構,見下:
Torrent總體結構
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
announce | string | required | Tracker的Url |
info | dictionary | required | 該條映射到一個字典,該字典的鍵將取決於共享的一個或多個文件 |
announce-list | array[] | optional | 備用Tracker的Url,以列表形式存在 |
comment | string | optional | 備注 |
created by | string | optional | 創建人或創建程序的信息 |
Torrent單文件Info結構
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
name | string | required | 建議保存到的文件名稱 |
piceces | byte[] | required | 每個文件塊的SHA-1的集成Hash。 |
piece length | long | required | 每個文件塊的字節數 |
Torrent多文件Info結構
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
name | string | required | 建議保存到的目錄名稱 |
piceces | byte[] | required | 每個文件塊的SHA-1的集成Hash。 |
piece length | long | required | 每個文件塊的字節數 |
files | array[] | required | 文件列表,列表存儲的內容是字典結構 |
files字典結構:
鍵名稱 | 數據類型 | 可選項 | 鍵值含義 |
---|---|---|---|
path | array[] | required | 一個對應子目錄名的字符串列表,最后一項是實際的文件名稱 |
length | long | required | 文件的大小(以字節為單位) |
Torrent實際結構預覽
以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文件編碼
根據上文所說,Torrent文件均以Bencoding編碼進行存儲,故我們需要大致了解一下Bencoding編碼。
Bencoding以四種基本類型數據構成:
- string : 字符串
- intergers : 整數類型
- lists:列表類型
- dictionary:字典類型
字符串類型
字符串類型由以下結構表示:字符串長度:字符串原文
,例如: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文件編碼的了解,那么我們使用代碼對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文件與Magnet
磁力鏈接與Torrent文件是可以相互轉換的,此文只討論根據Torrent文件如何轉換為Magnet磁力鏈接。
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轉換為Magnet
- dn : 向用戶顯示的文件名
即為Torrent文件中,Info字典下的name鍵所對應的值
- tr : tracker服務器地址
即為Torrent文件中,announce以及announce-list兩個鍵所對應的值
- bitch : 種子散列值
即為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());
}