BitTorrent/download.py中的Multitorrent对象能够开始实际的下载任务。要开始下载
,需要创建一个Multitorrent对象,然后反复得调用start_torrent方法开始一个新的下载
,调用这个方法时必须已经准备好相应的下载任务的信息作为参数,包括已经处理好的元信
息(经过BitTorrent/ConvertedMetainfo.py模块进行处理),配置信息,一个实现了
FeedBack接口的类(这样种子在下载的时候状态发生变化可以及时反映出来,至于是反应在
文字信息上还是在图形界面上那就看这个FeedBack接口的实现),以及保存种子文件内容的
本地目录。这个函数会返回一个_SingleTorrent对象,代表一个单一的种子文件下载任务,
这个对象前面的一条短线代表它是私有对象,不能单独创建,只能通过start_torrent来进
行创建。它除了使用FeedBack接口来反应状态变化以外,还可以允许界面模块主动地调用
_SingleTorrent.get_status来获取关于该种子文件下载状况的一些统计信息。当然不要忘
记调用multitorrent.rawserver.listen_forever()开始这一切的调度,在创建
multitorrent类时,它会在内部创建一个rawserver。
前面几次都是直接上来就通过流程来分析程序,但是这次不一样,因为客户端的程序结
构比较复杂,而且各种对象之间互相关联,必须先对这些对象的功能有一个大致的了解才好
继续分析,因此这一次将简要得介绍一下客户端的下载程序中牵涉到的主要对象。
Multitorrent:下载任务管理的主对象,定义于BitTorrent/download.py中,内部维护
了一个RawServer,且可以创建_SingleTorrent(与其定义于同一模块中。)它内部还维护了
其它对象。
SingleportListener:管理网络连接,是Multitorrent中的RawServer的网络连接处理
对象,定义于BitTorrent/Encoder.py中。
FilePool:管理文件池,定义于BitTorrent/Storage.py中,它可以保证同一时刻打开
硬盘上的文件数量在一个限定的值以内。
RateLimiter:速度限制类。定义于BitTorrent/RateLimiter.py中,它可以控制全部种
子文件下载时上传的速度。
Storage和StorageWrapper:储存管理类。定义于BitTorrent/Storage.py和
StorageWrapper.py中,它们的作用是对程序的其它部分屏蔽掉种子文件中第几块对应于实
际硬盘上的哪个文件的偏移量多少。即它对程序的其它部分提供诸如以下的这些服务,确定
现在本地有第几块,没有第几块;应其它部分要求读出第几块(其它程序就不用管第几块实
际上是硬盘上的那个文件),然后它们好发送到网络上;其它部分从网络上得到一块新的数
据,叫它存储到硬盘上。Storage和StorageWrapper都和_SingleTorrent一一对应。
Choker:阻塞管理类。定义于BitTorrent/Choker.py中,它的作用是确定上传的阻塞策
略,即当前的连接中,阻塞哪些连接。与_SingleTorrent一一对应。
Measure:速度测量器。定义于BitTorrent/CurrentRateMeasure.py中,它的作用是计
算速率。在_SingleTorrent中定义了若干Measure对象来计算各种速率(如上传,下载等)。
RateMeasure:也是速度测量器。定义于BitTorrent/RateMeasure.py中,和Measure不
一样的地方在于它可以在初始化的时候传入一个表示还剩多少字节的参数进去,因而它多了
一个功能,那就是根据当前的速率,估算出预计剩余时间。_SingleTorrent中定义了一个
RateMeasure。
PiecePicker:块选取器。定义于BitTorrent/PiecePicker.py中,进行“下一块下载哪
块”这件事情的决策工作,与_SingleTorrent一一对应。
Downloader:下载工作管理器。定义于BitTorrent/Downloader.py中,管理该种子任务
中的所有下载工作。因为一个种子文件的下载过程中要和很多个对等客户打交道,因此需要
建立若干个连接。与_SingleTorrent一一对应。
Encoder:连接管理器。定义于BitTorrent/Encoder.py中,管理该种子文件任务中的所
有连接(不管是主动连接到其它对等客户上或者是其它对等客户连接到本地),与
_SingleTorrent一一对应。
Connection:连接。定义于BitTorrent/Connecter.py中,一个该对象对应于一个连接
。因此一个_SingleTorrent中包含了若干个Connection对象(由Encoder负责统一管理)。
SingleDownload:单一下载。定义于BitTorrent/Downloader.py中,对应一个连接中的
下载。它与Connection一一对应,且由Downloader对象产生(Downloader.make_download),
每次新的连接建立时,Encoder都会把这个连接保存起来,并且产生一个SingleDownload对
象。
Upload:单一上传。定义于BitTorrent/Downloader.py中,对应于一个连接中的上传。
和SingleDownload一样,它与Connection一一对应,每次新连接建立时,由Encoder产生。
Bitfield:位图对象。定义于BitTorrent/bitfield.py中,用来表示一个比特数组。它
典型用途是表示当前的种子文件的下载过程中,本地有第几块,没有第几块。出现在两个地
方,StorageWrapper,储存本地的块拥有情况信息,以及SingleDownload中,储存别人的块
拥有情况信息(以方便决定以后是不是要从他那里下载)。
Rerequester:跟踪请求发生器。定义于BitTorrent/Rerequester.py中,作用就是和跟
踪服务器打交道,来获取对等客户的信息。与_SingleTorrent一一对应。
DownloaderFeedback:下载任务状态信息搜集器。定义于
BitTorrent/DownloaderFeedback.py中,它提供了搜集下载任务的状态信息的接口,可以完
成状态信息的搜集以显示给用户。图形界面程序或者其它的界面程序在调用
_SingleTorrent的搜集信息函数时,最终还是要和该对象打交道(可以参阅
_SingleTorrent.get_status函数的实现)。与_SingleTorrent一一对应。
这一次分析BT的存储管理。我们知道,BT把要共享的资源化分成统一大小的块,并且在
种子文件中记录每一块的消息摘要值,以便在下载时确定某一块是否已经正确下载。而且在
前面的种子文件的制作过程中我们已经看到,除非是最后一块,其它的块大小都是相同的,
因此很有可能出现在一个文件的开始多少个字节属于某一块,然后从中间偏移多少字节又属
于某一块,或者在文件比较小的情况下某一块包含了若干文件等。而BT的存储管理部分就对
程序的其它部分屏蔽了这些区别,即对其它部分而言,只需要按照块来进行存取。
首先了看FilePool类,它在Multitorrent中定义,就是说,全局只有一个。因此它可以
保证多个种子文件在下载时硬盘上的文件被打开的数量限制在一定数量。内部维护了如下变
量:handlebuffer为所有已经打开的文件的列表,allfiles是一个字典,记录所有的文件的
拥有者情况,即哪个文件是属于哪个种子文件。它的关键字是各个文件的文件名,值则是对
应的_SingleTorrent对象。handles则是记录文件名与对应句柄关系的字典,whandles还说
明了哪些文件是可写的,注意,在whandles中,并没有储存对应句柄,即如果有一个文件出
现在handles中,可以通过handles直接获取其句柄,避免多次打开或者关闭文件,而如果它
出现在whandles中,那么说明它还是可写的。
Storage是在每个_SingleTorrent中被定义。创建Storage需要一个文件和它们对应的大
小的列表,文件的大小方面的信息可以从种子文件的元信息里得到,另外,必须要把种子文
件的元信息的文件列表中的每一个项目都加上在硬盘中实际保存的目录,以便可以直接对应
到某个具体的文件。Storage在创建的时候建立文件名和全局的字节之间的映射关系,即列
表ranges,该列表的每一个列表项是一个三元组,起始偏移,结束偏移,文件名。它的意思
是种子文件的内容中,从第几个字节到第几个字节是属于哪个文件。另外,假设种子文件中
对所有信息的内容进行了块的划分,设这个块长为piecelen,那么每一块还有一个字节偏移
,如从第0个字节到第piecelen(不含)个字节是属于第一块,接下来属于第二块等。因此这
个Storage类就要解决BT的下载按块进行和硬盘中按文件进行存储之间的矛盾。这里再提一
下,之所以把piece翻译成块,是因为后面的BT的下载过程中,还要把每一块再切分成若干
的slice,而我习惯于把slice翻译成片。
在Storage中,有两个私有函数_intervals和_get_file_handle,它们给read和write提
供了两项重要的功能,而read和write是Storage对外提供的重要接口。_intervals的任务是
提供一个全局的偏移量和长度,返回一张表,说明要对这些数据进行访问应该分别访问哪些
文件的第几个字节开始的多少字节。这样,在read和write里面就可以用for ... in
_intervals(xx,xx)了。而_get_file_handle则是获取一个文件的实际句柄,以便对其进行
读写,在Storage中获取文件的句柄要用一个函数来处理的原因是必须考虑到文件打开数量
的要求限制,因而在_get_file_handle中要和FilePool打交道,在打开一个文件句柄的同时
,要对FilePool内部的一些变量进行维护。
Storage还有一项功能就是读写一个“快速恢复”状态文件。它只负责读写一部分,这
个文件中还有一部分数据由StorageWrapper类来进行读写。由Storage类负责读写的部分是
文件头,包括'BitTorrent resume state file, version 1'字样,和总的数据量,以及各
个文件的大小和更改时间等。
StorageWrapper类则是在_SingleTorrent._start_download中定义,提供的接口要更加
高级。例如,它提供按照某一块来进行访问,然后在内部通过把块号乘以一块的大小来得到
实际的字节偏移量,然后让Storage来进行读写。另外,它维护了一张本地拥有哪些块的比
特数组have,可以方便决策。还有两个表示块的存储状况的数组,places和rplaces,它们
的意思是数据的第几块存储在硬盘上的第几块以及硬盘上的第几块存储的实际上是数据的第
几块。这个数组基于两部分数据的抽象:第一部分的数据抽象是把种子文件中所表示的内容
(即共享的资源)看成是一块连续的数据,这块数据有若干块。第二部分的数据抽象是把存储
在硬盘上的文件,看成是一块连续的数据,即连续的存储空间,也分成若干块。当下载任务
全部结束后,应该有对于0到self.numpieces-1,有places[i]=rplaces[i]=i。而在下载过
程中,由于采取了一定的策略,不一定是先下第0块,再下第1块等,因此在这个过程中有可
能places[i]和rplaces[i]中的值不等于i。
我们先来看一下比特数组,即Bitfield,它定义在BitTorrent/bitfield.py中。它用最
节约的空间完成了比特数组的存储,即比特数除于8的字节数。另外,它实现了
__setitem__和__getitem__,这样就可以直接对have[i]进行读写操作来完成值的操作。注
意到在__setitem__的实现中有assert val,这就意味着只能把数组中的某一项赋值成1。这
项功能比较适用于表示块拥有的状况,即某一块只能从没有到有,不能“得而复失”。
StorageWrapper还有一项很重要的功能就是对每一块数据再次细分成若干个slice,而
一个slice就是两个对等客户之间通过网络进行数据交换的最小单位。在此基础上,它要负
责生成请求,inactive_requests储存的就是所有可能的请求。在看这部分代码时,注意1和
l的区别,在初始化时,inactive_requests[i]的值是1,表示某一块还没有(因而可以为此
生成网络请求),当得知某一块已经获取时,inactive_requests[i]的值变为None,具体得
知某一块已经获取时要进行的操作为markgot,它的意义是第piece块在硬盘上的存储空间中
的第pos块被发现。另外,初始化的时候调用_check_partial和_make_partial检查某个具体
的块,看看有哪些slice还需要下载。把这些请求放到inactive_requests中,以后当程序其
它部分决定要开始下某一块时,StorageWrapper为其生成相应的网络请求的参数(第几块,
偏移多少,请求多少长度的数据),new_request即完成这项工作,另外piece_came_in和
get_piece提供数据的读写操作,调用它们的时候都要指定index(第几块),begin(块内偏移
),length(长度,在piece_came_in中是piece,即数据本身,可以直接得到它的长度)。
最后,关于存储管理这部分,还有一点需要提到,那就是早期的某个BT版本是在下载刚
刚开始的时候就申请好相应的硬盘空间。而现在则是随着下载的进行文件逐渐增大。但是下
载的顺序很可能不是先下第0块,再下第1块这样,因此在文件中存储的顺序也不一样,这样
当新的下载数据到来存储到硬盘上时,很可能就要对起进行调整,尽可能让它们“对号入座
”。_move_piece函数就能进行数据的移动,而参考piece_came_in开头部分对_move_piece
调用的代码就可以理解BT在下载过程中逐渐使块的顺序“对号入座”的这个过程。
这一次开始恢复按照过程进行描述,即从Multitorrent.start_torrent函数的执行开始
。
通过前面的分析,我们知道当Multitorrent.start_torrent被调用时,一个新的种子下
载任务就开始了。这个函数本身很简单,就是创建(并返回)一个新的_SingleTorrent对象,
然后让其start_download方法开始调度。start_download这个函数一开始看上去有点奇怪,
其实这是作者设计的一个小技巧。python中的yield关键字可以使一个函数返回一个值,但
是它的内部执行状态仍然保存,这样下次调这个函数的时候,这个函数就继续从那里往下执
行。可以用诸如it = self._start_download(*args, **kwargs)这样的形式来确定一个迭代
器,注意在执行这一句的时候,_start_download并未得到执行。要使包含有yield的关键字
的函数得到执行,只需要反复调用it.next(),这将返回每次yield出来的值,当函数执行到
结尾时,将会抛出一个StopIteration异常,通过捕捉这个异常就可以知道函数以及执行完
毕。在start_download中干了以下事情,把一个函数赋值到_contfunc,并且执行了它一次
。这个函数的实际内容就是开始执行_start_download,为什么要这样费一下周折呢,这样
做的目的就是为了在合适的时候分出一个线程。到目前为止,程序还是只有一个主线程在运
行。继续往下看_start_download函数,根据元信息的文件列表和保存到硬盘上的地址,整
理出一个实际的文件列表,可以直接对它们进行存储。然后创建一个新的Storage对象,它
需要的文件名和大小的元组列表可以通过zip函数得到,这个函数的功能是从第一个参数(列
表类型)中获取第一个元素,然后和第二个参数的第一个元素组成一个元组,再将第一个参
数和第二个参数的第二个元素组成一个元组等,最后变成了一个列表。然后进行“快速恢复
”的文件检查。接下来注意到函数hashcheck,通过创建一个新的线程,然后让它开始运行
,接下来yield None,注意,从这一句开始,其实就已经返回了。hashcheck函数将在新的
线程开始执行,我们来看看hashcheck函数中都干了什么,主要就是创建了一个
StorageWrapper类,它初始化时就已经对硬盘上有的内容确定下来了。然后,它执行了
_contfunc()!没错,执行它的效果就是从yield None后面的部分继续执行下去了,但是,
这时已经是在另一个线程中。接下来创建一个Choker,以及一些速度测量器。然后要创建一
个PiecePicker,初始化完成后,还要告诉PiecePicker哪些块已经拥有了
(PiecePicker.complete)以及哪些块已经下了一部分(PiecePicker.requested)。下面创建
一个Downloader对象,但是对于Upload,只是定义一个函数make_upload,它能够随时生成
一个Upload对象。然后创建一个Encoder对象,注意它把Downloader和make_upload做为参数
,从结构上来说,它们就被绑定到一起了。接下来要用add_torrent把一个种子文件(以
infohash为关键字)和它的Encoder绑定到一起,这样,当收到其它的对等客户的连接的时候
就能够知道对方要下载的是哪个种子文件了。最后创建Rerequester和DownloaderFeedback
这两个对象。
最后执行Rerequester.begin,启动它,让它开始和跟踪服务器交互,然后就可以调用
feedback接口的started函数,意思就是说,我们已经开始了,至于是用图形界面还是文字
界面向用户表示这一事实那就是feedback接口的事情了。
Rerequester。它的作用即为向跟踪服务器要对等客户的信息,前面通过对跟踪服务器
的代码分析已经对客户端和跟踪服务器之间的通信协议有了一个基本的了解。我们称和跟踪
服务器进行一个http请求并获取它的回应数据的过程称为一次发布(announce),
Rerequester的begin函数能够保证自己每隔60秒_check一次。我们来看_check一次要做什么
:首先要保证两次发布的时间间隔不能过短,另外要根据自己的peerid制作
url(_makeurl:
&port=xxxx&key=xxxx),根据不同的情况调用_announce进行一次发布。给_announce的参数
的意义是'event'参数的值,这些'event'的意义可以在跟踪服务器的代码分析中看到,它们
确定了下载的不同的阶段。因为平时也还需要经常补充一些对等客户的信息,所以
_announce会经常被调用。它的主要任务是对url进行进一步的加工,计算出当前发布时所用
的url,保存在s中,然后用一个新的线程开始执行发布,使用新的线程的原因是不希望到跟
踪服务器的网络阻塞影响到程序的其它部分的执行。_rerequest就基本上可以只管发出这个
http请求了,当然,它开始的部分代码是要排除一些自己的外部IP和实际IP不相同的这种情
况。Request是zurllib中的模块,可以很轻松地发送一个http请求,然后获取返回的信息。
根据是否出错来决定调用_postrequest的情况。这里出错仅仅是http请求本身发生错误,如
网络问题等,跟踪服务器也可能会返回一些其它的错误信息,我们可以在_postrequest中看
到。
_postrequest首先便是检查是否有错误信息,然后把data进行bdecode,这个过程我们
已经很熟悉了。接下来用check_peers检查看这是不是一个规范的对等客户信息数据,
check_peers在BitTorrent/btformats.py中定义,btformats.py还有其它的检查信息格式的
函数。下一步是检查r中有没有关键字'failure reason',如果有的话,那就是说到跟踪服
务器的网络没有问题,但是跟踪服务器返回了其它的失败原因,这样还是一种失败的情况。
下面就是把r中的关键字为'peers'部分的数据解析出来了,这部分传回来的数据有可能是紧
凑的字符串也有可能是一个字典,在跟踪服务器的代码分析中我们可以看到这一点。最后就
可以调用connect函数试图逐个得与对等客户建立联系了。connect函数实际上是
Encoder.start_connection。
阅读(1179) | 评论(0) | 转发(0) |