BT源代码解析1

03月 22nd, 2008

bt的源代码是使用python写的,这是一种动态类型的语言,所有的对象不需要定义其类型,任何对象可以作为参数传入某个函数中,唯一的要求是当调用该对象的某个方法时,它必须存在。另外这种语言提供了大量的模块,这些模块中很多都能在不同的平台实现其功能,大大得方便了编写跨平台程序。

在bt的代码中,主要功能都有命令行模式和图形界面模式两种执行方式,但最后它们执行的核心功能的代码都是相同的,区别在于执行这些核心功能时,传递给它们的参数是从命令行和配置文件处获取还是从图形界面中获取。

在我开始学习时,看的是4.0.3版本的代码。主要有两个主要的执行模块btdownloadgui和btmaketorrentgui,前者是客户端,后者是制造种子文件的工具(从4.0.0版本开始,btmaketorrentgui代替了btcompletedir)。另外,还有个tracker模块也很重要。学习的时候如果喜欢直接切入正题,就可以不看和gui相关的部分,直接看实现核心功能的模块。

提一下图形界面,bt的图形界面模块用的是gtk,它的详细资料可以在这里找到:

http://www.pygtk.org/

使用gtk编写图形界面的好处是它的跨平台性很好,可以在不同的操作系统上生成风格相近的图形界面。另外在bt中貌似还用了另一个图形界面模块库(btdownloadcurses),我大概看了一下说明,好像这个curses只能用于某些平台,想了下我主要的学习目的是bt,于是在gui方面就集中精力攻gtk了,这个curses库就没有去看它。

我学习bt的过程大概如下:

看python语言教程熟悉python语言。

试着看btdownloadgui,发现看着头很大,另外发现很多模块在python网站上的模块参考手册上没有。遂发现了gtk的网站,熟悉了一下使用gtk编写gui程序的基本方法后,继续试图看btdownloadgui的图形部分,有些明白,但是还是感觉到有些吃力。

开始尝试转移一下目标,先看btmaketorrentgui,研究一下种子文件是怎么生成的,如果心里对种子文件的结构有了解再研究下载部分的代码应该能轻松些。这部分比较成功得完成了,学习到了bt的种子文件的结构,还对gtk的gui程序编写也比较熟悉了。

接下来看的是tracker部分的代码,看的时候基本上都看完了,知道了一个tracker是如何得与客户端通信。但是对于一些具体的数据结构可能还会存在一些模糊的地方。

最后回过头来看btdownloadgui的代码,发现终于可以顺利得看下去了。然后将所有看到的结果总结起来,学习到了bt的通信协议。

今后的部分将把以上说的学习过程具体展开。

程序运行参数的获取

把这部分单独列举出来,是因为我觉得bt的程序在处理配置参数方面的这部分代码很有参考价值。

程序的配置参数首先来源于bittorrent/defaultargs.py。这个模块中包含了一些参数的默认值,由于它们是直接编译进bt的模块中,因此即使其它的配置文件都丢失后,程序还是有一些默认值可以作为参数。defaultargs.py中定义了一个函数get_defaults可以让各个可执行模块得到它们对应的默认值。例如btdownloadgui和btmaketorrentgui得到的默认值是不同的。而且defaultargs.py中为可执行模块生成的默认属性集合是一个以三元组为元素的列表。每一个三元组代表一条属性(也就是一条程序的配置参数),每个三元组包含了属性的名称,属性的值和该属性的说明。值得注意的是,在后面我们可以看到,属性的说明直接作为帮助信息输出给用户。

在获取完默认值后,各个可执行模块通常都调用bittorrent/configfile.py中的函数parse_configuration_and_args来进一步从可能存在的配置文件和命令行参数中对程序运行的一些参数进行调整。这个函数中首先从配置文件中获取信息,值得一提的是它获取配置文件的方法。bt将在用户的"主目录"下建立一个.bittorrent的目录,并且从这里读取配置文件(如果这个文件不存在就建立一个,下次就存在了)。其中"主目录"的获取方法在bittorrent/__init__.py中,它的实现方法比较巧妙,利用了python的os和os.path库进行操作,可以针对不同的操作系统使用合适的方法得到这个"主目录"。在windows下,这个目录通常是文档和设置目录下的用户名目录下的"application data",而在linux下这个目录就是~。

在parse_configuration_and_args中,还要调用bittorrent/parseargs.py模块中的parseargs对命令行中输入的参数进行处理。根据其中的注释,可以看出程序将会把以下格式的参数作为一种控制方面的操作(options)而改变原来的配置,其它的参数都只是简单得堆在args数组中供可执行模块自己去处理。这些格式有–aaa 1型,即以两个-号开头的参数,将和他后面的参数一起,构成一个配置项(aaa=1),-a1型,即以一个-号开头的参数,直接取一个字母作为配置项的属性名,这个参数后面的子字符串作为配置项的属性值(a=1),-a 1型,即以一个-号开头的参数,但是后面只有一个字母,就将下一个参数作为属性值(因此这里也是a=1)。

经过这些处理后,程序的其它部分就可以很方便得使用配置好的参数了,例如:

if self.config['xxx']

…….

或者

aaa = self.config['bbb']

等等。

种子文件的编码方式

bt的作者使用了一种比较简单易懂的编码方式来对设计种子文件。这种编码方式能够很简单得对python中的各种数据类型,如字符串,整数,列表,字典等进行编码。而且对于类型的嵌套,如一个列表中的元素又是一个列表等情况能够进行很好得处理。

bittorrent/bencode.py模块负责进行编码解码的工作。函数bencode能够对python的复杂数据类型进行编码。这个函数的意思难道是bt encode?它通过恰当得递归调用自己来完成任务。

首先他判断要编码的数据的类型,然后根据这个类型调用相应的编码函数encode_func[type(x)],定义了以下类型的编码函数:

encode_int,负责对inttype和longtype类型进行编码。编码为一个字母"i"加上这个数值的字符串表示再加上一个字母"e"。就是说整数19851122将会被编码成"i19851122e"存在文件中。

encode_string,负责对stringtype类型进行编码。编码方式为字符串的长度加上一个冒号":"再加上字符串本身。例如helloworld将被编码成"10:helloworld"存放在文件中。

encode_list,负责对listtype和tupletype类型进行编码。编码方式为一个字母"l",然后递归得调用相应的编码函数将列表或者元组的所有元素进行bencode,最后编上一个"e"结束。

encode_dict,负责对dicttype类型进行编码。编码方式为一个字母"d",然后递归得对每个元素进行处理。在dicttype中,每个元素都由一个key和value对组成。首先以长度加":"加实际值的方式编码key,因为key通常都是简单值,所以可以这样编码。然后对value进行bencode,最后加上一个"e"结束。

通过分析以上的编码函数我们可以看出,复杂的对象被以此种编码方式进行编码后将能够无歧义地被还原出来。而bt的种子文件就是这样一种复杂的对象(字典类型)。知道编码方式后,下次介绍种子文件时,只需要解释这个字典类型包含的每个元素的情况即可,保存成文件和从文件中读取的这个过程就不需要再解释了。

种子文件的生成

在知道种子文件采取的编码方式后,我们现在可以来看一个种子文件具体是如何生成的了。在bt中,生成种子文件的可执行模块是btmaketorrent.py(命令行模式)或者btmaketorrentgui.py(图形界面模式),通过分析,可以知道它们最终都将调用函数make_meta_files进行种子文件的生成,区别仅仅在于提供给这个函数的参数从何而来。命令行模式下的程序很简单,即直接从命令行下获取参数,gui部分的程序以后再和下载客户端的图形界面程序一起分析,现在我们先直接切入正题。

bittorrent/makemetafile.py模块中提供函数make_meta_files。它的参数意义如下:

url:tracker的url地址,在bt的协议设计中,还是需要有个服务器作为tracker来协调各个客户端的下载的,tracker部分的程序以后会介绍,现在只需要知道这个url将要作为一条信息写入到种子文件中即可。

file:种子文件的来源文件或目录列表(即准备要在bt上共享的资源),注意,这里的列表意思是该列表中的每一项都为其生成一个种子文件,而此列表中的每一项可以是一个文件或者是一个目录。

flag:一个event对象,可以用来检查是否用户要求中止程序。程序设计得比较合理,可以在很细的粒度下检查这个event是否被触发,如果是则中止执行。

progressfunc:一个回调函数,程序会在恰当的地方调用它,以表示现在的工作进度,在命令行模式下,这个回调函数被指向在控制台上显示进度信息的函数,在gui模式下,这个回调函数则会影响一个图形界面的进度条。

filefunc:也是一个回调函数,程序会在恰当的地方调用它,以表示现在在处理哪个文件。

piece_len_pow2:分块的大小,bt中把要共享的资源分成固定大小的块,以便处理。这个参数就是用2的指数表示的块的大小,例如当该参数为19的情况下,则表示共享的资源将被分成512k大小的块为单位进行处理。

target:目标文件地址,即种子文件的地址。这个参数可以不指定(none),则种子文件将与公享资源处于同一目录。

comment:说明。一段可以附加在种子文件内的信息。

filesystem_encoding:文件系统编码信息。

make_meta_files的主要工作是进行一系列的检查。例如在开始的时候就检查files的长度(元素的个数)和target,当files的长度大于1且target不是none的时候就会报错,因为如果要生成多个种子文件的话,是不能指定target的(这样target只确定了一个种子文件的保存位置)。接下来检查文件系统的编码问题。然后把files中所有以.torrent结尾的项目全部刨掉,剩下的作为参数传递给make_meta_file进行处理,注意,这个函数一次生成一个种子文件。

下面来看make_meta_file,它一开始计算出块的大小,以2的指数为基础。接下来找到种子文件的保存地址,如果有target,以target为准,否则如果要对一个目录生成种子文件,则生成以那个目录名为名称,后缀".torrent"的文件。否则生成以源文件为名称,后缀".torrent"的文件。

下面调用函数makeinfo来生成一个"info"。这个info是什么东西呢?继续看。makeinfo首先检查传给它的path,看看是单个文件还是一个目录。如果是一个目录的话,则调用subfiles把这个目录下的所有文件全部列出来,这个subfiles设计得比较巧妙,使用堆栈的方法避免了递归调用。从subfiles得到结果后,首先对它们进行排序。然后使用变量fs保存这些文件的列表信息,fs是一个list结构,每个元素包含了文件名称和它的大小组成的二元组。接下来就是记录文件的内容了,下面的这个算法看上去有点晕,其实它的意义是很明确的,每次从要共享的资源里读取长度为piece_length(就是前面那个以2的指数为基础计算出来的块的大小)的数据,然后计算它的sha消息摘要值。如何做到这一点呢?就是根据那个排好序的文件列表,读出piece_length的长度的内容,如果这个文件长度不够,则再读下一个文件,知道长度够了或者读完所有文件为止。生成一个消息摘要后把它加入到pieces数组中,再读下一块,直到全部处理完。为一个文件生成info的方法类似,只是更简单,直接从这个文件中一块一块得处理即可。最后这个makeinfo返回的info是一个字典,它的数据如下:

pieces:每一块的消息摘要值的连接。

piece length:每一块的长度。

files:文件的列表信息,这里由于文件顺序和生成消息摘要的顺序是相同的,以后bt的客户端根据种子文件的描述,就可以很清晰得确定原始的文件名和它们的大小,再配以消息摘要值,就可以检查下载内容是否正确了。

name:种子文件的内部名称,种子文件可以被随便改名,但是为了识别它方便,内部还是起了这么一个名称的,通常用要共享的资源来命名它。

我们注意到flag.isset多处被检查,其中粒度最小的地方是在读取了一块之后。它返回后将一路返回到make_meta_files结束,这样用户随时可以中断程序的执行。

在makeinfo返回info这个字典类型的数据后,再调用check_info这个函数对其内容进行检查,这个函数定义在bittorrent/btformats.py模块中,后面在客户端进行下载的时候还需要检查它。

最后我们看到的是一个类型为字典的data,其中的元素包含了announce,一个字符串,creation date,一个整型数据,info,又是一个字典,如果有comment的话,那么还包含了字符串类型的comment。

最后把这个类型为字典的data保存到磁盘上,工作就算完成了。怎么对这种比较复杂的数据类型进行编码以方便保存呢?就是上次提到的bencode。

所以我们可以看到,一个种子文件就是一个类型为字典的data编码后的情形。

统一网络服务接口–rawserver

以后的部分都需要网络服务(种子文件的生成在本地就可以完成,但是通过这些种子文件下载实际的内容和提供跟踪器服务都需要网络),在bt的程序设计中,为网络服务提供了统一的接口,这样程序中的其它部分需要打开一个网络服务时,只需要向这个接口进行注册,并提供相应的处理对象(handler)即可,当网络事件发生时,将会自动这个处理对象中的相关函数进行处理。

这个统一网络服务接口定义在bittorrent/rawserver.py中,由它去实际调用和网络插口(socket)有关的库,另外,rawserver还提供add_task功能,可以允许一些任务被延后执行。

rawserver在初始化的时候,可以从外部传入一个doneflag参数,这是一个event的数据类型,可以从其它地方触发它,这样可以随时中断rawserver中的主循环(listen_forever中的)。另外还进行一些内部变量的设置。最后,它给自己增加了一个任务,scan_for_timeouts,这个任务会定时得检查超时的网络连接,并关闭它们。

我们可以看到add_task的所做的工作就是将要延时执行的任务计算出它的实际执行时间,并把它添加到一个排好序的列表中(funcs),且保持这个列表仍然处于有序状态,这个列表以实际执行时间为顺序。

当其它模块要提供网络服务时,它首先调用rawserver的create_serversocket函数,这个函数会返回一个socket对象,并且这个socket返回时,已经处于listen状态了。当然,这个时候如果真有外部的网络连接进来,还是不会有什么动作的,因为相应的处理对象还没有注册进来。

接下来应该调用start_listening函数,这个函数的作用是把得到的网络插口和它对应的处理对象添加到一个字典中,该字典以网络插口的描述符(fd)为主键。值得注意的是,这个函数名称中虽然有listen字样,但是socket.listen函数却不是在这里调用,而是在create_serversocket就已经被调用了。传递进来的处理对象的类型没有限制,唯一的要求是它必须包含有external_connection_made函数,这样当外部网络连接到来时,这个函数就会被调用。处理对象通常还应提供data_came_in函数来处理网络数据,以及connection_flushed函数来处理数据已经正式发出(相对于还在缓冲区的情况)时的处理,后面两个函数也可以不提供,因为在external_connection_made函数里,可以把新连接的网络数据处理对象重新定位到一个包含有data_came_in函数和connection_flushed函数的对象。start_listening函数处理完后,该网络插口就已经存在于serversockets字典中了。

而当其它模块要连接到外部网络时,应该调用start_connection函数,这个函数将把网络插口添加到另一个字典single_sockets中,当然,使用了singlesocket对象对其进行了一定程度的包装。从后面的分析可以看到,这个singlesocket对象的主要功能是对输出的数据进行了一定的缓冲,并在不会阻塞的情况下把这些数据实际得写到socket中。start_connection需要传入的处理对象是必须包含data_came_in而可以不包含external_connection_made的对象。

在start_listening和start_connection中都用到了poll对象,这是系统提供的一个提供轮询机制的模块,使用文件描述符作为参数,可以得到相应的事件(即该文件描述符对应的插口有数据流入或者留出等),而在这两个函数中,都调用了poll的注册函数,方便后面的poll轮询操作。

需要注意的是,在上面的这些函数被执行后,网络连接还是不会被处理,因为虽然打开了相应的网络插口,也注册了相应的处理对象,但是整个的轮询机制还没有建立起来。直到listen_forever函数被调用后,这个机制才真正得建立起来。这个函数的主体就是一个无限的while循环,只有doneflag这个事件可以被用来中止这个循环。它首先做的事情是从添加的任务funcs寻找最近要执行的任务的时间,并与当前时间相减,计算出period,然后用poll轮询这么长的时间,这样做就可以保证轮询结束后不会耽误外部任务过久。轮询到的结果返回在events里,这是一个列表,它的元素是以文件描述符和事件所组成的二元组。接下来就是根据时间的情况,把需要马上执行的外部任务都执行了,_make_wrapped_call的主要作用就是执行外部任务,只是给它们增加一些意外处理的保护代码。执行完这些外部任务后,调用_close_dead关闭不活跃的网络连接,接下来就是使用_handle_events来处理前面的poll搜集到的网络事件了。

_handle_events的主体是一个for循环,检查每一个sock和它对应的event。首先看它是在serversockets字典中还是在single_sockets字典中,如果是前者,那么这是一个侦听中的插口,再检查网络事件,如果不是出错事件的话,那么就说明是有外部连接到达,熟悉socket编程的人都应该知道,这时正确的处理方式是建立一个新的socket,然后让侦听中的插口去accept它,以后数据的读写应该在新的socket中进行。接下来的处理也是这样,新的socket被用singlesocket包装起来了,并且也被放到single_sockets字典中,因为它和用start_connection建立的socket一样,都是有可能有数据流入的,而侦听的插口只需要处理网络连接。接下来,前面注册的处理对象中的external_connection_made函数被调用了,允许进行一些其它的相关操作,我们注意到,这里处理对象被原封不动得传入到新的singlesocket中,当然实际上在external_connection_made函数中可以把singlesocket的处理对象重定向到其它对象中。

接下来的else语句说明sock在single_sockets字典中,只有一种情况例外,就是os.pipe。这种情况下不用处理这个事件,直接continue处理下一个事件即可。然后检查事件,如果是出错则关闭该插口,否则就说明是有数据流动,而数据流动无非是流入和流出两种情况,如果是流入的话,就把数据读到一个缓冲区里,然后调用处理对象中提供的data_came_in进行处理,而data_came_in得到的参数直接就是缓冲区中的数据,它不需要再处理socket以及考虑可能会形成的阻塞等问题了。另外由于singlesocket中对写操作也进行了包装,即如果网络有阻塞的可能,数据也会先写入缓冲区,这样data_came_in中就可以随便调用s.write了。最后如果是数据流出,则调用s.try_write,这个函数实现得也很安全。最后检查是否数据都已经真的发出去了(flushed),如果是,则调用处理对象中提供的connection_flushed函数进行收尾工作。

以后我们可以看到,在bt的实现中,创建了各种各样的对象,而且这些对象之间有各种各样比较复杂的关系,但是所有的网络服务,都是通过rawserver来进行的,再具体一些,那就是rawserver这个对象只会被创建一个,而所有要求网络服务的模块都会把网络服务的处理对象注册到这个rawserver中,方便统一管理。

最后说一下,今天用google搜索发现原来去年就已经有人分析过bt的源代码,不仅感叹自己孤陋寡闻,不过发现现在的版本(4.0.3)和当时的版本已经有了一些差别,而且我也可以以我的阅读源代码的思路继续前进,提供给大家一个不同的视角,因而决定把我的学习心得继续写完,希望大家能够支持。

跟踪服务器(tracker)的代码分析(初始化)

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动起来的效果。

跟踪服务器(tracker)的代码分析(http协议处理对象)

上次我们分析了tracker类初始化的过程,现在开始具体看跟踪服务器是如何提供服务的。

首先分析tracker处理对象是httphandler,它定义在bittorrent/httphandler.py中,这个对象的初始化函数很简单,只是把tracker.get函数赋值到自己的一个内部变量备用。当有外部网络连接到达时,根据前面对rawserver的分析,我们知道,httphandler.external_connection_made函数会被调用,它维护了自己内部的一个字典connections,以传进来的参数connection(它的类型是singlesocket)为关键字,值为一个新建立的httpconnection,新建立的httpconnection也主要是进行一些值的初始化,另外注意这句:

self.next_func = self.read_type

这个变量被指向自己的一个函数,后面我们还会看到,它还会发生变化,以灵活处理数据的不同部分。

现在可以分析客户端和跟踪服务器的网络通讯协议了。当有数据到达时,httphandler.data_came_in会被调用,从它的代码中我们可以一眼看出,起主要作用的其实是该网络连接对应的httpconnection的data_came_in函数,它首先检查donereading标志和next_func函数,即如果已经完成读的操作或者没有next_func来处理下一步,都直接返回,然后将data(网络中读到的数据)添加到自己内部的buf中,下面是一个while循环,可以看出,它的做法是每次从网络数据中寻找n值,以该值做为两个不同的处理单元,然后将这个回车前面的部分赋值到val,后面的部分赋值到buf(就相当于buf在这个回车前面的部分砍掉,剩下的留待下一次处理),然后将这个val交由next_func处理,处理的结果返回给next_func,意思就是在next_func里处理完这部分值后,它很清楚下面一部分该由哪个函数处理,然后将next_func重新定向到它就行了,最后进行一些检查看看还要不要继续处理。这个函数我们可以看出,设计得比较巧妙,能够自动得把一个协议的不同的部分分到不同的函数进行处理,而且即使网络阻塞了,只来了一部分数据,下次又来一部分数据,只要它和buf一整合,next_func永远指向处理下一部分数据的函数。

从httpconnection的初始化过程我们知道,第一部分的数据处理的函数read_type,首先去除空格,然后把它们按照空格符分开,如果有三个词,那么认定它的格式为command path garbage,否则,认为是command path。然后检查command必须是get或者head,现在也已经可以猜出来path应该是一个url路径,至此,我们可以看出,客户端和跟踪服务器的通信协议其实就是http协议。接下来就是read_header来读取http的头部。它首先看有没有数据,如果有的话,很简单,只是维护一个字典headers,且寻找到’:',’:'之前的就是关键字,之后的就是值,然后next_func还是read_header,就是说,剩下的数据都是一行一行的头部信息。全部读完后,检查headers里面有没有accept-encoding项,这项指定返回的数据的编码方式,只有两种,普通模式(’identity’)和压缩模式(’gzip’),然后调用getfunc,其实就是tracker.get来正式处理用户的http请求,而且已经把请求转化成比较方便的参数,即path(用户的请求url)和headers信息。处理完毕后,如果返回的结果不是none的话,则调用answer把处理结果返回给用户。

我们先看answer,看到它的参数,我们就知道,它把返回的结果转化成http协议的要求。传给它的参数是一个元组,包含回应代码,回应字符串,头部数据,正文数据四部分。它首先看是否要压缩,如果是的话,就进行压缩,但是压缩后它把压缩后的数据和之前的数据进行长度比较,如果压缩后数据反而更长,那么就不压缩了。接下来是进行日志的记录,诸如某年某月某日某时某分某人在这里请求了某物,返回了某些数据等等。前面我们注意到在tracker初始化的时候已经把标准输出重定向到日志文件中了,因此这里的print其实就是往日志文件中写。然后用一个stringio来处理字符串的操作,可以不断得往里面write,我们看到,程序按照标准的http应答格式("http 1.0 xxx responsestringblablabla..n")的格式,全部处理完后,一次性地往connection里write,把它传送到网络里,rawserver里面已经帮我们处理好了网络阻塞之类的问题,然后检查,如果数据全部写出去了,那么就关闭这个连接。http协议也确实是这样的,一个请求,一个回应,就完成了。

现在我们可以看到,在bt中客户端和跟踪服务器之间的通信协议就是http协议,而且httphandler和httpconnection已经把http的很细节的部分全部都处理好了,这就意味着tracker.get已经得到了一个连接对象,一个用户请求的地址,以及一个字典类型的http请求头部数据,并且这个函数只需要专心得完成处理,并把处理的结果以包含http回应代码(200,404,500等),回应字符串(如not found,这样和前面的代码合起来就是http 1.0 404 not found),http回应头部数据和正文数据的四元组返回即可。

下一次,我们就可以很仔细得看tracker到底是如何得处理用户请求了

标签:, ,

相关日志


This entry was posted on 星期六, 03月 22nd, 2008 at 11:47 pm and is filed under c语言教程. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Leave a Reply