上一次介绍了对等客户之间在连接建立后的一些动作,以及BT中的阻塞控制策略。这一
次将介绍当某个连接终于畅通时,双方的数据交互,也以此为基础介绍BT中另一重要的策略
控制器PiecePicker。
Choker在选择了解除一个连接的阻塞后,Upload.unchoke()将会执行,Connection对象
的send_unchoke()也在此被执行。当网络的另一端收到这条消息后,它对应的
SingleDownload.got_unchoke()将会开始进行处理。它再检查自己的interested状态,如果
自己也感兴趣的话,那么就用_request_more()开始向对方请求数据了。
_request_more()可以给一个indices作为参数,这个参数是一个列表,意思就是说优先
下载号码在这个列表中的块。如果这个参数为None,那意思就是说你自己看着办吧,觉得下
哪块合适就下哪块。首先检查自己的active_requests,就是当前连接中已经发出去的请求
,如果已经发出去的请求太多了(而还没有数据返回),就暂时不发出新的请求了而是直接返
回。下面检查endgame,如果已经进入这个阶段则按照这个阶段的方式去下载
(fix_download_endgame(),收尾阶段特殊方式下载)。
接下来就开始生成请求了,首先检查indices,如果是None,那么让PiecePicker来挑一
块,否则,逐个的检查indices中的值,如果这个号码的块对方有(have[i])而自己又想要
(do_I_have_requests(i)),那么就是它了。PiecePicker如何进行块的选取的策略我们稍后
再分析,现在我们知道的就是它已经决定下载某一块了。然后要检查interested,如果有必
要,还要通知一下对方。下面一段就是不断向StorageWrapper要网络请求的参数,
new_request根据自己在硬盘上的某一块的拥有情况,不断得返回块内相对偏移和长度。在
这里,我们可以看出,对等客户之间要求传输实际的数据的请求有三个参数,即第几块,块
内偏移多少,长度多少。而这个长度是根据配置文件中的参数决定的,通常就是一个slice
,它要能一次下载完。当然,一块的长度不一定是slice的整数倍,因此最后一个slice的长
度要短一些,不过,这些细节在StorageWrapper中已经处理好了。从StorageWrapper得到请
求后,就把它加到自己的active_requests中,然后让自己的Connection对象去
send_request()。现在我们也应该更加清楚active_requests和inactive_requests的意义了
,即平时StorageWrapper根据实际情况,准备好inactive_requests,然后在
SingleDownload对象中请求发出时,把它们逐渐转移到自己的active_requests中。
在两个while循环的下面,检查active_requests,意思就是说如果经过以上的所有过程
,如果active_requests还是空的,那么说明什么呢?只能说明对方根本就没有(或者说曾经
有,但是现在已经没有了)自己感兴趣的数据,而如果自己还是interested的话,要调用一
个send_not_interested(),意思是我不再对你感兴趣了。下面检查lost_interests中的值
,这些都是在下载过程中曾经是自己想要的,但是现在已经不想要了(主要原因是自己已经
拥有了)。接下来这个for循环的意思就是检查所有的SingleDownload对象,告诉它们某一块
已经有了,不用再去下了,而且有些SingleDownload要因此发出NOT_INTERESTED。最后再次
检查是否进入endgame阶段,如果是,则按照这种阶段的行为进行处理。
现在我们就可以来研究PiecePicker这个块选择策略控制器的行为了,从前面的分析我
们知道,每个PiecePicker对应一个_SingleTorrent,使用它时经历了以下几步:首先是初
始化,然后根据自己已经有的块,把它告诉给PiecePicker(complete(i)),以后就不要从这
中间选了,还有就是当一个SingleDownload对象获取对方的块拥有状况位图时,也要告诉
PiecePicker(got_have(i)),意思是这一块有人有了。最后当需要PiecePicker做出选择时
,只要调用其next函数,它需要一个判断函数(_want),以及一个对方是否是种子的标志
(self.have.numfalse == 0),_want函数就是这样的一个函数,当PiecePicker选了一块后
,要拿给它检查,看看这一块是不是它确实想要的,如果不是的话,PiecePicker会重新选
择。而_want()函数的判断标准很简单,那就是别人有而自己又想要的。
PiecePicker的初始化工作主要是对自己的内部变量进行。这里要解释一下这些变量的
作用,这样能够更加方便地对后面的部分进行理解。numpieces,总的块数。interests是按
照拥有者的数量排序的块列表的列表,就是说,它是一个列表,列表中的第0个元素是所有
的自己感兴趣而没有人有的块的列表,第1个元素是所有的自己感兴趣而只有一个人有的块
的列表,等。pos_in_interests,就是每一块在interests中的对应元素所表示的列表中的
位置,如果某一块比如说i,自己已经有了,那么pos_in_interests[i]的值没有意义。
numinterests的值就是某块有多少人拥有(不包括自己),以上三个变量保持这样的关系:如
果一块i,自己没有,那么interests[numinterests[i]][pos_in_interests[i]]=i。have是
一个布尔数组,表示自己已经有那块,在初始化完成后,它应该和StorageWrapper中的实际
情况保持一致。crosscount则是一个统计情况数组,即有多少块没有人拥有,有多少块有一
个人拥有,等,自己拥有的某一块也在这里参加计数。numgot,已经得到的块数。
scrambled,一个包含从0到numpieces-1的序列,但是被随机打乱了。
现在来看PiecePicker.complete,即自己有了某块,首先have中的值要设置,然后从
numinterests中查到自己原来有多少人拥有,把crosscount中对应的项减一,然后把它下一
项加一,如果没有下一项,那么就在后面添加一项。由此我们可以看到,crosscount数组是
逐渐增大的。然后它做的事情是把interests中的对应的项删除掉,因为它已经不在自己感
兴趣的范围内了,其它几行代码是为了保持这些变量值的一致性。然后试图从started和
seedstarted中删除这一块(如果没有这一块也无所谓,什么也不用做)。
PiecePicker.got_have,处理的情况是别人有了某一块。首先还是保持crosscount的一
致,然后处理interests列表。调用_shift_over把piece从interests列表中的一个元素转移
到另一个元素(同时还要保持其它变量的值的意义的一致性)。_shift_over做的事情就是从
第一个列表中删除一个元素,然后将其插入第二个元素随机的位置,同时维护
pos_in_interests值的意义。
PiecePicker.requested,哪一块已经开始下载了,这个在SingleDownload中会被调用
,它只是维护两个列表,started和seedstarted。
PiecePicker.next,可以说是PiecePicker中提供的最重要的功能,选择一块进行下载
。它选择的第一条原则是,已经开始下载的优先把它下载完(return choice(bests)及其前
面的代码)。它检查选择的两个数组,根据对方是否是种子选择一个数组。然后在所有的这
个数组中选择出自己想要的,检查它们的numinterests,即拥有此块的人数,选出拥有人数
最少的块,放入bests中,如果有并列的,则添加到bests,因此在这里结束后,bests中的
元素是所有正在下载的且自己想要的块中拥有人数最少的块的列表,那么就从中间随机选择
一个返回即可。选择的第二条原则是,当自己拥有的块数少于一定的数量时,随机选择自己
想要的块进行下载(第一阶段结束后的那个if块),因此它用到了那个scrambled列表,而当
自己所拥有的块数超过一定的值(config['rarest_first_cutoff'])后,执行第三阶段的方
案。选择的第三条原则是,优先选择下载拥有的人数最少的块,我们看到,它从interests
中第1个元素开始检查,选择最先找到的自己想要的块,第0个元素不用检查,因为没有人拥
有的块肯定下载不到。我们可以看出,它的选择原理是比较简单但是又很有效的,优先下载
拥有人数最少的块就能够保证所有的块能够在最短的时间内尽可能得让更多的人拥有,换一
个术语说就是能尽快提高要下载的内容的副本率。
这一次我们分析了对等客户在下载的过程中,如何进行下载的策略控制。下一次将分析
收到对方的下载请求后的处理方式等。
上一次分析了下载过程中如何进行下载某一块的选取。这次分析在收到对方的下载请求
后程序的处理行为。
首先,仍然看Connection._got_message中收到请求消息的处理代码,即elif t ==
REQUEST:后面的部分。首先检查这个消息是否符合格式,它的长度必须是13(1个字节的消息
类型加上3个4字节整数,分别代表块的位置,块内偏移,请求长度),以及块的位置必须小
于自己拥有的总块数,然后由Upload.got_request进行处理。在Upload.got_request中,首
先检查状态,如果对方还没有声明interested就或者申请的长度大于自己的
max_slice_length,即一次能够发送出去的最长的数据块,那么中断连接,由此可见,在
BT通信协议中,要先声明interested才可以向对方请求数据。然后在自己的Connection没有
发送choke时就可以发送数据了,但是这里发送数据它并不是直接发送数据,而是把请求保
持在自己的buffer中,然后让RateLimiter把自己的Connection加入到它的队列中。
RateLimiter,在Multitorrent中定义,作用是对全局的速度进行限制。由于BT通信协
议中,只有发送实际的数据会需要比较多的带宽,因而也只有在这种情况下会需要用
RateLimiter来对其进行限制。现在我们可以注意到在每个Connection中还有一个
next_upload变量,它在其它地方都没有用到,仅仅是在这里,它的作用就是把若干个连接
通过这种方式组成一个链表。next_upload的类型是Connection,不是Upload,这里要注意
。我们看到RateLimiter.queue函数中进行的就是数据结构中很常见的链表操作,其中
self.last指向了上一个Connection对象,插入新的Connection对象时,last会指向它。另
外如果原来队列是空的话,那么开始try_send,否则就不用做什么,因为try_send会检查队
列,逐个从中取出连接对象,并且发送数据。try_send中首先计算offset_amount,这个值
的意义就相当于可以发送多少字节,也就是一种“配额”,它的值小于0就可以继续发送,
发送了一些字节后增加相应的字节,如果大于0,那么就停下来,把发送的任务往后延一段
时间。其中如果check_time标志为真的话,那就清0,以前的时间不算,重新开始计算。配
额每次减少的字节数是上一次的时间(self.lasttime)和这次的时间差乘以upload_rate,这
也很好理解,隔了这么些时间,又可以上传若干字节了。下面的while循环就是在配额还有
的情况下,不断调用send_partial函数进行数据的发送,然后发送完毕后,检查该连接是否
已经暂时没有发送需求了(即返回的实际发送的字节数是0或者连接还未刷新,即flushed),
如果该连接暂时没有需求,则将其从链表中删除。但是无论它还有没有需求,接下来发送的
都是链表中的下一个元素。另外,在python中允许while循环后跟一个else语句,它被执行
的条件是循环正常结束,即因为while的循环条件不满足而结束循环,而当使用break来退出
循环时,这个else语句后面的内容是不会被执行的。在这里,while的结束条件是配额用完
。那么意味着还有数据要下载,那么就等待一段新的时间继续执行此任务,等待多久呢?它
等待的时间是刚好能把配额又降到0的时间。另外,由于直接执行可能会有一些延迟,因此
,这里肯定可以保证下次运行时有上传配额。另外这个while循环中唯一的break只有在发现
队列已经清空的情况下被执行到。
Connection.send_partial,负责实际发送数据。它有个参数bytes,指定了它最多只能
发送这么多数据。_partial_message是它维护的分块消息变量,如果它不能一次把它发送出
去,就把它截断,然后下次发送。首先看看它是否是空的,如果是,先从Upload处获取一块
代上传的消息(get_upload_chunk),这个函数的做法是从自己的buffer(Upload.buffer,前
面提到,表示自己要上传的请求,但是当时只是把自己的连接对象加到RateLimiter的队列
中)中获取一块请求,然后让StorageWrapper.get_piece去实际得按照要求把某一块的某一
部分读取出来,然后再更新一些速率统计对象的值,最后把这块数据返回。回到
send_partial中,得到数据块后,把_partial_message制造出来,做成可以直接往网络上发
送的那种格式。下面检查bytes,如果这次不让发送这么多数据,则只发送开始的部分,然
后截断剩余的部分。这样下次调用该连接的send_partial时就会继续发送剩下的数据。而如
果可以一次发送完,则在其队列尾部增加上choke或者unchoke消息,这里,我们看到,程序
保证了一部分(其实就是一个slice)如果要发送的话一定能发送完,即使阻塞控制器要求阻
塞某个连接,它也只能阻止发送完一部分后再继续发送下一部分。
好了,现在终于能够收到实际的数据了,我们继续来看Connection._got_message中的
elif t == PIECE:这一段。再次提醒,如果程序执行到这里的话,收到的部分一定是完整的
,因为每一条消息都是先发送了它的长度然后才是它的内容,而如果只收到部分消息的话,
程序最多执行到Connection._read_messages。当收到对方的发送的数据块后,先把开始的
两个整数解出来,即第几块,块内偏移多少(长度多少不用给出,因为已经有数据块的实际
内容),然后做一些基本检查。检查通过后,将其交给SingleDownload,
SingleDownload.got_piece会对其进行进一步处理。如果这个函数返回真值,意思就是说这
一块已经完整了,因为每一块被分成了若干个slice进行下载,因此下载到一个slice不一定
能使一块完整。而如果这一块确实完整了,那么给此Encoder的所有的正常Connection都发
出HAVE消息(send_have),意思就是通知所有和自己连接的对等客户,我刚刚下到了某一块
,以后你们要下载这一块也可以来找我。
现在来研究SingleDownload.got_piece,它的作用就是处理网络上到来的实际数据。首
先,从自己的active_requests中试图清除掉该数据对应的请求,如果发现自己根本就没有
请求那些数据,就直接丢弃它们。然后进行endgame检查以及更新一些速率测量器。接下来
要注意,StorageWrapper.piece_came_in会对数据进行检查,如果它返回真并不是说明这一
块数据下载完了,只是说明它没有检查出问题,而如果它返回假的值,那么后果就很严重了
,说明这一块数据有问题,整块的数据都需要重新下载。这个if块内的代码做的工作就是重
新分配下载任务。要调用StorageWrapper.do_I_have后才知道这个部分(slice)下载完后是
不是整个的这一块也完成了,如果是则再将这一信息通知
PiecePicker(PiecePicker.complete)。下载完后要进行一些检查,确定下一步的下载策略
,这些在以下的代码中可以看到。最后返回的值是自己是否已经下载完了这一块。
现在我们已经把BT的运作原理,即对等客户之间是如何交换数据基本上分析完了,剩下
的未分析的部分代码基本上可以自行阅读。下一次将对整个学习心得做一些总结。
阅读(1045) | 评论(0) | 转发(0) |