Fork me on GitHub

2015年5月

Windows下搭建PHP本地开发环境

一直以来,我都是使用新浪的SAE作为我本地的PHP开发环境。SAE使用起来非常方便,是个纯绿色的软件包,可以从这里下载,使用之前首先配置sae.conf文件中的DocumentRoot参数为网站根目录(也就是你PHP开发的目录),然后运行init.cmd脚本即可(Win7以上系统可能需要以管理员身份运行)。 我将SAE作为我的本地开发环境,官方的SAE只包含了PHP、Apache和Redis,而我的生产环境是PHP + Nginx + MySQL。这样不仅需要安装MySQL,而且不能和生产环境的Nginx保持一致,导致很多生产上的Nginx重写规则,在本地要再写一份Apache版的,虽然差异不大,但是依然感觉很麻烦。所以就想打造一个自己的PHP开发环境,像SAE一样绿色版,并且支持Apache和Nginx两个服务器。

一、搭建步骤

我们按PHP、Apache和Nginx的顺序来搭建PHP的本地开发环境,MySQL没有包含其中,以后有机会再做。这次的重点是学习Apache和Nginx的配置以及PHP是怎么和这两大Web服务器交互的。

1.1 PHP

首先,我们在这里下载PHP的Windows版本,我们直接下载编译好的Zip包即可。这里要特别注意的是,PHP for Windows有多种不同的版本,VC9和VC11,TS和NTS,x86和x64等。

  • VC9 vs VC11:代表使用哪个VS版本编译的,VC9表示Visual Studio 2008,VC11表示Visual Studio 2012
  • TS vs NTS:TS代表线程安全,也就是说支持多线程,PHP以模块形式加载到Web服务器中需要使用TS;NTS代表只支持单线程,在CLI/CGI/FastCGI等模式下建议使用NTS
  • x86 vs x64:使用32位还是64位,x64目前是实验版本
    综上所述,这里我们选择PHP 5.6 (5.6.9) VC11 x86 Thread Safe

1.2 Apache

Apache官网上并没有提供Windows上的可执行文件供下载,而是只提供了源码和编译安装步骤。我们这里省去编译可能会带来的问题和麻烦,直接使用第三方编译好的可执行文件。我们有很多不同的选择,下面是Apache官方提供的选择列表:

这里我们使用Apache Lounge提供的httpd-2.4.12-win32-VC11.zip,这是因为上面提到的PHP中一些SAPI就是使用Apache Lounge提供的二进制文件来编译的。注意选择和上面PHP一样的VC11和32位版本。

1.3 Nginx

比较流行的Nginx for Windows有两种不同的版本,一种是官方提供的使用native Win32 API的原生版本,另一种是利用Cygwin模拟编译出来的版本。这里我们选择原生版本

二、PHP的两种配置方式

将可执行文件都下下来之后,我们就需要对其进行配置了。首先我们以下图所示结构对刚下载的软件包进行组织:

structure

webserver目录用于存放Web服务器的二进制文件,譬如Apache和Nginx,之后可以添加lighttpd,IIS等;database目录用于存放数据库相关文件,譬如MySQL、Redis和Memcached等;interpreter目录用于存放脚本解析文件,如PHP、Perl、Python等。 PHP一般有两种配置方式:一种方式是以Apache的模块mod_php运行在Apache服务器中,这种方式最传统最著名,历史也最为悠久,在Nginx服务器没流行起来的时代,Apache统治了Web服务器的绝大部分,当时几乎都是以mod_php方式来配置的。这种配置方式虽然简单而且运行效率很高,但是这种配置不够灵活,导致Web服务器臃肿,也导致Apache服务器会占用更多的内存。于是出现了第二种配置方式CGI/FastCGI,CGI由于其效率低下已经基本上被FastCGI替代了。这种配置方式将Web服务器和PHP解析器解耦分离开来,PHP和Web服务器使用两个进程,甚至可以运行在两个不同服务器上,两者之间通过CGI/FastCGI协议进行通信。关于CGI/FastCGI/PHP-FPM的资料网上有很多,可以参考这里这里这里了解下。关于PHP的两种配置方式可以参考这里这里这里。 下面我们就以这两种方式分别来配置我们刚下载的Apache和Nginx。

2.1 mod_php

Apache支持以模块的方式来配置PHP,这个模块就是mod_php5.so,要注意的是mod_php是Linux下的名称,在Windows下的名称是php5apache2_4.dllphp5apache2_4.dll这个文件只包含在TS版的PHP打包中,如果你下载的是NTS版的可能找不到这个文件,因为Apache以模块形式载入PHP需要保证其是线程安全的。Nginx暂时还不支持这种配置方式。 参考PHP官方文档,我们在webserver\Apache-2.4\conf\httpd.conf文件中加入下面的代码来完成配置:

#
# mod_php
#
LoadModule php5_module "full-path-of-php5apache2_4.dll"
PHPIniDir "full-path-of-php.ini-dir"
AddType application/x-httpd-php .php

确保配置文件中的ServerRootDocumentRoot路径正确,然后运行webserver\Apache-2.4\bin\httpd.exe即可。可以在htdocs目录下新建一个PHP文件,使用phpinfo()方法来确保PHP和Apache已经正确运行。

2.2 CGI / FastCGI / PHP-FPM

在Windows环境下通过CGI方式解析PHP脚本一般使用的是PHP自带的php-cgi.exe程序。该程序运行方式如下:

php-cgi.exe -b 127.0.0.1:9000 -c path/to/php.ini

一旦按上面的方式运行php-cgi.exe程序之后就可以通过9000端口使用CGI协议来和它进行通信解析PHP脚本了。然后我们参考这里,在Nginx的配置文件中配上下面的代码(Nginx的配置文件位于webservernginx-1.9.0confnginx.conf):

#
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
location ~ \.php$ {
    root           html;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
}

配置好了以后,运行webserver\nginx-1.9.0\nginx.exe即可。在html目录下新建一个PHP文件,使用phpinfo()方法确保PHP和Nginx正确运行。可以看到mod_php方式配置简单,而且只要运行Web服务器即可,FastCGI方式不仅要运行Web服务器,还要运行FastCGI解析程序。 当然不仅仅是Nginx支持FastCGI这种配置方式,几乎所有其他的Web服务器都支持这种配置方式,譬如:Apache、IIS和lighttpd等。如果你想在Apache上使用这种配置,可以使用mod_fcgid扩展模块,在httpd.conf配置文件中添加如下代码:

#
# fast_cgi
#
LoadModule fcgid_module modules/mod_fcgid.so
FcgidInitialEnv PHPRC        "c:/php" 
FcgidWrapper "c:/php/php-cgi.exe" .php  
AddHandler fcgid-script .php

另外要提出的一点是,这里不管是mod_php方式还是FastCGI方式,我们都使用的是PHP的线程安全(TS)版本。而一般推荐做法是mod_php使用多线程(TS)版本,FastCGI使用单线程(NTS)版本,使用NTS版本不仅可以提高性能,也是为了和PHP的扩展(有很多PHP扩展是非线程安全的)保持兼容。

2.3 总结

为了方便,我写了两个批处理程序来启动Apache和Nginx。完整的版本可以看这里start-apache.bat文件:

@echo off
set APACHEROOT="path/to/webserver/Apache-2.4"
set PHPROOT="path/to/interpreter/php-5.6.9"
echo Starting Apache...
copy "%APACHEROOT:~1,-1%\conf\httpd.conf.bak" "%APACHEROOT:~1,-1%\conf\httpd.conf" /Y
utils\fnr.exe --cl --dir "%APACHEROOT:~1,-1%\conf" --fileMask "httpd.conf" --find "

start-nginx.bat文件:

@echo off
set NGINXROOT="path/to/webserver/nginx-1.9.0"
set PHPROOT="path/to/interpreter/php-5.6.9"
echo Starting PHP FastCGI...
utils\RunHiddenConsole "%PHPROOT:~1,-1%\php-cgi.exe" -b 9000 -c "%PHPROOT:~1,-1%\php.ini-development"
echo Starting Nginx...
utils\RunHiddenConsole "%NGINXROOT:~1,-1%\nginx.exe" -p "%NGINXROOT:~1,-1%"
pause

三、PHP SAPI

上面介绍了PHP的两种不同的配置方式,这两种配置都可以使PHP工作,实现这一点其实要归功于PHP的SAPI。SAPI(Server Application Programming Interface)是服务器应用编程接口的缩写,PHP通过SAPI提供了一组接口供应用和PHP内核之间进行数据交互。PHP提供很多种形式的接口,包括apache、apache2filter、apache2handler、cgi 、cgi-fcgi、cli、cli-server、embed、isapi、litespeed等等。但是常用的只有5种形式,CLI/CGI(单进程)、Multiprocess(多进程,PHP可以编译成Apache下的prefork MPM模式和APXS模块)、Multithreaded(多线程,Apache2的Worker MPM模块)、FastCGIEmbedded(内嵌)。可以使用PHP的php_sapi_name()函数获取当前的SAPI接口类型。参考这里这里了解更多。另外在简明现代魔法上有一个系列的文章对SAPI进行了详细的介绍:

参考

  1. PHP For Windows
  2. Using Apache HTTP Server on Microsoft Windows
  3. Apache Lounge
  4. nginx for Windows
  5. 什么是CGI、FastCGI、PHP-CGI、PHP-FPM、Spawn-FCGI?
  6. 概念了解:CGI,FastCGI,PHP-CGI与PHP-FPM
  7. 搞不清FastCgi与PHP-fpm之间是个什么样的关系
  8. mod_php vs FastCGI vs PHP-FPM
  9. [[好文]mod_php和mod_fastcgi和php-fpm的介绍,对比,和性能数据](http://wenku.baidu.com/view/887de969561252d380eb6e92.html)
  10. mod_php和mod_fastcgi(待整理)
  11. Microsoft Windows 下的 Apache 2.x
  12. php-fcgi on Windows
  13. windows下配置nginx+php环境
  14. Thread Safe PHP with FastCGI on IIS 7 or not?
  15. 【问底】王帅:深入PHP内核(二)——SAPI探究
  16. 第二章 » 第二节 SAPI概述 | TIPI: 深入理解PHP内核
扫描二维码,在手机上阅读!

使用WireShark进行磁力链接协议分析

上一篇博客中,我介绍了两种方法来将磁力链接转换为一个种子文件:一种方法是直接从种子站点下载,另一种方法通过libtorrent库实现。第二种方法通过libtorrent库的强大功能,只有寥寥几行代码就实现了种子文件的转换。正如前文所说,简单的背后往往蕴含着复杂的逻辑设计,这篇博客通过对WireShark截获下的所有网络包的分析,深入学习BitTorrent协议,包括:DHT网络,KRPC协议,种子文件格式,B编码,BitTorrent扩展,uTP协议等。

一、加入DHT网络

磁力链接的发明使得P2P客户端直接从DHT网络中寻找资源,而不是传统的依赖于Tracker服务器,这样就避免了Tracker服务器的单点故障问题,所以从DHT中获取的种子有时候也叫做Trackerless torrent。DHT网络是一种分布式的去中心化网络,每个加入DHT网络的节点都要负责存储这个网络中的资源和其他成员的联系信息。小虾在他的一篇文章中做了一个非常形象的比喻:

举个形象点的例子,把DHT网络比作一个朋友圈子,你想进入这个圈子必须要有一个人带领你进去,通常会有一些特定人负责介绍你进入这个圈子。当你被A带进这个朋友圈,此刻你就只认识A而已。但是你的目的是想找奥巴马总统,所以你会问A要奥巴马的联系方式,但是A没有奥巴马的联系方式,他会介绍一个美国朋友B给你认识。于是你去问B要奥巴马的联系方式,B其实也没有奥巴马的联系方式,但是B认识一个州长C。于是你又得到了C的联系方式,C把奥巴马的联系方式告诉你之后,你就可以写信或者致电给奥巴马了。

可以看到如果一个新节点要加入到DHT网络中,它必须要先认识一个人带你进去。这样的人我们把他叫做bootstrap node,常见的bootstrap node有:router.bittorrent.comrouter.utorrent.comrouter.bitcomet.comdht.transmissionbt.com等等。所以首先我们通过DNS将其转换为IP:

dns

这里以dht.transmissionbt.com为例进行说明,从DNS查询结果得到了两个IP地址:212.129.33.50 和 91.121.59.153。

二、了解KRPC协议,获取Peers

DHT协议早在2005年就已经成为了官方BitTorrent协议的一部份,官方文档参考BEP-005,这里有一份中文翻译。DHT建立在UDP之上,想要获取需要的Peers信息,首先要了解下KademliaKRPC协议。 关于路由表和Kademlia的介绍,可以参照官方文档。我们这里的重点在于如何根据磁力链接获取拥有该磁力链接对应的种子文件信息的Peers,所以只需要了解KRPC协议。 KRPC协议是由B编码组成的一个简单的RPC结构,有4种请求:pingfind_nodeget_peersannounce_peer

  • ping: 检测节点是否可达,请求包含一个参数id,代表该节点的nodeID。对应的回复也应该包含回复者的nodeID。
  • find_node: 该请求包含两个参数idtargetid为该节点的nodeID,target为要查询的nodeID。回复中应该包含被请求节点的路由表中距离target最接近的8个nodeID。
  • get_peers: 该请求包含两个参数idinfohashid为该节点的nodeID,infohash为种子文件的SHA1哈希值,也就是磁力链接的btih值。如果被请求的节点有对应info_hash的peers,他将返回一个关键字values,这是一个列表类型的字符串。每一个字符串包含了CompactIP-address/portinfo格式的peers信息。如果被请求的节点没有这个infohash的peers,那么他将返回关键字nodes,这个关键字包含了被请求节点的路由表中离info_hash最近的K个nodes,使用Compactnodeinfo格式回复。
  • announce_peer: 如果节点正在下载torrent文件,则需要通知其他人你正在哪个端口进行下载,这样就可以分享给其他人,让其他人连接你进行下载。

在我们的实验中,因为并没有真正的下载torrent文件,所以并不需要announce_peer请求。ping非常简单,我们忽略之,只看find_nodeget_peers这两个请求。形象点说find_node请求就好比我们交朋友,如果运气好的话每次发送find_node都可以得到8个新node,再对这8个新node发送find_node,就可以得到8*8=64个新node(当然这是理想情况,我们没考虑丢包,节点不在线,返回节点数没8个这些情况)。一般情况下find_nodetarget参数就是我们自己的nodeID,所以得到的8个node都是离自己最近的(Kademlia距离,并非物理距离)。这样我们很快就得到了非常多的node信息,路由表就是这样建立起来的。 但是实际上find_node请求在这里对我们用处并不大,要想根据磁力链接获取种子信息,最重要的请求还是get_peers,它可以根据infohash来找到拥有该种子信息的peers。 下图是find_node请求,idtarget参数都是当前节点的nodeID:909f9cbdedf4f7e29e820e3fd5e00a2965450b8a

find_node

下图是对find_node请求的回复。回复报文中可以看到返回了8个node,每个node都是紧凑的nodeID IP/Port格式:

find_node_resp

下图是get_peers请求,infohash = 1619ecc9373c3639f4ee3e261638f29b33a6cbd6,正是磁力链接magnet:?xt=urn:btih:1619ecc9373c3639f4ee3e261638f29b33a6cbd6&dn;=ubuntu-14.10-desktop-i386.iso中的btih值。

get_peers

下图是对get_peers请求的回复。回复报文中包含了8个node,以及100个peer。可见包含该种子文件的peer非常多。

get_peers_resp

三、Bencoding编码和种子文件格式

Bencoding编码(简称B编码)在BitTorrent协议中非常常见,从上面的四张图中就可以看到KRPC协议采用了B编码来发送消息,不仅如此,在后面介绍的PEX和BitTorrent扩展中我们也会看到B编码的影子。而且更重要的是,BT种子文件本身就是一个B编码的字典,所以要想学习BitTorrent,首先得学习B编码。B编码是一种非常简洁的数据格式,共支持4种不同的类型:字节串、整数、列表和字典。

  • string: 格式为 <字符串长度>:<字符串>。如 hell: 4:hell
  • integer: 格式为 i<整数>e。如 i1999e 表示数字1999
  • list: 格式为 l[数据1][数据2][数据3][…]e。如 l5:hello5:worldi101ee 表示列表[hello, world, 101]
  • dictionary: 格式为 d[key1][value1][key2][value2][…]e,其中 key 必须是 string 而且按照字母顺序排序。如 d2:aai100e2:bb2:bb2:cci200ee 表示字典 {aa:100, bb:bb, cc:200}

BT种子文件整个是一个dictionary格式,比较重要的key有announce( tracker 服务器的地址)、announce-list(可选的 tracker 服务器地址)、creation date(文件创建时间)、created by(文件创建者)、info(该bt种子文件的文件信息)等。其中info对应的value根据种子包含的是单文件还是多文件有所区别,其中piece length(每一数据块的长度)和pieces(所有数据块的 SHA1 校验值)是公共部分,如果是单文件的话,则包含name(文件名称)和length(文件的长度)两个key,如果是多文件的话,则包含name(文件夹名称)和files(文件列表,每个文件列表下面是包括每一个文件的信息,文件信息是个字典)两个key。 关于Bencoding和种子文件格式更详细的信息可以参考这里这里这里,另外这里有一份BitTorrent的非官方规范,其中也有关于Bencoding的介绍。 我们这里以ubuntu-14.10-desktop-i386.iso.torrent文件为例进行介绍,使用WinHex打开该文件如下图所示:

torrent

图的右边我将其转换成容易阅读的格式,可以看到上面介绍的announceannounce-listinfo等字段。其中info字段对于一个种子文件来说最为重要,根据磁力链接从DHT网络中下载种子文件也是下载info字段的内容,其他内容包括announceannounce-listcreated by等在DHT网络中并没有保留。磁力链接的infohash也是根据info字段来计算的,info字段的pieces为每个数据块的校验值,其作用是验证下载下来的文件是否正确,如果下载下来的文件块计算出来的SHA1值和pieces中的SHA1校验值不一致,该数据块要重新下载。 所以,我们可以看出根据磁力链接下载文件是分成两个步骤的:先根据infohash下载种子文件的info字段,种子文件并不是必须的,但是info字段却必不可少;然后根据infohash下载源文件,将下载的每一个数据块和info中的对应的SHA1校验码进行比较,不一致重新下载该数据块。我们这里研究的其实就是第一步,要注意的是info字段的下载也是分块的,下面将进行介绍,下载完成后使用infohash进行校验。 需要注意的是,一般的种子文件会包含announce,也就是tracker服务器的地址,如果没有tracker服务器,文件中可能会包含nodesnodes是存有种子信息的peer节点,这样的种子文件就是trackerless torrent。如果有nodes客户端直接从nodes获取种子信息,而从DHT网络中下载下来的种子文件既没有annouce也没有nodes,客户端只能通过info字段计算出hashinfo,再从bootstrap node节点开始在DHT网络中寻找种子信息。

四、uTP协议

在介绍info字段的下载之前,我们还要了解下uTP协议,它是一个基于UDP的开放的BT点对点文件共享协议。在uTP协议出现之前,BT下载会占用网络中大量的链接,直接导致其它网络应用服务质量下载和网络的拥堵,因此有很多ISP都开始限制BT的下载。uTP减轻了网络延迟并解决了传统的基于TCP的BT协议所遇到的拥塞控制问题,提供可靠的有序的传送。 一个有效的uTP数据包包含下面格式的报头:

uTP-header

其中,我们最为关心的是typeconnection idseq_nrack_nr这几个值。type字段表示包类型,uTP的包类型有下面5种:

  • ST_DATA = 0: 最重要的数据包,uTP就是使用该类型的包传送数据
  • ST_FIN = 1: 关闭连接,这是uTP连接的最后一个包,类似于TCP中的FIN
  • ST_STATE = 2: 简单的应答包,表明已从对方收到了数据包,该包不包含任何数据,seq_nr值不变
  • ST_RESET = 3: 终止连接,类似于TCP中的RST
  • ST_SYN = 4: 初始化连接,类似于TCP中的SYN,这是uTP连接的第一个包

关于uTP协议的内容参考官方文档BEP-029uTP的一个很重要的特点是使用connection id来标识一次连接,而不是每个包算一次连接。所以在分析ST_DATA时,需要注意找所有connection id相同的数据包,然后按seq_nr排序,seq_nr应该是依次递增的(注意ST_STATE包不会增加seq_nr值),如果发现两个ST_DATAseq_nr值相同则说明后面那个报文是重复报文需要忽略掉,如果发现两个ST_DATAseq_nr值不是连续的,中间差了一个或多个,则可能是由于网络原因发生了丢包现象,数据包将不可用。 下面是一个简单的ST_SYN报文,从192.168.18.33发送到112.208.162.161,seq_nr = 31445,ack_nr = 0, connection id = 14487

uTP-syn

112.208.162.161收到ST_SYN报文后,会向192.168.18.33发送一个ST_STATE报文,表示已收到。如果没有回复,则连接不能建立。如下图所示,seq_nr = 9690,ack_nr = 31445,connection id = 14487

uTP-state

uTP连接建立之后,就开始传送需要的数据了。peer和peer之间传送数据也是遵循着一定的规范,这就是下面要讲的Peer Wire协议。

五、Peer Wire协议

Peer Wire协议是Peer之间的通信协议,通常由一个握手消息开始。握手消息的格式是这样的:<pstrlen><pstr><reserved><info_hash><peer_id> 下面是一个握手报文的示例:

peer-wire-handshake

在BitTorrent协议的v1.0版本, pstrlen = 19, pstr = "BitTorrent protocol",info_hash是上文中提到的磁力链接中的btihpeer_id每个客户端都不一样,但是有着一定的规则,根据前面几个字符可以推断出客户端的类型,譬如这里的peer_id-LT开头可以大致推断出客户端为libtorrent这里有一份非常全面的列表,这里也有一个列表。 收到握手消息后,对方也会回复一个握手消息,并且开始协商一些基本的信息。如下图是握手报文的回复:

peer-wire-handshake-resp

从上图中可以看到,对方除了回复了一个握手消息外,在握手消息的后面还附带了一个扩展消息,一个bitfield消息和多个have消息。这些都是Peer Wire协议基本的通信消息,拥有统一的格式:<length prefix><message ID><payload>,可以从message ID看出是哪种类型的消息。Peer Wire消息有:keep-alive、choke(0)、unchoke(1)、interested(2)、not interested(3)、have(4)、bitfield(5)、request(6)、piece(7)、cancel(8)、port(9),另外还有下面要介绍的扩展协议extend(20)。 关于Peer Wire协议的详细内容,请参考BitTorrent的规范,这里是中文翻译这里也有一点资料。 在下载文件时,这些消息扮演着非常重要的角色,而在这里,我们只关注message ID为20的extend扩展协议。

六、BitTorrent协议扩展与ut_metadata和ut_pex

经过了上面层层的铺垫,终于到了最后一步下载我们的种子文件了。获取种子文件的metadata使用的是BitTorrent的协议扩展,BEP-010BEP-009是关于这个的权威规范,这里有一份BEP-009的中文翻译。 根据BEP-010我们知道,扩展消息一般在Peer Wire握手之后立即发出,是一个B编码的字典,其中有一个key很重要,那便是mm也是一个字典,表示客户端支持的所有扩展以及每个扩展的编号。我们将上图中握手消息后的扩展消息写成下面的格式:

{
    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
}

其中ut_pex表示该客户端支持PEX(Peer Exchange)ut_metadata表示支持BEP-009(也就是交换种子文件的metadata)。PEX在这里派不上用场,我们主要关心metadata的交换。接着上面的握手消息,我们在完成双方握手之后,并且得到了对方支持的扩展信息。接着我们发出下面的请求:

metadata_request

该请求也通知对方我们支持的扩展情况,然后后面接着一个扩展消息。如何识别一个扩展消息是什么消息呢?我们从上面的m字典可以看到可能会有多种不同的扩展消息,那么我们这个消息是哪个扩展呢?答案是message ID后面那个数字,这个数字对应着m字典中的编号。譬如我们这里的消息是:00 00 00 1b 14 02 ... 00 00 00 1b 表示消息长度为 0x1b (27 bytes) 14 表示是 扩展消息(0x14 = 20) 02 对应上面m字典中的 ut_metadata 所以我们这个消息是ut_metadata消息,下面我们再来研究下BEP-009BEP-009定义了三种不同的消息类型:requestdatareject。我们这里的图显示的是[msg_type: 0, piece: 2]正是request消息,意思是向对象请求第二个piece的数据。而piece又是什么意思呢?根据BEP-009我们知道,种子文件的metadata(也就是info部分)会按16KB分成若干块,除最后一块每一块的大小都是16KB,每一块从0开始按顺序进行编号。所以这个请求的意思就是向对象请求第三块的metadata。我们再来看一下torrent文件,如下图:

torrent-info

从图中形象的表示可以看到torrent文件整个info的长度为45377,这个值正是上面握手报文后的扩展消息中的metadata_size的值。在发送request消息之后,接下来对方应该回复data消息(如果对方有数据)或reject消息(如果对方没有数据)。下图是针对上面的request消息的回复:

metadata_resp

msg_type为1表示是回复就是我所需要的数据,但是注意这里的数据并没完,由于uTP协议的缘故,我们可以根据connection id找到这个连接后续的所有数据。 这里其实一共收到了三个消息,我们分别来看一下:

00 00 00 03 09 83 c5 --> message ID为9,port消息,表示端口号为0x83c5 = 33733
00 00 00 03 14 03 01 --> message ID为20(0x14),extend消息,编号03upload_only,表示设置upload_only = 1
00 00 31 70 14 02 xx --> message ID为20(0x14),extend消息,编号02ut_metadata,后面的xx表示[msg_type: 1, piece: 2, total_size: 45377]和相应块的metadata数据

看第三个消息可以知道消息长度为0x3170,这个长度包括了[msg_type...]这一串字符串的长度,共0x2f个字节,我们将其减去就得到了piece2的长度:0x3170 - 0x2f = 0x3141 我们上面说过每个块的大小应该是16KB,也就是0x4000,这里的大小为0x3141,只可能是最后一块。我们稍微计算验证下,将整个info的长度45377(0xb141)按16KB分块:

piece 0: 0x0001 ~ 0x4000 长度0x4000
piece 1: 0x4001 ~ 0x8000 长度0x4000
piece 2: 0x8001 ~ 0xb141 长度0x3141

可以看到piece2正是最后一块,大小为0x3141。至此我们得到了第二块的metadata,然后通过request消息获取piece0和piece1获取第一和第二块的metadata,将三块的消息合并成torrent文件info字段,然后再加上create datecreate bycomment等信息,种子文件就算完成下载了。

七、校验info_hash

我们将从DHT网络中下载的种子文件和原始的种子文件进行比较,可以看到annouceannouce-list字段都丢掉了,create date发生了变化,info字段不变:

torrent-comp

另外,我们也可以使用这篇文章中介绍的方法将种子文件转换为磁力链接,检查infohash的值是否一致。从下面的代码也可以看出infohash的计算只和info字段有关。

import bencode, hashlib, base64, urllib

torrent = open('ubuntu.torrent', 'rb').read()
metadata = bencode.bdecode(torrent)
hashcontents = bencode.bencode(metadata)

参考

  1. 写了个磁力搜索的网页 - 收录最近热门分享的资源
  2. BEP-005 DHT Protocol
  3. 【P2P网络】BitTorrent的DHT协议(译自官方版本)
  4. BitTorrentDraftDHTProtocol
  5. torrent文件分析
  6. bttorrent 种子文件结构解析
  7. BEncode编码方式以及torrent文件的一些内容
  8. Bittorrent Protocol Specification v1.0
  9. BitTorrent协议规范
  10. BEP-020 Peer ID Conventions
  11. BEP-029 uTorrent transport protocol
  12. peer之间的通信协议
  13. BEP-010 Extension Protocol
  14. extension protocol for bittorrent
  15. BEP-009 Extension for Peers to Send Metadata Files
  16. 【P2P网络】Extension for Peers to Send Metadata Files翻译稿
  17. Python将BT种子文件转换为磁力链的两种方法
  18. 常见P2P协议之BitTorrent 分析
  19. BT(带中心Tracker)通信协议的分析
  20. BT协议具体分析
  21. BitTorrent 协议分析与实现
  22. How PEX protocol (Magnetic links) finds it first IP?
  23. BT协议库libtorrent的种子文件解析方法探究
扫描二维码,在手机上阅读!

磁力链接是如何实现下载的?

一直很好奇磁力链接是如何工作的,只通过简单的一串字符就能下载任何文件,这确实是很神奇的一件事。而简单的背后,往往蕴含着非常复杂的逻辑和巧妙的设计,磁力链接正是如此。通过一段时间的学习,对磁力链接及相关的概念有了一定的了解,Magnet、Torrent、P2P、DHT、KAD等等等等,在这里做个小结。

一、磁力链接与种子文件

磁力链接并不是一个新概念,早在2002年,相关的标准草稿就已经制定了。但直到2012年海盗湾为规避版权问题删除了站点上的所有Torrent文件停止了下载服务,并将之前所有的种子转换为磁力链接之后,磁力链接技术才真正的飞速发展起来。说起磁力链接,不能不提BT;而说起BT,则不能不提P2P。在很早的网络时代,下载都是简单的集中式客户端/服务器模式,一个或多个服务器支撑成千上万的客户端连接下载,不仅带宽遇到了瓶颈,而且太容易出现单点故障。这时P2P被提出来解决这个问题,P2P也不是一经提出就完美无缺的,在经过了Napster到Gnutella到BitTorrent三代的P2P技术的发展才渐进成熟。(这里有对P2P的科普) 维基百科上对BT做了详细的说明:

根据BitTorrent协议,文件发布者会根据要发布的文件生成提供一个.torrent文件,即种子文件,也简称为“种子”。 种子文件本质上是文本文件,包含Tracker信息和文件信息两部分。Tracker信息主要是BT下载中需要用到的Tracker服务器的地址和针对Tracker服务器的设置,文件信息是根据对目标文件的计算生成的,计算结果根据BitTorrent协议内的Bencode规则进行编码。它的主要原理是需要把提供下载的文件虚拟分成大小相等的块,块大小必须为2k的整数次方(由于是虚拟分块,硬盘上并不产生各个块文件),并把每个块的索引信息和Hash验证码写入种子文件中;所以,种子文件就是被下载文件的“索引”。 下载者要下载文件内容,需要先得到相应的种子文件,然后使用BT客户端软件进行下载。 下载时,BT客户端首先解析种子文件得到Tracker地址,然后连接Tracker服务器。Tracker服务器回应下载者的请求,提供下载者其他下载者(包括发布者)的IP。下载者再连接其他下载者,根据种子文件,两者分别告知对方自己已经有的块,然后交换对方所没有的数据。此时不需要其他服务器参与,分散了单个线路上的数据流量,因此减轻了服务器负担。 下载者每得到一个块,需要算出下载块的Hash验证码与种子文件中的对比,如果一样则说明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容准确性的问题。 一般的HTTP/FTP下载,发布文件仅在某个或某几个服务器,下载的人太多,服务器的带宽很易不胜负荷,变得很慢。而BitTorrent协议下载的特点是,下载的人越多,提供的带宽也越多,下载速度就越快。同时,拥有完整文件的用户也会越来越多,使文件的“寿命”不断延长。

综上所述,可以看出Tracker服务器在BT网络中充当着非常重要的作用,和传统的客户端/服务器模式一样,Tracker服务器同样会存在单点故障问题。所以在BT技术的基础上,后来又衍生出DHT网络和磁力链接技术,DHT全称为分布式哈希表(Distributed Hash Table),是一种分布式存储方法。DHT网络是Tracker-less的,不依赖于其他的Tracker服务器。在这种情况下,每个客户端负责一个小范围的路由,并负责存储一小部分数据,从而实现整个DHT网络的寻址和存储。使用支持该技术的BT下载软件,用户无需连上Tracker就可以下载,因为软件会在DHT网络中寻找下载同一文件的其他用户并与之通讯,开始下载任务。 在网络中定位资源最简单的方法是URL(统一资源定位符),它是通过资源的位置来进行定位。而在DHT网络中,则是使用URN(统一资源名称)来进行定位,磁力链接就是基于文件内容的散列函数值来链接到特定文件,生成一个唯一的文件识别符,从而在DHT网络中定位并下载文件。 一个最简单的磁力链接格式如下:
magnet:?xt=urn:btih:51df6808c739174c8f264701ba94460c5238d6ce
其中urn为统一资源名称,btih是BitTorrent Info Hash的缩写,是BitTorrent使用的Hash函数。除了btih还可以是其他类型的Hash函数,但不如btih用的多。这一串长度为40的字符串正是文件内容的Hash,BT下载工具就根据这个Hash在DHT网络中定位下载文件。

二、libtorrent实现磁力链接转BT种子

处理磁力链接最简单的库莫过于Python中的libtorrent,它封装了几乎所有的BitTorrent相关的内容,以至于你可以在10分钟内使用libtorrent写出一个自己的BT下载工具。libtorrent中的add_magnet_uri方法是用来解析磁力链接的,该方法非常简单是完全透明的,不用关心磁力链接或DHT协议之类的细节,方法返回一个句柄,在磁力链接未成功解析之前,这个句柄的has_metadata方法会一直返回False,一旦成功解析到磁力链接,根据这个句柄调用get_torrent_info方法即可获取种子信息了。示例代码如下:

handle = lt.add_magnet_uri(sess, link, params)

# waiting for metadata
while (not handle.has_metadata()):
    time.sleep(5)    

# create a torrent
torinfo = handle.get_torrent_info()
torfile = lt.create_torrent(torinfo)

注意,在使用libtorrent处理磁力链接之前,首先需要记得调用add_dht_routerstart_dht启用DHT,这就是所谓的bootstrap node。如果不启用DHT,可能要很久都不能解析磁链。如下:

sess = lt.session()
sess.add_dht_router('router.bittorrent.com', 6881)
sess.add_dht_router('router.utorrent.com', 6881)
sess.add_dht_router('router.bitcomet.com', 6881)
sess.add_dht_router('dht.transmissionbt.com', 6881)
sess.start_dht();

常见的bootstrap nodebittorrent、utorrent、bitcomet、transmissionbt等,端口都是6881。完整的代码在这里

三、BT种子转磁力链接

上面讲了这么多,大家会发现磁力链接的下载原理其实就是先根据磁力链接获取种子文件,然后再根据种子文件进一步下载。这是因为种子文件才有分片信息,文件大小,文件名等必要的信息,所以种子是必不可少的。那么既然有了种子文件,我们为什么还需要磁力链接呢?这是因为现在对BT的封锁太严重,传播种子并不是那么简单的事了,动不动就被和谐了,所以磁链的作用就是便于传输,因为磁链就是一个小小的文本,而种子确是一个文件。所以总结一下他们的优缺点:
A. 种子:稳定性高,信息多,不便于传播扩散
B. 磁力链接:仍需要先获得种子,不稳定,可能在某时刻不能获取,但是很便于扩散。

使用libtorrent将种子转换为磁链简单的不能再简单了。另外也可以使用bencode来进行解析,这里有代码示例

def torrent2magnet(torrent_file):
    info = lt.torrent_info(torrent_file)
    link = "magnet:?xt=urn:btih:%s&dn=%s" % (info.info_hash(), info.name())
    return link

四、种子站点的缓存

使用libtorrent将磁力链接转换成种子文件,实际上是效率很低下的工作,运行上面的Python脚本,可以发现转换一个磁力链接至少也要花费5到10秒的时间,有的磁力链接更要将近一分钟才能转换。那么如何提高转换速度呢?其实,大多数的下载工具在下载磁力链接时会将种子文件保留在服务器上,所以我们在转换之前先判断下种子服务器上是否已经存在该种子,如果存在则直接下载,这将比通过DHT网络转换磁力链接快的多。常见的种子服务器有:(网上很多种子服务器都已经被墙,或者已关闭,或者提供了验证码不能直接下载了)

  1. http://bt.box.n0808.com
    这是迅雷的种子库,种子文件地址格式为:http://bt.box.n0808.com/HASH值最前面两位/HASH值最后面两位/HASH值.torrent
  2. http://magnet2torrent.com/
    实际上调的是torcache的接口,格式为:https://torcache.net/torrent/HASH值.torrent
  3. http://torrent-cache.bitcomet.org:36869/
    这是BitComet的服务器,格式为:http://torrent-cache.bitcomet.org:36869/get_torrent?info_hash=HASH值&key;=KEY值。关于KEY值的计算可以参考这篇文章
  4. http://magnet.vuze.com/
    这是Vuze的种子库,格式为:http://magnet.vuze.com/magnetLookup?hash=KEY值,其中KEY值是Hash的base32编码。

参考

  1. P2P技术科普
  2. 磁力链接 - 维基百科
  3. BitTorrent (协议) - 维基百科
  4. 根据磁力链导出BT种子
  5. 通过 BT 种子 Hash 值从 BitComet 服务器上下载种子文件
  6. 根据磁力链获得BT种子
  7. 磁力链接转换为种子文件 magnet to torrent
  8. libtorrent中从magnet link生成torrent
  9. Converting a Magnet Link into a Torrent
  10. Python将BT种子文件转换为磁力链的两种方法
扫描二维码,在手机上阅读!