https://github.com/zytc2009/BigTeam_learning
分类: C/C++
2010-12-13 11:16:08
tracker服务器是BT下载中必须的角色。一个BT client 在下载开始以及下载进行的过程中,要不停的与 tracker 服务器进行通信,以报告自己的信息,并获取其它下载client的信息。这种通信是通过 HTTP 协议进行的,又被称为 tracker HTTP 协议,它的过程是这样的:
client 向 tracker 发一个HTTP 的GET请求,并把它自己的信息放在GET的参数中;这个请求的大致意思是:我是xxx(一个唯一的id),我想下载yyy文件,我的ip是aaa,我用的端口是bbb。。。
tracker 对所有下载者的信息进行维护,当它收到一个请求后,首先把对方的信息记录下来(如果已经记录在案,那么就检查是否需要更新),然后将一部分(并非全部,根据设置的参数已经下载者的请求)参与下载同一个文件(一个tracker服务器可能同时维护多个文件的下载)的下载者的信息返回给对方。
Client在收到tracker的响应后,就能获取其它下载者的信息,那么它就可以根据这些信息,与其它下载者建立连接,从它们那里下载文件片断。
关于client和tracker之间通信协议的细节,在“BT协议规范”中已经给出,这里不再重复。下面我们具体分析 tracker服务器的实现细节。
从哪里开始?
要建立一个 tracker服务器,只要运行 bttrack.py 程序就行了,它最少需要一个参数,就是 –dfile,这个参数指定了保存下载信息的文件。Bttrack.py 调用 track.py 中的 track()函数。因此,我们跟踪到 track.py 中去看track() 函数。
Track.py:track()
这个函数首先对命令行的参数进行检查;然后将这些参数保存到 config 字典中。在BT中所有的工具程序,都有类似的处理方式。
接下来的代码:
r = RawServer(Event(), config['timeout_check_interval'], config['socket_timeout'])
t = Tracker(config, r)
r.bind(config['port'], config['bind'], True)
r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes']))
t.save_dfile()
首先是创建一个 RawServer 对象,这是一个服务器对象,它将实现一个网络服务器的一些细节封装起来。不仅tracker服务器用到了 RawServer,我们以后还可以看到,由于每个 client端也需要给其它 client 提供下载服务,因此也同时是一个服务器,client的实现中,也用到了RawServer,这样,RawServer的代码得到了重用。关于 RawServer的详细实现,在后面的小节中进行分析。
接着是创建一个 Tracker对象。
然后让RawServer绑定在指定的端口上(通过命令行传递进来)。
最后,调用 RawServer::listen_forever() 函数,使得服务器投入运行。
最后,在服务器因某些原因结束运行以后,调用 Tracker::save_dfile() 保存下载信息。这样,一旦服务器再次投入运行,可以恢复当前的状态。
其它信息:
1、 BT源码的分布:
把BT的源码展开之后,可以看到有一些python程序,还有一些说明文件等等,此外还有一个BitTorrent目录。这些 python程序,实际是一些小工具,比如制作 metafile的btmakemetafile.py、运行tracker服务器的bttrack.py、运行BT client端的 btdownloadheadless.py 等等。而这些程序中,用到的一些 python 类的实现,都放在子目录 BitTorrent 下面。我们的分析工作,通常是从工具程序入手,比如 bttrack.py,而随着分析的展开,则重点是看 BitTorrenet子目录下的代码。
BT作者 Bram Cohen 在谈到如何开发可维护的代码的一篇文章中(),其中提到的一条就是开发一些小工具以简化工作,我想BT的这种源码结构,也正是作者思想的一种体现吧。
2、 我们看到,python和我们以前接触的 c/c++ 不一样的第一个地方就是它的函数在定义的时候,不用指定参数类型。既然这样,那么,在调用函数的时候,你可以传递任意类型的参数进来。例如这样的函数:
def foo(arg):
print type(arg)
你可以这样来调用:
a = 100
b = “hello world”
foo(a)
foo(b)
输出结果是:
这是因为,第一次调用 foo()的时候,传递的是一个整数类型,而第二次调用的时候,传递的是一个字符串类型。
这种参数具有动态类型的特性,是 c/c++等传统的语言是所不具备的。这也是 python 被称为动态语言的一个原因吧。C++的高级特性模板,虽然也使得参数类型可以动态化,但使用起来,远没有python这么简单方便。
RawServer 类的实现代码,在 BitTorrent 子目录的RawServer.py 中
RawServer 这个类的作用是实现一个网络服务器。关于网络编程的知识,《unix网络编程:卷1》是最经典的书籍,你如果对这块不了解,建议抽时间看看这本书。RawServer 实现的是一种事件多路复用、非阻塞的网络模型。它使用的是 poll() (而不是我们常见的select(),关于 poll和select的比较,也在《unix网络编程:卷1》中有介绍)函数,处理过程大致是这样的:
首先创建一个监听 socket,然后将这个 socket 加入 poll 的事件源;
随后进入服务处理循环,即:
调用 poll() 函数,这个函数会阻塞,直到网络上有某些事件发生或者超时才返回给调用者;
在 poll()返回之后,先检查一下是否有没有处理的任务,如果有,那么先完成这些任务。然后根据事件类型进行处理。
如果是连接请求(监听 socket上的POLLIN事件)到来,它 accept这个请求,如果 accept 成功,那么就和一个 client建立了连接,于是将 accept() 新创建的 socket 加入 poll 的事件源;
如果在已经建立的连接上(连接socket上的POLLIN事件),有数据可读,那么将数据从 client 端读过来,做进一步处理;
如果已经建立的连接已经准备好(连接socket上的POLLOUT事件),可以发送数据,则检查是否有数据需要发送,如果有,那么发送数据给 client 端。
(所以,tracker是一个单进程的服务器,并没有用到线程。)
Bram Cohen 认为软件的可维护性非常重要,使代码易于维护的重要一条就是设计可重用的类,RawServer 在设计的时候,充分考虑到了可重用性,集中表现在两个地方:
1、 将网络 I/O 和数据分析处理分离。
网络服务器的事件多路复用、网络I/O 部分通常是固定不变的,而数据在读取之后,进行分析处理的过程则是可变的。RawServer 将可变的数据处理工作,交给另外一个抽象的类 Handler (实际上并没有这么一个类)来处理。比如,在 tracker 服务器的实现中,具体使用的就是 HTTPHandler 类,而在 以后将要分析的 BT client 实现代码中,用到的具体的Handler 是 Encoder 类。
2、 采用任务队列来抽象出任务处理的过程。
RawServer维护了一个任务队列 unscheduled_tasks(实际是一个二元组的list,二元组的第一项是一个函数,第二项是超时时间)。在初始化的时候,首先向这个队列中加入一个任务:scan_for_timeouts(),这样,每隔一段时间,服务器就会去检查一下是否有连接超时。如果有其它
RawServer的成员函数中,对外暴露的有:
u __init__:(初始化函数)
u add_task():
在任务列表中增加一项任务(一个任务是一个函数以及一个指定的超时时间的组合)
u bind():
首先创建一个socket,然后设置socket的属性: SO_REUSEADDR和IP_TOS,,这两个属性的具体含义请参考《unix网络编程:卷1》,另外还将 socket 设置为非阻塞的。相对于阻塞的 socket来说,非阻塞的 socket 在网络 I/O 性能上要提高许多,但是与此同时,编程的复杂度也要提高一些。象 tracker这种可能同时要处理成千上万个并发连接的服务器,只能采用非阻塞的socket。
然后将该 socket和指定ip已经端口绑定;
最后把这个socket 加入 poll的事件源。
u start_connection():
对外主动建立一个连接,这个函数在处理NAT穿越的时候用到了,我们后面分析到 NAT穿越的时候,再具体讲解。
u listen_forever():
这个函数的功能就是实现了我在前面描述的网络服务器的处理过程。我们看到,它唯一的参数是handler,handler的作用就是封装了对数据的具体处理。
listen_forever()把对网络事件的处理过程,交给了 handle_events()。
其它函数,包括handle_events(),都是内部函数(也就是外部不会直接来调用这些函数)。Python没有c++那样 public、protected、private 这样的保护机制,python类的内部函数命名的惯例是以下划线开始,例如 RawServer 中的 _close_dead()等。
u handle_events():
事件处理过程,主要是根据三种不同的网络事件分别处理,一是连接事件,二是读事件、三是写事件。
if sock == self.server.fileno()
这段代码判断发生事件的socket是否是监听 socket,如果是,那么说明是连接事件。
连接事件的处理:
通过 accept 来接受连接,并将新建立的 socket 设置为非阻塞。
判断当前连接数是否已经达到了最大值(为了限制并发连接的数目,在初始化 RawServer的时候,需要指定最大连接数目),如果已经达到最大值,那么关闭这个新建的连接。
否则,根据新的 socket 创建一个 SingleSocket 对象,(SingleSocket 封装了对 socket的操作。)将这个对象加入内部的列表single_sockets中,以备后用。
将这个新 socket加入 poll 的事件源
最后,调用 Handler 的external_connection_made() 函数,关于这个函数,在后面分析 HTTPHandler 时再讨论。
if (event & POLLIN) != 0:
这段代码判断是否是读事件
读事件的处理:
首先刷新一下连接的最后更新时间 (last_hit)。
然后读取数据;
如果什么也没读到,那么说明连接被关闭了(在网络编程中,如果一个连接正常的被关闭,那么,也会触发读事件,只不过什么也读不到)
否则,调用 Handler的 data_came_in() 函数来处理读到的数据。
if (event & POLLOUT) != 0 and s.socket is not None and not s.is_flushed():
这段代码判断是否是写事件,而且确实有数据需要发送。在一个连接可以写的时候,就会发生写事件。
写事件的处理:
实际代码是在 SingleSocket的 try_write()函数中。
在一个非阻塞的连接上发送指定大小的数据,很可能在一次发送过程中,数据没有被完全发送出去(只发送了一部分)就返回了,所以,每次 write之后,必须判断是否完全发送了数据。如果没有发送完,那么下次有读事件的时候,还得回来继续发送未完得数据。这也是这个函数叫做 try_write 的原因吧。
try_write() 在最后,要重新设置 poll 的事件源。如果数据全部发送完毕了,那么只需要监听读事件(POLLIN)否则,既要监听读事件,也要监听写事件(POLLOUT),这样,一旦连接变的可写,可以继续将剩下的数据发送出去。
u scan_for_timeouts():
任务处理函数,它首先把自身加入未处理任务队列中,这样,经过一段时间,可以保证这个函数再次被调用,从而达到周期性调用的效果。
它检查每个连接是否超过指定时间没有被刷新,如果是,则该连接可能已经僵死,那么它关闭这个连接。
u pop_unscheduled():
从任务列表中弹出一个未处理的任务。
与 RawServer 配合使用的是 SingleSocket 类,这是一个辅助类,主要目的是封装对 socket的处理吧。包括数据的发送,都交给它来处理了。这个类比较简单,大家可以自己去看,我就不罗嗦了。
以上是对 RasServer 的具体实现的一个分析,可能读者看的还是晕晕糊糊,没办法,还是必须自己去看源代码,然后在遇到问题的时候,回头再来看这篇文章,才会有帮助。如果不亲自看源码,终究是纸上谈兵。
我们再来小结一下。
RawServer 封装了网络服务器的实现细节,它实现了一种事件多路处理、非阻塞的网络模型。它主要负责建立新的连接,从网络读取和发送数据,而对读到的数据的具体处理工作,交给 Handler 类来处理,从而把网络I/O和数据处理分离开来,使得 RawServer可以重用。Handler 类是在调用 listen_forever() 的时候,由调用者传递进来的,具体到 tracker服务器,就是HTTPHandler。有了 RawServer,tracker 就可以作为一个网络服务器运行了。
下一节,我们开始分析具体实现 tracker HTTP 协议处理的 HTTPHandler类和Tracker类。