BT源代码学习心得(十一):客户端源代码分析(存储管理) 转自CSDN:gushenghua的专栏

BT源代码学习心得(十一):客户端源代码分析(存储管理)

发信人: wolfenstein (NeverSayNever), 个人文集
标  题: BT源代码学习心得(十一):客户端源代码分析(存储管理)
发信站: 水木社区 (Tue Aug 16 07:15:06 2005), 文集
(本文包含HTML标记,终端模式下可能无法正确浏览)
    这一次分析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在下载过程中逐渐使块的顺序“对号入座”的这个过程。 
原文地址:https://www.cnblogs.com/kokoliu/p/616925.html