BT源代码学习心得(十四):客户端源代码分析(对等客户连接中的阻塞管理)
Author: wolfenstein
从上一次我们的分析可以看出当对等客户建立连接后,通过握手协议交换信息,这样对于每个连接都有一个Connection对象,然后有一个SingleDownload和Upload与其对应。这一次将从握手协议完成后继续分析,然后介绍Choker,阻塞策略控制器的工作原理。
SingleDownload在初始化时没有做什么特殊的操作,仅仅是创建了一个BadDataGuard对象和它对应。这个对象是用来统计坏数据的信息,以便确定坏的对等客户的。而Upload对象在创建的时候,如果自己已经有部分下载数据,就把自己的块拥有情况发送出去(send_bitfield)。现在就可以来看send_bitfield,我们可以看到在Connection中定义了不少send_xxx函数用来发送某种消息,并且在Connection对象定义之前,定义了那些消息的类型的对应的常数项。另外这些send_xxx函数大都调用了_send_message,它的作用就是在要发送的消息前面添加上它的长度(4个字节),然后发送出去,如果有必要,则放入队列中稍后发送。这样,每一次_got_message得到的就是消息的内容了。
现在来看_got_message,它直接取第一个字节就行了,这就是消息的类型。以后我们可以再检查其它类型的消息,现在我们直接看elift==BITFIELD这部分,得到对方的块拥有状况比特数组后,让自己的download对象记录下来,即调用SingleDownload.got_have_bitfield()。这个函数首先检查自己是否下载完成,然后检查对方的比特数组中"假"值的个数,如果自己下载完成了且对方的比特数组中"假"值为0,则说明对方也下载完成了,而两个都在做种的对等客户之间的连接是没有意义的,可以关闭它。
然后self.have=have这一句记录下对方的块拥有情况。所以前面提到StorageWrapper保存自己的块拥有状况,而对应于每个Connection对象的SingleDownload对象中则保留了对方的块拥有状况。然后让PiecePicker记录下别人有一块(got_have,complete记录的是自己有一块,在_SingleTorrent的代码中可以看到)。下面的这个endgame则是一种策略模式,即表示进入收尾阶段,它检查自己所有的网络请求all_requests(后面还会分析到它),如果对方有某一块(再次注意,这里self.have[piece]是对方有第piece块,而不是自己),那么发送一条消息,send_interested()。表示说我对你(所拥有的内容)感兴趣。而如果没有进入收尾阶段,则只是检查自己有那块没有而对方有,如果有的话,则send_interested()。注意send_interested()调用一次,对方知道这个意思就行了。
看Connection._got_message中得到这个消息后怎么处理。是self.upload.got_interested()。这个函数中维持自己的interested变量为真值,然后通知choker这件事情,choker.interested则选择是否要进行一次_rechoke()。现在应该注意到choked和interested这两个变量,这两个变量的值的意义分别是是否阻塞和是否感兴趣,它们对下载起到直接开关的作用。在每个SingleDownload和Upload对象中都有这两个变量。在初始化时,choked都为真而interested都为假,这样就不会有实际的内容(即种子文件的共享资源)在流通,而要有实际的内容流通必须这两个变量的值和它们初始化时的值刚好相反才行,也就是说,只有当一方对另外一方感兴趣,而对方又没有拒绝你(choked=false)的时候,你们之间在这个方向才可能会存在实际的下载流量。另外这两个变量在网络连接的每个方向都是保持一致的,即Upload中的这两个变量和连接另外一头的SingleDownload中的这两个变量保持一致,如果有某个变量发生变化,要发送消息给对方,让对方能继续保持一致。注意这里的保持一致指的是网络连接的两头,而不是本地的Connection对象对应的SingleDownload和Upload,即本地的Connection的SingleDownload和对等客户的Upload保持一致,而本地的Connection的Upload和对等客户的SingleDownload保持一致,而在同一个连接中,下载和上传的两个方向有可能不一致,即一个方向阻塞了,另一个方向还在下载。
前面已经注意到,interested这个变量的改变很容易,只要发现对方有自己没有的块,就会发送这条消息,而choked这个变量的控制就有一定的策略了。Choker就控制所有的连接(_SingleTorrent级别)的阻塞。它在初始化时即保证_round_robin每十秒种执行一次,而每次有连接进入时,用connection_made来进行登记,Choker中维护了所有连接的列表,且这个列表是故意打乱顺序的。在BT的控制策略中,我们还可以多次看到随机打乱顺序的情况发生,因为有时随机数就是最好的策略。在_round_robin中,首先检查是否已经完成,如果完成则调用_rechoke_seed(),按照自己已经开始做种的情况进行处理。而计算count%3的余数就可以保证_round_robin执行三次这部分代码会执行一次,因为count只有在_round_robin中会被加一。这部分代码就是选择一个choked和interested同时为真的连接放到列表的开头(不要让喜欢你的人等待太久)。
在_rechoke()中,首先选择出一些符合解除choked状态的连接(条件是interested和下载方向的is_snubbed,即当前时间是否距离上次下载到东西的时间过短),然后把所有的这些连接按照下载的速度排序,由于前面增加了一个负号,因此下载速度最块的排在前面。然后根据配置项中的最大上传数计算一个配额,这个配额不能等于最大上传数,最多只能对于这个数减一,从这个列表中取出排名前面的若干位,设置一个mask标志。下面计算出最小上传数,count。count至少要为一,如果最小上传数比前面的配额还大,那么count也相应增大。下面就是解除choke状态了,首先mask为1的,无条件解除,如果mask不为1,但是count还大于0,那么用掉一个count,解除choke状态,其它的连接,一律choke掉。
Upload的choke和unchoke都是在确定状态改变的情况下,开始向对方通知这一消息。这一次结合连接中开始的部分消息交互过程,介绍了choker这一阻塞策略管理器的工作原理。下一次将开始介绍在连接的双方的已经同意交换数据(choked为假而interested为真)时的情况。