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

来源:互联网 发布:数据库基础教程 推荐 编辑:程序博客网 时间:2024/06/05 05:11

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),且保持这个列表仍然处于有序状态,这个列表以实际执行时间为顺序。
    当其它模块要提供网络服务时,它首先调用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)和当时的版本已 经有了一些差别,而且我也可以以我的阅读源代码的思路继续前进,提供给大家一个不同的视角,因而决定把我的学习心得继续写完,希望大家能够支持。 

原创粉丝点击