本篇文章分析
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
苦笑枯 閱讀(455)
評論(0) 編輯 收藏 所屬分類:
P2P