author:wolfenstein
Tracker在BT中是一个很重要的部分。这个名词我注意到以前的文章中都是直接引用,没有翻译过来,想了一下,决定把它翻译成跟踪服务器。
在BT下载中,种子文件表明了要下载的文件的信息和对它进行检查的消息摘要码,但是每个对等客户(peer,以后我把peer全部翻译成对等客户,以区别client)要获取其它对等客户的信息时,还是要和跟踪服务器联系的。跟踪服务器上面不保存任何和种子所代表的内容有关的文件,它只记录所有下载该种子的机器的IP地址,端口等信息,并在客户向它请求是返回一些这样的信息列表,具体的实际内容,由对等客户之间完成交互。
跟踪服务器的代码实现在BitTorrent/track.py中,在bttrack.py中只是很简单得一行:
track(argv[1:])
这样就把参数传到track.py的track函数。track函数本身也比较简单,处理参数和相关的配置文件,建立一个RawServer,然后用create_serversocket创建服务器套接字,然后开始服务。关于在BT中使用网络服务上次已经有很详细地介绍,这里不再重复。只是针对tracker函数的具体情况,分析一下运行到listen_forever后的情况,首先,建立了Tracker对象,打开了在某个端口(config['port'])侦听的网络服务,这个函数的处理对象是一个HTTPHandler。所以我们要分析程序的流程只需要先分析Tracker的初始化函数,看看它创建后都做了些什么,然后再看HTTPHandler实际分析它的网络协议。
在Tracker对象的初始化函数中,首先还是对各种变量的初始化。然后要从一个状态文件中进行一些状态恢复,也就是恢复state变量。这个变量中的值很重要,我们可以需要从一些地方来得知它的结构,状态文件的读取和保存出得不到它的信息,因为这两处的实现方式就是bencode和bdecode,只能保证无论state的结构是什么都能合适得被保存和恢复,由此又看出bencode编码设计的巧妙。但是有一个函数对我们分析state的内部结构很有帮助,那就是statefiletemplate,这个函数检查state中的值是否合法,因此我们可以从这里得到state的一些结构信息。
首先,state必须是一个字典类型的变量。然后检查每一项的值。如果发现一项关键字是'peers',那么它的值必须也是一个字典,这个字典是一个以种子文件的信息部分的消息摘要值为关键字的字典,由于sha摘要算法比较好得满足了摘要算法的要求,即不同的种子文件它们生成相同摘要的概率极小。而且由于这是由种子文件的内容生成的摘要值,因此即使把种子文件改名,还是可以识别出来是哪个种子文件。因此'peers'的值可以看成是为每一个种子文件记录的信息,那么为每个种子文件记录的是什么信息呢?这个信息又是一个字典,这次以每个对等客户的ID为关键字,每个对等客户在连接到跟踪服务器的时候都会为自己生成一个ID,这个ID怎么生成的以后看客户端的代码可以知道,现在我们知道的是,它的长度必须为20。这个字典的值,嗯,又是个字典,不过这个字典的意义就明显多啦,包括了IP是多少,端口是多少,还剩多少没有下载完。因此state的内容可以看成是这样的:{'peers':{},...},其中peers的结构是这样的:{hash1:{ID1:{'ip':xxx.xxx.xxx.xxx,'port':xxxx,left:XXXX},ID2:{'ip':yyy.yyy.yyy.yyy,'port':yyyy,left:YYYY},...},hash2:{...},...}。以上是state中'peers'这一项。'completed'这一项就相对结构简单了,它记录的是每个种子文件的下载完成情况,它的结构是个字典,以每个种子的信息部分的消息摘要值为关键字,而对应的值就是一个整数,表示该种子文件已经有多少人完成了下载。接下来是'allowed'项,这项记录了该跟踪服务器所关注的所有的种子的信息,仍然以信息部分的消息摘要值为关键字,内容就是该种子文件的实际信息,从后面的分析(对BitTorrent/parsedir.py的分析)可以知道是哪些信息,另外由于之前对种子文件的内部结构我们已经比较清楚,所以也可以猜出部分。state中还有'allowed_dir_files'项,这一项也是记录文件信息的字典,但它是以每个文件的文件名为关键字(而不是消息摘要值),每个文件的项目是一个列表,结构如下:[(文件修改时间,文件大小),消息摘要值],就是说,这个以文件名为关键字的字典它的每一个值都是一个列表,这个列表有两个元素,第一个元素是一个二元组,内容是文件修改时间和文件大小,第二个元素是消息摘要值。最后,我们注意到statefiletemplate在处理'allowed'项和'allowed_dir_files'项时还有一些额外的检查代码,即所有在'allowed'项里面出现的元素,它的消息摘要值都必须在'allowed_dir_files'项中出现,且'allowed_dir_files'中所有的项中的值的消息摘要部分必须在'allowed'中出现,另外'allowed_dir_files'中不得出现重复的消息摘要值('allowed'项本身就以消息摘要值为关键字,而字典的关键字已经保证不会重复)。
因此现在我们知道了state中的注意部分的结构。下面我们注意这两句:
self.downloads = self.state.setdefault('peers', {})
self.completed = self.state.setdefault('completed', {})
这样就把state中的'peers'和'completed'的值传到了downloads和completed中,更重要的是,以后在跟踪服务器的运行过程中,如果'peers'和'completed'的值发生改变(那简直是一定的),state中的相应值也会发生变化,这样,保存dfile时,就可以及时更新state的值了。以后我们分析跟踪服务器运行过程的时候少不了和它们打交道,现在我们可以先记住,downloads保存了所有的下载的客户端的信息,completed保存所有的种子的下载完成情况的统计信息。
下面的这个for循环根据配置文件处理NAT的问题,以及计算种子的个数。completed只是记录所有下载完成的客户的数目,而只有已经下载完成(left=0),但是还在downloads中出现(即下载完毕但是没有关闭客户端)的客户端才算是一个种子。这里我们可以很容易得看出,seedcount是一个以信息摘要为关键字,整型为值的统计种子数的一个字典。
下面是一个计算的变量,times表示了每个种子(以信息摘要为关键字)中每个客户(以客户ID为关键字)的上次的有活动的时间。接下来增加了两个任务,每隔一段时间保存一下dfile,并且检查下载的客户端是否已经有很长时间没有反应的。
接下来准备一个日志文件,并试图把标准输出重定向到这个日志文件中。
最后要去寻找该跟踪服务器所关注的所有的种子,即parsedir,这个函数可以自己去看,相信在知道了种子文件的编码格式和前面的状态中的项的要求后,不难分析。总得说来,这个函数做了以下事情,即寻找某个目录下所有的.torrent文件,把这些文件中的信息读取进来,并且排除错误,重复等等不合要求的,然后进行加工,输出符合要求的结果,储存在allowed和allowed_dir_files中,进而影响state。
现在tracker对象已经建立起来,它已经有它要进行跟踪的所有种子的信息,并且准备好了维护所有连接进来的客户的列表,因此它可以正式开始提供跟踪服务了。下一次我们就可以看看tracker动起来的效果。