本篇文章分析
Tracker
类,它在
track.py
文件中。
在分析之前,我们把前几篇文章的内容再回顾一下,以理清思路。
BT
的源码,主要可以分为两个部分,一部分用来实现
tracker
服务器,另一部分用来实现
BT
的客户端。我们这个系列的文章围绕
tracker
服务器的实现来展开。
BT
客户端与
tracker
服务器之间,通过
track HTTP
协议
进行通信,而
BT
客户端之间以
BT
对等协议
进行通信。
Tracker
服务器的职责是搜集客户端的信息,并帮助客户端相互发现对方,从而使得客户端之间能够相互建立连接,进而互相能下载所需的文件片断。
在实现
tracker
服务器的时候,首先是通过
RawServer
类来实现网络服务器的功能,然后由
HTTPHandler
类来完成对协议数据的第一层分析。因为
track HTTP
协议是以
HTTP
协议的形式交互的,所以
HTTPHandler
按照
HTTP
的协议对客户端的请求进行第一层处理(也就是取得
URL
和
HTTP
消息头),然后把
URL
和
HTTP
消息头进一步交给
Tracker
类来进行第二层分析,并把分析的结果按照
HTTP
协议的格式封装以后,发给客户端。
Tracker
类对
track HTTP
协议做第二层分析,它根据第一层分析后的
URL
以及
HTTP
消息头,进一步得到客户端的信息(包括客户端的
ip
地址、端口、已下载完的数据以及剩余数据等等),然后综合当前所有下载者的情况,生成一个列表,这个列表记录了下载同一个文件的其它下载者的信息(但不是所有的下载者,只是选择一部分),并把这个列表交给
HTTPHandler
,由它进一步返回给客户端。
如此,整个
tracker
服务器的实现,在层次上就比较清晰了。
为了分析
Tracker
类,首先要理解“状态文件”。
l
状态文件:
在第一篇文章中,我们说到,要启动一个
tracker
服务器,至少要指定一个参数,就是状态文件。在
Tracker
的初始化函数中,主要就是读取指定的状态文件,并根据该文件做一些初始化的工作。所以必须弄清楚状态文件的作用:
1.
状态文件的作用:
tracker
服务器如果因为某些意外而停止,那么所有的下载者不仅不能继续下载,而且先前所做的努力都前功尽弃。这种情况是不能容忍的,因此,必须保证在
tracker
重新启动之后,所有的下载者还能继续工作。
Tracker
服务器周期性的将当前系统中必要的下载状态信息保存到状态文件中,在它因故停止,而后又重新启动的时候,可以根据这些信息重新恢复“现场”,从而使得下载者可以继续下载。
2.
状态文件的格式:
状态文件的信息对应着一个比较复杂的
4
级嵌套的字典。
要详细分析这个字典类型,必须理解一点:一个
tracker
服务器,可以同时为下载不同文件的几批下载者提供服务。
我们知道,一批下载同一个文件的下载者,它们必然拥有同样的
torrent
文件,它们能根据
torrent
文件找到同一个
tracker
服务器。而下载另一个文件的一批下载者,必然拥有另外一个
torrent
文件,但是这两个不同的
torrent
文件,可能指向的是同一个
tracker
服务器。所以说“一个
tracker
服务器,可以同时为下载不同文件的几批下载者提供服务。”
实际上,那些专门提供
bt
下载的网站,都是架设了一些专门的
tracker
服务器,每个服务器可以同时为多个文件提供下载跟踪服务。
理解了这一点,我们继续分析状态文件的格式。
第一级字典:
在
Tracker
的初始化函数中,有这样的代码,
if exists(self.dfile):
h = open(self.dfile, 'rb')
ds = h.read()
h.close()
tempstate = bdecode(ds)
else:
tempstate = {}
这段代码是从从状态文件中读取信息,由于读到的是经过
Bencoding 编码后的数据,所以还需要经过解码,解码后就得到一个字典类型的数据,保存到 template 中,这就是第一级字典。
它有两个关键字,
peers
和
completed
,分别用来记录参与下载的
peer
的信息和已经完成了下载的
peer
的信息(凡是出现在
completed
的
peer
,也必然出现在
peers
中)。这两个关键字对应的数据类型都是字典,我们重点分析
peers
关键字所对应的第二级字典。
第二级字典:
关键字:
torrent
文件中
info
部分的
SHA hash
数据:第三级字典
一个被下载的文件,唯一的被一个
torrent
文件标识,
tracker
通过计算
torrent
文件中
info
部分的
SHA hash
,这是一个
20
字节的字符串,它可以唯一标识被下载文件的信息。第二级字典以此字符串作为关键字,保存下载此文件的下载者们的信息。
第三级字典:
关键字:下载者的
peer id
数据:第四级字典
解释:每个下载者,都创建一个唯一标识自己的
20
字节的字符串,称为
peer id
。第三级字典以次为关键字,保存每个下载者的信息。
第四级字典:
关键字:
ip
、
port
、
left
等
数据:分别保存下载者的
ip
地址、端口号和未下载完成的字节数
另外还有两个可选的关键字
given ip
和
nat
,它们是用于
NAT
的,关于
NAT
的情况,后面会再提到。
理解了这个
4
级嵌套的字典,对
Tracker
的分析才好继续进行下去。
下面我们挨个看
Tracker
类的成员函数。
l
初始化函数
__init__()
:
开始是一些参数的初始化,其中比较难理解的有:
self.response_size = config['response_size']
self.max_give = config['max_give']
要理解这两个参数,必须看那份更详细的
BT
协议规范中对“
numwant
”关键字的解释:
·
numwant
:
Optional. Number of peers that the client would like to receive from
the tracker. This is permitted to be zero. If omitted, typically
defaults to 50 peers.
If a client wants a large peer list in the response, then it should specify the numwanted parameter.
意思就是说,默认情况下,
tracker
服务器给下载者响应的
peers
个数是
response_size
个,但有时候,下载者可能希望获得更多的
peers
信息,那么它必须在请求中包含
numwant
关键字,并指定希望获得
peers
的个数。例如是
300
,
tracker
取
300
和
max_give
中较小的一个,作为返回给下载者的
peers
的个数。
self.natcheck = config['nat_check']
self.only_local_override_ip = config['only_local_override_ip']
这两个参数是和
NAT
相关的,我们终于必须要说到
NAT
了。
我们知道,如果一个
BT
客户端处在局域网中,通过
NAT
之后连到
tracker
服务器的话,那么
tracker
服务器从连接中获得的该客户端的
IP
地址是一个公网
IP
,如果其它客户端通过这个
IP
试图连接该客户端的话,肯定会被
NAT
拒绝的。
通过一些
NAT
穿越的技术,在某些情况下,可以让一些客户端穿过
NAT
,与处在局域网中的客户端建立连接,具体的技术资料我已经贴在论坛上了,大家有兴趣可以去看一看。原来我以为
BT
也用到了一些
NAT
穿越技术,但现在发现并没有,可能是技术实现上比较复杂,而且不能保证在任何情况下都有效的原因吧。
我们来看那份比较详细的协议规范中,对“
ip
”关键字的解释:
·
ip
: Optional. The true IP address of the client machine, in dotted quad format. Notes:
In general this parameter is not necessary as the address of the client
can be determined from the IP address from which the HTTP request came.
The parameter is only needed in the case where the IP address that the
request came in on is not the IP address of the client. This happens if
the client is communicating to the tracker through a proxy (or a
transparent web proxy/cache.) It also is necessary when both the client
and the tracker are on the same local side of a NAT gateway. The reason
for this is that otherwise the tracker would give out the internal
(RFC1918) address of the client, which is not routeable. Therefore the
client must explicitly state its (external, routeable) IP address to be
given out to external peers. Various trackers treat this parameter
differently. Some only honor it only if the IP address that the request
came in on is in RFC1918 space. Others honor it unconditionally, while
others ignore it completely.
在客户端发给
tracker
服务器的请求中,可能包含“
ip
”,也就是指定自己的
IP
地址。你可能有疑问了,客户端为什么要通知
tracker
服务器自己的
ip
地址了?
tracker
服务器完全可以从连接中获得这个
ip
啊。嗯,实际的网络情况是非常复杂的,如果客户端是在局域网内通过
NAT
后上网,或者客户端是通过某个代理服务器之后,再与
tracker
服务器建立连接,那么
tracker
从连接中获得的
ip
地址并不是客户端真实的
ip
地址,为了获得真实的
ip
,必须让客户端主动在协议中通知
tracker
。因此,就出现了两个
ip
地址,一个是从连接中获得的
ip
地址,我把它叫做“连接
ip
”,另一个是客户端通过请求传递过来的
ip
,我叫它“真实
ip
”。显然,
tracker
应该把客户端的“真实
ip
”记录下来,并把这个“真实
ip
”通知给其它下载者。
这个“
ip
”参数又是可选的,也就是说,如果客户端拥有一个公网的
ip
,而且并没有通过
NAT
或者代理,那么,它并不需要传递这个参数,“连接
ip
”就是“真实
ip
”。
按协议规发的说法,“
ip
”这个参数在以下两种情况下有用:
1
、客户端可能拥有一个公网
IP
,但它又是通过一个代理服务器与
tracker
服务器建立连接的,它需要传递“
ip
”。
2
、客户端在某个局域网中,恰好
tracker
也在同一个局域网中,。。。(这种情况又会怎么样了?我还没有弄明白
:)
回过头来看
natcheck
和
only_local_override_ip
,
natcheck
:
how many times to check if a downloader is behind a NAT (0 = don't check)
only_local_override_ip
:如果从
GET
参数中传递过来的
ip
,是一个公网
ip
,是否忽略它?它的默认值是
1
。
现在还不好理解它的意思,我们看后面代码的时候,再来理解它。
self.becache1 = {}
self.becache2 = {}
self.cache1 = {}
self.cache2 = {}
self.times = {}
这里出现
5
个字典,其中
times
用来,而其它
4
个字典的作用是什么?
嗯,还是让我们先来看看在“
BT
移植邮件列表”中,
Bram Cohen
发的一个帖子,
There are two new GET parameters for the tracker in the latest release. They are –
key=xxxx
- this is like peer id, but it's only known to the client and the
tracker. It allows clients to be behind dynamic IP. If a peer announced
a key previously, then it's accepted if and only if it gives the same
key again. If no key was given, then the fallback is checking that the
IP hasn't changed. If the IP has changed, mainline currently will give
a peer list but not change any data related to that peer, so that peers
behind dynamic IP using old clients will continue to work okay.
Currently mainline generates the associated with key as eight
random hex s, and the tracker accepts any string from clients.
compact=1
- when a client sends this, the 'peers' return is a single string
whose length is a multiple of 6 rather than a dict. To extract peer
information from the string, chop it into substrings of length 6. For
each substring, the first four bytes are the IP and the last two are
the port, encoded big-endian. This results in huge bandwidth savings.
Everybody developing ports should implement these keys, they're very useful.
-Bram
|
BT
在不停的向前发展,所以协议规范也在发展之中,新引入了两个关键字,其中一个是
compact
,如果客户端请求中
compact=1
,表示紧凑模式,也就是
tracker
给客户端响应的数据,采用一种比原来更紧凑的形式,这样可以有效的节约带宽。
Becache1
和
cache1
用于普通模式,而
becache2
和
cache2
用于紧凑模式。我们马上能看到它们的初始化操作。
if exists(self.dfile):
h = open(self.dfile, 'rb')
ds = h.read()
h.close()
tempstate = bdecode(ds)
else:
tempstate = {}
if tempstate.has_key('peers'):
self.state = tempstate
else:
self.state = {}
self.state['peers'] = tempstate
self.downloads = self.state.setdefault('peers', {})
self.completed = self.state.setdefault('completed', {})
statefiletemplate(self.state)
这部分代码是读取状态文件,初始化
downloads
和
completed
这两个字典,并检查读取的数据是否有效。
现在,
downloads
里面是保存了所有下载者的信息,而
completed
保存了所有完成下载的下载者的信息。
for x, dl in self.downloads.items():
self.times[x] = {}
for y, dat in dl.items():
self.times[x][y] = 0
if not dat.get('nat',1):
ip = dat['ip']
gip = dat.get('given ip')
if gip and is_valid_ipv4(gip) and (not self.only_local_override_ip or is_local_ip(ip)):
ip = gip
self.becache1.setdefault(x,{})[y] = Bencached(bencode({'ip': ip, 'port': dat['port'], 'peer id': y}))
self.becache2.setdefault(x,{})[y] = compact_peer_info(ip, dat['port'])
这里,对
times
、
becache1
、
becache2
初始化。它们都是
2
级嵌套的字典,第一级的关键字是
torrent
文件中的
info
部分的
hash
,第二级关键字是下载者的
peer id
,
becache1
保存的是一个
Bencached
对象,而
becache2
保存的是一个字符串,它是把
ip
和
port
组合成的一个字符串。
参数设置完之后,有:
rawserver.add_task(self.save_dfile, self.save_dfile_interval)
add_task()
我们已经见到过好多次了,这表示每隔一段时间,需要调用
save_dfile()
来保存状态文件。
再后面的代码,我没有仔细看了,象
allow_get
和
allowed_dir
等的意义,还需要看相关的代码才能明白,如果你仔细看了这些部分,希望能补充一下。
初始化以后,就是
Tracker
的最重要,也是代码最长的函数:
get()
。
l
get()
:
在第三篇文章中,我们已经看到,在由
HTTPHandler
对
track HTTP
协议进行第一层分析之后,就是调用
Tracker::get()
来进行第二层分析的。它的参数是
URL
和
HTTP
消息头。
在这个函数中,首先调用
urlparse()
对
URL
进行解析,例如这样的
URL
:
/announce?ip=192.168.112.1&port=9999&left=2000
解析之后,就获得了
path
,是
announce
,还有参数,包括:
ip
:
192.168.112.1
port
:
9999
left
:
2000
然后,根据
path
的不同,分别处理。
一般来说,客户端发给
tracker
的请求中,
path
都是
announce
,但有时候,第三方可能也想查询一下
tracker
服务器的状态,那么它可以通过其它的
path
来向
tracker
服务器请求,例如
scrape
。在一些专门提供
bt
下载的网站上,我们可以看到不停更新的下载者、种子个数等信息,就是用这种方式从
tracker
服务器处获得的。
我们只看
path
是
announce
的情况。
首先是对客户端传递来的参数的有效性进行检查,包括是不是有
info_hash
关键字?
ip
地址是否合法等等。
然后,
ip = connection.get_ip()
这样得到的
ip
,是根据客户端与
tracker
服务器建立的连接中获取的
ip
,就是“连接
ip
”了。
接下来,
ip_override = 0
if params.has_key('ip') and is_valid_ipv4(params['ip']) and (not self.only_local_override_ip or is_local_ip(ip)):
ip_override = 1
这段代码的意图,是为了判断在随后保存客户端的
ip
地址的时候,是否要用“真实
ip
”来取代“连接
ip
”。如果
ip_override
为
1
,那么就保存“真实
ip
”,也就是“连接
ip
”被“真实
ip
”覆盖(
override
)了。
分析源码的过程其实就是揣测作者意图的过程,我的揣测是这样的:
如果客户端从请求中传递了“真实
ip
”,那么对
tracker
来说,,既然客户端都已经报告了“真实
ip
”了,那么当然就保存“真实
ip
”就好了。可如果“真实
ip
”是个公网
ip
,而且
only_local_override_ip=1
,也就是说,忽略“真实
ip
”为公网
ip
的情况,那么,保存的是“连接”
ip
。
说句实话,为什么要设置
only_local_override_ip
这么一个参数,我还是没有弄明白。
if peers.has_key(myid):
myinfo = peers[myid]
if myinfo.has_key('key'):
if params.get('key') != myinfo['key']:
return (200, 'OK', {'Content-Type': 'text/plain', 'Pragma': 'no-cache'},
bencode({'failure reason': 'key did not match key supplied earlier'}))
confirm = 1
elif myinfo['ip'] == ip:
confirm = 1
else:
confirm = 1
这段代码涉及到身份验证吧,我没有仔细看了,关于
“
key
”的解释,请看上面
Bram Cohen
的帖子。
接下来,如果验证通过,而且事件不是“
stopped
”,那么就把客户端的信息保存下来。如果已经存在该客户端的信息,那么就更新一下。注意这里
ip_override
派上了用场,也就是如果覆盖,那么保存的是“真实
ip
”,否则保存的是“连接
ip
”。
if port == 0:
peers[myid]['nat'] = 2**30
elif self.natcheck and not ip_override:
to_nat = peers[myid].get('nat', -1)
if to_nat and to_nat < self.natcheck:
NatCheck(self.connectback_result, infohash, myid, ip, port, self.rawserver)
else:
peers[myid]['nat'] = 0
第一个
port == 0
的情况,不知道是什么意思?
第二个表示要检查
NAT
的情况。大概意思就是
tracker
服务器主动用
BT
对等协议与该客户端进行握手,如果握手成功,那么说明该客户端是可以被直接连接的。这一点很重要,如果
tracker
服务器无法和客户端直接建立连接的话,那么其它下载者也无法和该客户端建立连接。
这里用到的
NatChecker
类,也是一个
Handler
类,具体细节,大家自己分析吧。
data = {'interval': self.reannounce_interval}
从这到最后,就是根据紧凑模式和普通模式两种不同情况,分别从
becache1
或者
becache2
中,返回随机的
peers
的信息。
在这里,我们来总结一下
cache1
、
becache1
、
cache2
、
becache2
的用处。我感觉
cache1
和
cache2
好像没什么作用,因为从代码中没有看到它们两的意义。
Becache1
和
becache2
则分别用于普通模式和紧凑模式情况下,对
peers
的信息进行缓存。它们从状态文件中初始化自己;如果有新的
peer
出现,被添加到这两个缓存中;如果是“
stopped
”事件,那么从缓存中删除对应的
peer
。最后,
tracker
根据情况,从其中一个缓存取得随机的
peers
的信息,返回给客户端。
l
connectback_result()
这个函数,用于
NatCheck
类作为回调函数。它根据
tracker
服务器主动与客户端建立连接的结果做一些处理。其中的参数
result
,是表示
tracker
与客户端建立连接是否成功。如果建立成功,显然对方不在
NAT
后面,否则就是在
NAT
后面了。
record['nat'] += 1
这没看懂,为什么不是直接
record['nat'] = 1
?最后,如果建立连接成功,那么更新一下
becache1
和
becache2
。
posted on 2007-01-19 00:18
苦笑枯 阅读(443)
评论(0) 编辑 收藏 所属分类:
P2P