BT源代码学习心得(1-5总体)

BT源代码学习心得():总体描述
author: wolfenstein

    BT的源代码是使用python写的,这是一种动态类型的语言,所有的对象不需要定义其类型,任何对象可以作为参数传入某个函数中,唯一的要求是当调用该对象的某个方法时,它必须存在。另外这种语言提供了大量的模块,这些模块中很多都能在不同的平台实现其功能,大大得方便了编写跨平台程序。
   
BT的代码中,主要功能都有命令行模式和图形界面模式两种执行方式,但最后它们执行的核心功能的代码都是相同的,区别在于执行这些核心功能时,传递给它们的参数是从命令行和配置文件处获取还是从图形界面中获取。
   
在我开始学习时,看的是4.0.3版本的代码。主要有两个主要的执行模块btdownloadguibtmaketorrentgui,前者是客户端,后者是制造种子文件的工具(4.0.0版本开始,btmaketorrentgui代替了btcompletedir)。另外,还有个tracker模块也很重要。学习的时候如果喜欢直接切入正题,就可以不看和gui相关的部分,直接看实现核心功能的模块。
   
提一下图形界面,BT的图形界面模块用的是gtk,它的详细资料可以在这里找到:
[
http://www.pygtk.org/ ] http://www.pygtk.org/
    使用gtk编写图形界面的好处是它的跨平台性很好,可以在不同的操作系统上生成风格相近的图形界面。另外在BT中貌似还用了另一个图形界面模块库(btdownloadcurses),我大概看了一下说明,好像这个curses只能用于某些平台,想了下我主要的学习目的是BT,于是在GUI方面就集中精力攻gtk了,这个curses库就没有去看它。
   
我学习BT的过程大概如下:
   
python语言教程熟悉python语言。
   
试着看btdownloadgui,发现看着头很大,另外发现很多模块在python网站上的模块参考手册上没有。遂发现了gtk的网站,熟悉了一下使用gtk编写GUI程序的基本方法后,继续试图看btdownloadgui的图形部分,有些明白,但是还是感觉到有些吃力。
   
开始尝试转移一下目标,先看btmaketorrentgui,研究一下种子文件是怎么生成的,如果心里对种子文件的结构有了解再研究下载部分的代码应该能轻松些。这部分比较成功得完成了,学习到了BT的种子文件的结构,还对gtkGUI程序编写也比较熟悉了。
   
接下来看的是tracker部分的代码,看的时候基本上都看完了,知道了一个tracker是如何得与客户端通信。但是对于一些具体的数据结构可能还会存在一些模糊的地方。
   
最后回过头来看btdownloadgui的代码,发现终于可以顺利得看下去了。然后将所有看到的结果总结起来,学习到了BT的通信协议。
   
今后的部分将把以上说的学习过程具体展开。

BT源代码学习心得():程序运行参数的获取
author: wolfenstein

   
把这部分单独列举出来,是因为我觉得BT的程序在处理配置参数方面的这部分代码很有参考价值。
   
程序的配置参数首先来源于BitTorrent/defaultargs.py。这个模块中包含了一些参数的默认值,由于它们是直接编译进BT的模块中,因此即使其它的配置文件都丢失后,程序还是有一些默认值可以作为参数。defaultargs.py中定义了一个函数get_defaults可以让各个可执行模块得到它们对应的默认值。例如btdownloadguibtmaketorrentgui得到的默认值是不同的。而且defaultargs.py中为可执行模块生成的默认属性集合是一个以三元组为元素的列表。每一个三元组代表一条属性(也就是一条程序的配置参数),每个三元组包含了属性的名称,属性的值和该属性的说明。值得注意的是,在后面我们可以看到,属性的说明直接作为帮助信息输出给用户。
   
在获取完默认值后,各个可执行模块通常都调用BitTorrent/configfile.py中的函数parse_configuration_and_args来进一步从可能存在的配置文件和命令行参数中对程序运行的一些参数进行调整。这个函数中首先从配置文件中获取信息,值得一提的是它获取配置文件的方法。BT将在用户的"主目录"下建立一个.bittorrent的目录,并且从这里读取配置文件(如果这个文件不存在就建立一个,下次就存在了)。其中"主目录"的获取方法在BitTorrent/__init__.py中,它的实现方法比较巧妙,利用了pythonosos.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源代码学习心得():种子文件的编码方式
author: wolfenstein

    BT的作者使用了一种比较简单易懂的编码方式来对设计种子文件。这种编码方式能够很简单得对python中的各种数据类型,如字符串,整数,列表,字典等进行编码。而且对于类型的嵌套,如一个列表中的元素又是一个列表等情况能够进行很好得处理。
    BitTorrent/bencode.py
模块负责进行编码解码的工作。函数bencode能够对python的复杂数据类型进行编码。这个函数的意思难道是BT encode?它通过恰当得递归调用自己来完成任务。
   
首先他判断要编码的数据的类型,然后根据这个类型调用相应的编码函数encode_func[type(x)],定义了以下类型的编码函数:
     encode_int
,负责对IntTypeLongType类型进行编码。编码为一个字母"i"加上这个数值的字符串表示再加上一个字母"e"。就是说整数19851122将会被编码成"i19851122e"存在文件中。
    encode_string
,负责对StringType类型进行编码。编码方式为字符串的长度加上一个冒号":"再加上字符串本身。例如helloworld将被编码成"10:helloworld"存放在文件中。
    encode_list
,负责对ListTypeTupleType类型进行编码。编码方式为一个字母"l",然后递归得调用相应的编码函数将列表或者元组的所有元素进行bencode,最后编上一个"e"结束。
    encode_dict
,负责对DictType类型进行编码。编码方式为一个字母"d",然后递归得对每个元素进行处理。在DictType中,每个元素都由一个keyvalue对组成。首先以长度加":"加实际值的方式编码key,因为key通常都是简单值,所以可以这样编码。然后对value进行bencode,最后加上一个"e"结束。
   
通过分析以上的编码函数我们可以看出,复杂的对象被以此种编码方式进行编码后将能够无歧义地被还原出来。而BT的种子文件就是这样一种复杂的对象(字典类型)。知道编码方式后,下次介绍种子文件时,只需要解释这个字典类型包含的每个元素的情况即可,保存成文件和从文件中读取的这个过程就不需要再解释了。

BT源代码学习心得():种子文件的生成
author: wolfenstein

    在知道种子文件采取的编码方式后,我们现在可以来看一个种子文件具体是如何生成的了。在BT中,生成种子文件的可执行模块是btmaketorrent.py(命令行模式)或者btmaketorrentgui.py(图形界面模式),通过分析,可以知道它们最终都将调用函数make_meta_files进行种子文件的生成,区别仅仅在于提供给这个函数的参数从何而来。命令行模式下的程序很简单,即直接从命令行下获取参数,GUI部分的程序以后再和下载客户端的图形界面程序一起分析,现在我们先直接切入正题。
    BitTorrent/makemetafile.py
模块中提供函数make_meta_files。它的参数意义如下:
    URL
TrackerURL地址,在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的长度大于1target不是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编码后的情形。

 

 

 

BT源代码学习心得():统一网络服务接口--RawServer
author:wolfenstein

    以后的部分都需要网络服务(种子文件的生成在本地就可以完成,但是通过这些种子文件下载实际的内容和提供跟踪器服务都需要网络),在BT的程序设计中,为网络服务提供了统一的接口,这样程序中的其它部分需要打开一个网络服务时,只需要向这个接口进行注册,并提供相应的处理对象(handler)即可,当网络事件发生时,将会自动这个处理对象中的相关函数进行处理。
   
这个统一网络服务接口定义在BitTorrent/RawServer.py中,由它去实际调用和网络插口(socket)有关的库,另外,RawServer还提供add_task功能,可以允许一些任务被延后执行。
    RawServer
在初始化的时候,可以从外部传入一个doneflag参数,这是一个Event的数据类型,可以从其它地方触发它,这样可以随时中断RawServer中的主循环(listen_forever中的)。另外还进行一些内部变量的设置。最后,它给自己增加了一个任务,scan_for_timeouts,这个任务会定时得检查超时的网络连接,并关闭它们。
   
我们可以看到add_task的所做的工作就是将要延时执行的任务计算出它的实际执行时间,并把它添加到一个排好序的列表中(funcs),且保持这个列表仍然处于有序状态,这个列表以实际执行时间为顺序。
   
当其它模块要提供网络服务时,它首先调用RawServercreate_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_listeningstart_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语句说明socksingle_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)和当时的版本已经有了一些差别,而且我也可以以我的阅读源代码的思路继续前进,提供给大家一个不同的视角,因而决定把我的学习心得继续写完,希望大家能够支持。 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值