Torrent文件的解析與轉換

Torrent簡介

BitTorrent協議的種子文件(英語:Torrent file)能夠保存一組文件的元數據。這種格式的文件被BitTorrent協議所定義。擴展名通常爲「.torrent」。java

.torrent種子文件本質上是文本文件,包含Tracker信息和文件信息兩部分。Tracker信息主要是BT下載中須要用到的Tracker服務器的地址和針對Tracker服務器的設置,文件信息是根據對目標文件的計算生成的,計算結果根據BitTorrent協議內的Bencode規則進行編碼。它的主要原理是須要把提供下載的文件虛擬分紅大小相等的塊,塊大小必須爲2k的整數次方(因爲是虛擬分塊,硬盤上並不產生各個塊文件),並把每一個塊的索引信息和Hash驗證碼寫入種子文件中;因此,種子文件就是被下載文件的「索引」。git

Torrent結構

Torrent文件內容都已Bencoding編碼類型進行存儲,總體上是一個字典結構,見下:github

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字典結構:json

鍵名稱 數據類型 可選項 鍵值含義
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以四種基本類型數據構成:ui

  • 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
開始offset
到整個info結束,以e的地址做爲哈希原文的終止索引地址,則爲Adress:03 0BE7
結束offset

根據上述可知:

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

解析類源碼:https://github.com/Rekent/common-utils/blob/master/src/main/java/com/rekent/tools/utils/torrent/TorrentFileResovler.java

依賴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());
}

Reference

相關文章
相關標籤/搜索