这篇文章,我们来分析
RawServer
以及一些相关的类。
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
类。
posted on 2007-01-19 00:18
苦笑枯 阅读(365)
评论(0) 编辑 收藏 所属分类:
P2P