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

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

发信人: wolfenstein (NeverSayNever), 个人文集
标  题: BT源代码学习心得(五):统一网络服务接口--RawServer
发信站: 水木社区 (Fri Aug  5 18:54:08 2005), 文集
(本文包含HTML标记,终端模式下可能无法正确浏览)
    以后的部分都需要网络服务(种子文件的生成在本地就可以完成,但是通过这些种子文
件下载实际的内容和提供跟踪器服务都需要网络),在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)和当时的版本已经有了一些差别,而且我也可
以以我的阅读源代码的思路继续前进,提供给大家一个不同的视角,因而决定把我的学习心
得继续写完,希望大家能够支持。

你可能感兴趣的:(server)