BT源代码学习心得(十六):客户端源代码分析(实际数据的传输及其速率限制策略)
Author: wolfenstein
上一次分析了下载过程中如何进行下载某一块的选取。这次分析在收到对方的下载请求后程序的处理行为。
首先,仍然看Connection._got_message中收到请求消息的处理代码,即elift==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中的elift==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的运作原理,即对等客户之间是如何交换数据基本上分析完了,剩下的未分析的部分代码基本上可以自行阅读。下一次将对整个学习心得做一些总结。