概要
上一節我們分析了BT客戶端與tracker之間的通信過程。通過與 tracker 的通信,客戶端獲得了參與下載的其它peers
的列表。有了這些 peers
的信息,客戶端就可以主動向它們發起連接,為進一步從它們那里獲取所需要的文件片斷做好準備。這些連接稱為“主動連接”。因為連接的發起方是客戶端自己。
同時,每個客戶端在啟動以后,都會監聽某個端口,用于接受其它 peers
的連接請求。P2P的核心理念就是“公平、對等”,你向別人有所求(下載),就必須有付出(上傳)。客戶端在接受外來的連接請求之后,就會產生一個新的連
接,稱之為“被動連接”,因為連接的發起方是其它 peer。
無論是被動連接,還是主動連接,一旦連接建立之后,雙方就要進行“BT對等協議”的握手。握手成功之后,雙方才可以通過這個連接互相傳遞消息了。為什么要進行握手了?主要目的是為了防止一些錯誤的連接。這就好比地下黨接頭,暗號對上了,彼此才建立信任。
在這個示意圖中,客戶端 A 與 其它 peers B、C、D、E 都建立了連接。通過箭頭來表示連接建立的方向。A主動與 B、D、E 建立連接;A被動接收C的連接。同時C與D、E與D、B與D之間也都有連接。這樣,所有下載者之間就形成了一個網狀的結構。
同時,這些下載者都要與 tracker 之間不斷通信。
無論是被動連接,還是主動連接,一旦在“BT對等握手”成功之后,它們就沒有任何區別了。下載通過這個連接進行,上傳也是通過這個連接進行。
本文重點分析BT客戶端如何主動向其它 peers 發起連接;BT客戶端如何被動接收其它 peers 的連接請求;以及在連接建立成功之后,如何進行BT對等協議握手的過程。
客戶端主動發起連接
【Encrypter.py】
上一節的最后,我們看到調用 Encoder::start_connection() 來向其它 peer 發起連接。所以,從這里開始看起。
class Encoder:
# start_connection() 需要兩個參數:
dns:對方的ip、port 的組合。
id: 對方的 id。每個BT客戶端都要創建一個唯一的 id 號,并且把這個 id 號報告給 tracker。Id號唯一標識了一個客戶端。Id 號的計算方式在 download.py 中。
myid = 'M' + version.replace('.', '-')
myid = myid + ('-' * (8 - len(myid))) + b2a_hex(sha(repr(time()) + ' ' +
str(getpid())).digest()[-6:])
seed(myid)
①def start_connection(self, dns, id):
if id:
# 如果 id 是自己,不連接
if id == self.my_id:
return
# 如果已經和這個peer建立了連接,也不再建立連接。
for v in self.connections.s():
if v.id == id:
return
# 如果當前連接數過多,暫時不發起連接
if len(self.connections) >= self.max_initiate:
# self.spares 起到緩存的作用。在當前連接數過多的情況下,把本次要連接的 peer 的 ip、port 緩存起來。一旦當前連接數小于設定值 max_initiate, 則可以從 spares 中取出備用的 peers。
if len(self.spares) < self.max_initiate and dns not in self.spares:
self.spares.append(dns)
return
try:
# 調用 RawServer::start_connection(),發起 TCP 的連接。RawServer的代碼分析請參看“服務器源碼分析”系列文章,不再贅述
# 返回的 c 是一個 SingleSocket 對象,它封裝了 socket 句柄
# 如果出錯,拋出 socketerror 異常
c = self.raw_server.start_connection(dns)
# 成功建立 TCP 連接。構造一個 Connection對象,加入 connections 字典中。注意,最后一個參數是 True,它表面這條連接是由客戶端主動發起建立的。我們立刻去看 Connection 類的構造
self.connections[c] = Connection(self, c, id, True)
except socketerror:
pass
【Encrypter.py】
class Connection:
② def __init__(self, Encoder, connection, id, is_local):
self.encoder = Encoder
self.connection = connection #這個 connection 是 SingleSocket 對象,就是上面
RawServer::start_connection() 返回的值,它封裝了對socket句柄的操作。名字起的不好,容易弄混淆。
self.id = id
self.locally_initiated = is_local #這個連接是否由本地發起?
self.complete = False
self.closed = False
self.buffer = StringIO()
self.next_len = 1
⑦self.next_func = self.read_header_len
# 如果由本地發起,那么給對方發送BT對等連接的握手消息。
④if self.locally_initiated:
connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id)
if self.id is not None:
connection.write(self.encoder.my_id)
客戶端被動接受外來連接
客
戶端在與 tracker 通信的時候,已經把自己的 ip 和監聽的 port 報告給了 tracker。這樣,其它 peers 就可以通過這個
ip 和 port 來連接它了。例如上圖中的C,就主動給 A 發一個連接,從C的角度來說,它是“主動連接”,但從A的角度,它是“被動連接”。
一旦有外來連接請求,就會調用 RawServer::handle_events(),下面是摘錄的“Tracker 服務器源碼分析之二:RawServer類”中的一段。
最
后調用的是 Handle 的 external_connection_made(),對客戶端來說,這個Handle 是 Encoder
類對象。所以,外來連接建立成功后,最后調用的是 Encoder:: external_connection_made():
③def external_connection_made(self, connection):
# 同樣是創建一個新的 Connection 類,并加入 connections 字典中。但不同之處在于最后一個參數是 False,表明這個連接是由外部發起的。
self.connections[connection] = Connection(self, connection, None, False)
BT對等連接握手:第一步
如果是主動連接,那么一旦連接建立成功之后,就給對方發送一個握手消息,我們折回去看序號為 4 的代碼:
if self.locally_initiated:
connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id)
if self.id is not None:
connection.write(self.encoder.my_id)
在《BT協議規范》中,如此描述握手消息:
對等協議由一個握手開始,后面是循環的消息流,每個消息的前面,都有一個數字來表示消息的長度。握手的過程首先是先發送19,然后發送協議名稱“BitTorrent protocol”。19就是“BitTorrent protocol”的長度。
后續的所有的整數,都采用big-endian 來編碼為4個字節。
在協議名稱之后,是8個保留的字節,這些字節當前都設置為0。
接下來對元文件中的 info 信息,通過 sha1 計算后得到的 hash值,20個字節長。接收消息方,也會對 info 進行一個 hash 運算,如果這兩個結果不一樣,那么說明雙方要下載的文件不一致,會切斷連接。
接下來是20個字節的 peer id。
可
以看到,最后兩項就是 Encoder::download_id 和
Encoder::my_id。download_id是如何得來的?它是首先對 torrent 文件中 info 關鍵字所包含的信息進行
Bencoding 方式的編碼(請看《BT協議規范》關于 Bencoding的介紹),然后再通過 sha 函數計算出的
hash(摘要)值。(相關代碼都在 download.py 中)。在一次下載過程中,所有的下載者根據 torrent 文件計算出的這個
download_id應該都是一樣的,否則就說明不處于同一個下載過程中。
至于 peer_id,可以看出是可選的。它的計算方式也在 download.py 中:
myid = 'M' + version.replace('.', '-')
myid = myid + ('-' * (8 - len(myid))) + b2a_hex(sha(repr(time()) + ' ' + str(getpid())).digest()[-6:])
seed(myid)
它用來唯一標識一個 peer。
握手過程已經完成了第一步,還需要第二步,接收到對方的握手消息,握手過程才算完成。所以接下去看在有數據到來的時候,是如何處理的。
BT對等連接握手:第二步
當TCP連接上有數據到來的時候, RawServer 會調用到 Encoder:: data_came_in()
【Encoder】
⑤def data_came_in(self, connection, data):
self.connections[connection].data_came_in(data)
進一步調用 Connection::data_came_in()
【Connection】
⑥def data_came_in(self, s):
#這個循環處理用來對BT對等連接中的消息進行分析
while True:
if self.closed:
return
i = self.next_len - self.buffer.tell()
if i > len(s):
self.buffer.write(s)
return
self.buffer.write(s[:i])
s = s[i:]
m = self.buffer.get()
self.buffer.reset()
self.buffer.truncate()
try:
x = self.next_func(m) #調用消息分析函數,第一個被調用的是read_header_len
except:
self.next_len, self.next_func = 1, self.read_dead
raise
if x is None:
self.close()
return
self.next_len, self.next_func = x
⑧def read_header_len(self, s):
# 協議的長度是否為 19?
if ord(s) != len(protocol_name):
return None
return len(protocol_name), self.read_header # 下一個處理函數
def read_header(self, s):
# 協議名稱是否是“BitTorrent protocol”?
if s != protocol_name:
return None
return 8, self.read_reserved # 下一個處理函數
def read_reserved(self, s):
#8個保留字節
return 20, self.read_download_id # 下一個處理函數
def read_download_id(self, s):
對方 download_id 和自己計算出的是否一致?
if s != self.encoder.download_id:
return None
檢查完 download_id,就認為對方已經通過檢查了。
這里很關鍵!!!,需要仔細體會。這實際上就是在被動連接情況下,完成握手過程的處理。如果連接不是由本地發起的(被動接收到一個握手消息),那么給對方回一個握手消息。這里的握手消息發送處理和第一步是一樣的
if not self.locally_initiated:
self.connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id + self.encoder.my_id)
return 20, self.read_peer_id #下一個處理函數
def read_peer_id(self, s):
# Connection 類用來管理一個 BT 對等連接。在握手完成之后,就用對方的 peer_id 來唯一標識這個 Connection。這個值被保存在 self.id 中。顯然,在握手完成之前,這個 id 還是空值。
if not self.id:
# 對方的peer_id 可千萬別跟自己的一樣
if s == self.encoder.my_id:
return None
唔,如果 peer_id 已經收到過,也不繼續下去了
for v in self.encoder.connections.s():
if s and v.id == s:
return None
用對方的 peer_id 為 self.id 賦值,唯一標識這個 Connection
self.id = s
if self.locally_initiated:
self.connection.write(self.encoder.my_id)
else:
self.encoder.everinc = True
else:
if s != self.id:
return None
# OK,握手完成!!!
self.complete = True
self.encoder.connecter.connection_made(self)
return 4, self.read_len #下一個處理函數。從此以后,就是對其它BT對等消息的處理過程了。這是我們下一節分析的問題。
小結:
這篇文章重點分析了BT客戶端主動發起連接和被動接收連接的過程,以及在這兩種情況下,如何進行BT對等握手的處理。
在BT對等握手完成之后,連接的雙方就可以互相發送BT對等消息了,這是下一節的內容。
posted on 2007-01-19 00:24
苦笑枯 閱讀(649)
評論(0) 編輯 收藏 所屬分類:
P2P