Home » Code » torrent种子结构解析与B编码

torrent种子结构解析与B编码

BT(PT)下载相信大家都不陌生,最近就看了看BT协议,发现相关资料比较少,尤其是中文的,故将自己知道的记录一下。今天先是了解一下torrent种子文件的大体结构以及如何解析出相关信息,当然,少不了BT协议中的关键点:B编码(Bencoding)。

torrent种子文件结构

种子文件里的内容其实就是文本,是经过B编码之后的文本。它主要包含以下字段:

  • info,必须。一个描述torrent文件的字典,有两种可能形式,一种是没有目录结构的“单一文件”,一种是有目录结构的“多文件”;
  • announce,必须。tracker服务器的地址URL(字符串);
  • announce-list,可选。tracker服务器列表,这是官方规范的一个扩展,向后兼容。可用来存储备用服务器列表;
  • create date,可选。torrent文件的创建时间,为Unix时间戳;
  • comment,可选。一些备注信息之类;
  • created by,可选。说明torrent文件是由哪个程序创建的;
  • encodeing,可选。info字典中pieces字段的编码格式。

其中info字段又包含以下的结构:

  • piece length,必须。每个piece的长度;
  • pieces,必须。20字节的SHA1散列值,每块(piece)一个,没有经过urlencode的字符串;
  • private,可选。如果设置为1,客户只从种子规定的tracker获取peers,如果为0,会从外部获取。对于PT来说,肯定是1;

对于“单一文件”,其info字段还包含以下信息:

  • name,必须。文件名,下载时可以更改;
  • length,必须。文件的字节数;
  • md5sum,可选。一个32位16进制的字符串,其实就是文件md5值之和??(这不就一个文件?看英文吧),并不是BT必须,只是某些程序需要。

对于“多文件”,其info字段则还包含以下信息:

  • name,必须。“多文件”所在的文件夹名称,下载时可以更改;
  • files,必须。字典列表,每个文件都有一个,每个字典又包含以下字段:
  • —path,必须。代表当前路径和文件名的一个或多个字符串;
  • —length,必须。文件字节数,同“单文件”;
  • —md5sum,可选。同“单文件”。

以上这些字段了解一即可,大部分都是制作种子的客户端(比如uTorrent)给编码好了的,咱们要修改的是非info字段的信息,比如announce、comment,再加一些无关紧要的其他字段等。info字段虽然不需要修改,却要获得它的内容,因为我们需要计算种子的info_hash,种子的info_hash就是info字段的内容字符串的sha1值再urlencode一下,客户端与tracker通信的时候就是发送这个info_hash,tracker根据这个info_hash去寻找peers。

B(Bencoding)编码

种子文件以及tracker服务器返回的信息都是以B编码的格式编码的,B编码有以下4种数据类型:

1、字符串。字符串的编码格式:<十进制ASCII编码的长度>:<串数据>,注意没有开始结束符。如4:span,表示字符串span。

2、整数。整数的编码格式:i<十进制ASCII编码的整数>e,以i开关,e结尾,中间的数值可以是负数,如i-3e是有效的,但不能把0放到数值前面,如i03e。另外,i0e是有效的。

3、列表。列表的编码格式:l<编码值>e,以l开头e结尾,<编码值>可以是任意已B编码的字符串、整数、字典或者其他列表。如l4:span3:doge,表示两个字符串span、dog的列表。

4、字典。字典的编码格式:d<编码串><编码元素>e,以d开关e结尾,编码值可以是任意已B编码的字符串、整数、列表或者其他字典。注意<编码串>必须是B编码的字符串,<编码元素>也即值可以是任意已B编码的字符串、整数、列表或者其他字典。编码串必须是字符串且出现顺序是按原始字符串排列好的。这里有点拗口,不好理解。比如:

  • d3:cow3:moo4:spam4:eggse 代表字典 { “cow” => “moo”, “spam” => “eggs” }
  • d4:spaml1:a1:bee代表字典 { “spam” => [ “a”, “b” ] }
  • d9:publisher3:bob17:publisher-webpage15:www.example.comee 代表字典{ “publisher” => “bob”, “publisher-webpage” => “www.example.com”}
  • de代表空字典{}

php实现B编码解析

随便给一个种子,我们需要将它编码好的内容字符串解析为各字段的数组,这样一来我们可以修改各个字段的内容,或者增加一些字段,更重要的是获得info字段的内容从而计算出种子的info_hash,最后再编码回去,重新生成种子。代码来自维基百科附上个人注释:

<?php

/*
d8:announce83:http://115.28.132.38/nexusphp/announce.php?passkey=41d83138bc772166b847976814a176ce
10:created by13:uTorrent/221013:creation datei1421729049e8:encoding5:UTF-84:infod6:lengthi982129e
4:name18:what_beautiful.rar12:piece lengthi65536e6:pieces300:.喁.B莝cIM"\-憈 {?Q繁窸G惃"掚?
:privatei1e6:source33:[115.28.132.38/nexusphp] NexusPHPee

以这个字典为例,粗看decode的解析过程
*/

class BEncode
{
	public static function decode($string)
	{
		static $pos = 0;
		if ($pos >= strlen($string))
		{
			return NULL;
		}
		switch ($string[$pos]) {
			case 'd'://正常的字典,第一个字符肯定是d
				$pos++;//位置移至1
				$result = array();
				while ($string[$pos] !== 'e')//下一位也不应该是e,如果是e就结束了字典,返回空数组
				{
					$key = self::decode($string);//递归调用自身继续查找整个内容字符串,这时pos为1,对应数据为8,应该到default
					$val = self::decode($string);//字典编码格式d<编码串><编码元素>e,上边得到了编码串,再一次获得编码元素,也就是值
					if ($key === NULL || $val === NULL)
					{
						break;//返回NULL,只会是当前位置大于整个字符串长度的时候
					}
					$result[$key] = $val;//将字段与字段的值存入结果数组
				}
				$result['isDict'] = TRUE;//标记这个结果是字典,在encode的时候需要,否则无法区分是字典还是列表
				$pos++;//当前位置再移一位
				return $result;
			case 'l':
				$pos++;
				$result = array();
				while ($string[$pos] !== 'e')
				{
					$val = self::decode($string);
					if ($val === NULL)
					{
						break;//列表跟字典差不多,只是列表只有一个编码值,不像字典有编码串和值。其实二者就基本是枚举数组与索引数组的区别
					}
					$result[] = $val;
				}
				$pos++;
				return $result;
			case 'i':
				$pos++;
				$offset = strpos($string, 'e', $pos) - $pos;//对于整数,我们是找它的结束符e,而不是像字符串那样子找分割符:
				$val = round((float)substr($string, $pos, $offset));//使用float型而不是int型,前者能存的更大。结果应该都是整数,四舍五入意义不大
				$pos += $offset + 1;
				return $val;
			default://不是d、l、i这些开头结束标记的都到这里来,那么肯定是字符串了,其编码格式<字符串长度>:<字符串值>
				$offset = strpos($string, ':', $pos) - $pos;
				//从当前位置(这里是1)开始找这一段字符串的分割符:在整个字符串中的位置,再减去当前位置,得出它们中间隔了多少位,它们中间就是字符串的长度值(8)
				$len = (int)substr($string, $pos, $offset);//截取长度值的字符串,转化为整数,得到字符串长度(8)
				$pos += $offset + 1;//移动当前位置到该字符串长度值的右边(当前位置变成到8:的右边a的位置)
				$str = substr($string, $pos, $len);//有了长度值,截取具体的字符串值(这里得到announce)
				$pos += $len;//再将当前位置移动到字符值的右边(这里到了83的8位置,明显下一次还是到default这里,重复这个过程)
				return (string)$str;//返回字符串值,这才是我们想要的。字符串长度不是我们需要的
		}
	}

	public static function encode($data)
	{
		if(is_array($data))
		{
			$result = 'l';
			if(isset($data['isDict']) && $data['isDict'])
			{
				$result = 'd';
				$isDict = TRUE;
				ksort($data, SORT_STRING);//是字典,需要对字典的字段按原始字符串排好序,否则有些客户端程序会阻塞。
			}
			foreach ($data as $key=>$value)
			{
				if(isset($isDict) && $isDict)//是字典
				{
					if($key === 'isDict')
					{
						continue;//跳过我们自己添加的isDict字段
					}
					$result .= strlen($key).':'.$key;//先连接字典的编码串,编码值可能是字符串、整数、列表或者字典
				}
				if(is_int($value) || is_float($value))
				{
					$result .= "i{$value}e";//是整数
				}
				elseif(is_string($value))
				{
					$result .= strlen($value).':'.$value;//是字符串
				}
				else
				{
					$result .= self::encode($value);//是列表或者字典,递归
				}
			}
			return $result.'e';//结束符
		}
		elseif(is_int($data) || is_float($data))
		{
			return "i{$data}e";//对单个整数编码
		}
		elseif(is_string($data))
		{
			return strlen($data).':'.$data;//对单个字符串编码
		}
		else
		{
			return NULL;
		}
	}

}
//我们使用uTorrent制作一个种子,看看如何decode与encode

header("Content-Type:text/html;charset=utf-8");
$file = 'e:\ted2-tlr1_h1080p.mov.torrent';//种子文件
$result = BEncode::decode(file_get_contents($file));
echo '<pre/>';
var_dump($result);//decode出来的数组
echo '<hr/>';
$result['announce'] .= '?passkey=41d83138bc772166b847976814a176ce';//添加个人passkey
$result['comment'] = 'come from xiaomlove.com';//添加备注信息

$outPut = BEncode::encode($result);
var_dump($outPut);//重新生成种子,要生成文件file_put_contents即可

echo '<hr/>';
$encodeInfo = BEncode::encode($result['info']);
$sha1Info = sha1($encodeInfo);
$clientInfoHash = urlencode(sha1($encodeInfo, TRUE));
echo '常规hash值:'.$sha1Info.'<br/>';
echo '客户端发送的hash值:'.$clientInfoHash.'<br/>';

//输出信息如下,中间大部分乱码省略,这是单个文件的情况。
/*
array(6) {
  ["announce"]=>
  string(42) "http://115.28.132.38/nexusphp/announce.php"
  ["created by"]=>
  string(13) "uTorrent/2040"
  ["creation date"]=>
  float(1422973809)
  ["encoding"]=>
  string(5) "UTF-8"
  ["info"]=>
  array(6) {
    ["length"]=>
    float(176947318)
    ["name"]=>
    string(20) "ted2-tlr1_h1080p.mov"
    ["piece length"]=>
    float(262144)
    ["pieces"]=>
    string(13520) "�8t��ߤM;Y�Zlʆt��!0Y�=�#���ֲ5�{cm�ɫ,������2G=
    float(1)
    ["isDict"]=>
    bool(true)
  }
  ["isDict"]=>
  bool(true)
}
—————————————————————————————————————————————————————————————————————————————————————————————————————————————————
string(13832) "d8:announce83:http://115.28.132.38/nexusphp/announce.php?passkey=41d83138bc772166b847976814a176ce7:comment23:come from xiaomlove.com10:created by13:uTorrent/204013:creation datei1422973809e8:encoding5:UTF-84:infod6:lengthi176947318e4:name20:ted2-tlr1_h1080p.mov12:piece lengthi262144e6:pieces13520:�8t��ߤM;Y�Zlʆt�

常规hash值:9d60a3ae1e4b1218a3409eb5590573cbf9002520
客户端发送的hash值:%9D%60%A3%AE%1EK%12%18%A3%40%9E%B5Y%05s%CB%F9%00%25+

//还有一个问题,数据库一般存常规hash值,客户端发来的却是urlencode过的值,怎么判断二者是否是否相等呢?

$a = '9d60a3ae1e4b1218a3409eb5590573cbf9002520';
$b = '%9D%60%A3%AE%1EK%12%18%A3%40%9E%B5Y%05s%CB%F9%00%25+';

$c = pack('H*', $a);//经查询结合个人测试,这样子可以判断
$d = urldecode($b);

var_dump($c === $d);//true
*/

解析种子完成了,接下来就是客户端与tracker服务器的交互,客户端发送的是普通的HTTP GET请求,tracker服务器返回的是B编码过的字符串,里面主要是peers的信息以及一些告知客户端何时再发请求等一些信息。这里面的内容等下一篇再分享吧。~.~

参考链接:
https://wiki.theory.org/BitTorrentSpecification
http://stackoverflow.com/questions/3272167/how-to-convert-torrent-info-hash-for-scrape

13 comments

  1. 不错不错,继续写,我之前也是到客户端和tracker交互部分了,期待~

  2. 您好,博主!我使用你写的类循环读取种子信息一直导致内存溢出,请问有更好的办法解决吗?

  3. 博主你好.写的很好!
    看了下http://115.28.132.38 ,你是想重新写一遍nexusphp吗?

    • 不能算重写,Nexus是Nexus,我的是我的,差别还是蛮大的。一些功能、界面参考自它而已。~.~

      • 我也有这种冲动,Nexus写的真是太丑了…膜拜楼主中..
        然后验证码好像有点问题…

  4. 字典 相当于是 php 数组里的索引和值 ,torrent文件就是以 索引=>值 的形式写成的.这样说更好理解一点

  5. 小喵爱你,我又来了..你有没有nexusphp 的文档?

  6. 是哇,我是某个小站的技术员…想着增些新功能啥

  7. I enjoy the article

Leave a Reply

Your email address will not be published. Required fields are marked *

*

Time limit is exhausted. Please reload CAPTCHA.