BT源代码学习心得(十六):客户端源代码分析(实际数据的传输及其速率限制策略)

时间:2022-05-22 16:37:44

BT源代码学习心得(十六):客户端源代码分析(实际数据的传输及其速率限制策略)

发信人: wolfenstein (NeverSayNever), 个人文集
标  题: BT源代码学习心得(十六):客户端源代码分析(实际数据的传输及其速率限制策略)
发信站: 水木社区 (Fri Aug 19 12:01:16 2005), 文集
(本文包含HTML标记,终端模式下可能无法正确浏览)
    上一次分析了下载过程中如何进行下载某一块的选取。这次分析在收到对方的下载请求
后程序的处理行为。
    首先,仍然看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的运作原理,即对等客户之间是如何交换数据基本上分析完了,剩下
的未分析的部分代码基本上可以自行阅读。下一次将对整个学习心得做一些总结。