BitTorrent 是一個(gè)用于文件分發(fā)的協(xié)議。它通過 URL 來標(biāo)識內(nèi)容,其設(shè)計(jì)使其可以與 Web 無縫集成。BitTorrent 相對于 HTTP 的優(yōu)勢在于,當(dāng)相同文件的多個(gè)下載并行進(jìn)行時(shí),下載者之間還可以互傳數(shù)據(jù),這就使得文件源在僅增加少量負(fù)載的情況下支持?jǐn)?shù)量眾多的下載成為可能。
BitTorrent 文件分發(fā)由以下部分組成
- 一個(gè)普通的 Web 服務(wù)器
- 一個(gè)靜態(tài)的 'metainfo' 文件
- 一個(gè) BitTorrent tracker
- 一個(gè) '原始的' 下載者
- 終端用戶 Web browsers
- 終端用戶下載者
理想情況下,單個(gè)文件有多個(gè)終端用戶在下載。
要提供 BitTorrent 下載服務(wù),主機(jī)需要執(zhí)行如下步驟
- 啟動(dòng)運(yùn)行一個(gè) Tracker。
- 啟動(dòng)運(yùn)行一個(gè)普通的 Web 服務(wù)器,比如 apache。
- 在他們的 Web 服務(wù)器上將擴(kuò)展名 .torrent 和 mimetype application/x-bittorrent 關(guān)聯(lián)起來。
- 使用要發(fā)布的完整文件和 Tracker 的 URL 生成一個(gè) metainfo(.torrent)文件。
- 將 metainfo 文件放到 Web 服務(wù)器上。
- 將 metainfo(.torrent)文件的鏈接放到其它的 Web 頁面里。
- 啟動(dòng)一個(gè)已經(jīng)有了完整文件的下載器('origin')。
要啟動(dòng)下載,用戶需執(zhí)行如下步驟
- 安裝 BitTorrent。
- 瀏覽 Web。
- 點(diǎn)擊 .torrent 文件的鏈接。
- 選擇保存文件的本地路徑,或者選擇一個(gè)已經(jīng)部分下載的文件恢復(fù)下載。
- 等待下載完成。
- 退出下載器(它會(huì)持續(xù)上傳數(shù)據(jù)直到退出)。
B編碼
- 字符串表示為,十進(jìn)制的長度前綴,后跟冒號和字符串本身。比 4:spam 表示 spam。
- 整數(shù)表示為,i 后跟十進(jìn)制數(shù)字,然后是一個(gè) e 字符表示結(jié)束。比如 i3e 表示 3,i-3e 表示 -3。整數(shù)沒有大小限制。i-0e 是無效的。所有以 0 為前導(dǎo)的編碼,比如 i03e,都是無效的,除了 i0e,而它當(dāng)然表示 0。
- 列表被編碼為,l 后跟它們的元素(也是 B 編碼的),然后是 e 字符表示結(jié)束。比如 l4:spam4:eggse 表示 ['spam', 'eggs']。
- 字典編碼為,d 后交替地跟著鍵和它們的對應(yīng)值,然后是 e 字符表示結(jié)束。比如 d3:cow3:moo4:spam4:eggse 表示 {'cow': 'moo', 'spam': 'eggs'},而 d4:spaml1:a1:bee 表示 {'spam': ['a', 'b']}。鍵必須是字符串,且順序排列(以原始串排序,而不是字母順序)。
metainfo 文件
Metainfo 文件(即 .torrent 文件)是 B 編碼的字典,它具有如下的鍵:
announce
- Tracker 的 URL。
info
- 它映射到一個(gè)字典,下面會(huì)描述它的鍵。
一個(gè) .torrent 文件中所有包含文本的字符串必須以 UTF-8 編碼。
info 字典
name 鍵映射為一個(gè) UTF-8 編碼的字符串,它建議的保存文件(或字典)時(shí)所采用的名字。這純粹是建議性的。
piece length 映射為文件分割后每個(gè)片的字節(jié)數(shù)。為了便于數(shù)據(jù)傳輸,文件被分割為固定大小的片,它們具有相同的長度,除了最后的文件片可能由于截?cái)喽煌狻?code>piece length 幾乎總是 2 的冪,最常見的是 2^18 = 256K(3.2 版之前的 BitTorrent 版本使用 2^20 = 1M 作為默認(rèn)值)。
pieces 映射為長度是 20 的整數(shù)倍的字符串。它將被細(xì)分為長度為 20 的字符串,其中每個(gè)都是對應(yīng) index 處的片的 SHA1 hash 值。
還有一個(gè)鍵 length 或鍵 files,但不會(huì)都有或都沒有。如果是 length,表示下載是單個(gè)文件,否則表示下載是目錄結(jié)構(gòu)中的一系列文件。
在單個(gè)文件的情況中,length 映射為文件的字節(jié)長度。
出于其他鍵的目的,多文件的情況僅被看作將 files 列表中出現(xiàn)的文件順序連接形成的單個(gè)文件。文件列表是 files 映射的值,它是包含如下鍵的字典的列表:
length - 文件的長度,以字節(jié)為單位。
path - UTF-8 編碼的字符串的列表,對應(yīng)于子目錄的名字,它的最后部分是實(shí)際的文件名(長度為 0 的列表是錯(cuò)誤情況)。
在單個(gè)文件的情況中,name 鍵是文件名,在多文件的情況中,它是目錄名。
注:
用 JSON 來表示 .torrent 文件使我們可以對這種文件格式的結(jié)構(gòu)有更清晰的認(rèn)識。單個(gè)文件的 .torrent 文件結(jié)構(gòu)如下:
{
"announce":"xxxx",
"info":{
"name":"xxxx",
"piece length":"xxxx",
"pieces":"xxxx",
"length":"xxxx"
}
}
多個(gè)文件的 .torrent 文件結(jié)構(gòu)如下:
{
"announce":"xxxx",
"info":{
"name":"xxxx",
"piece length":"xxxx",
"pieces":"xxxx",
"files":[
{
"length":"xxxx",
"path":"xxxx"
},
{
"length":"xxxx",
"path":"xxxx"
},
{
"length":"xxxx",
"path":"xxxx"
}
]
}
}
trackers
Tracker GET 請求具有如下的鍵:
info_hash
是
metainfo文件中info值的 B 編碼形式的 20 字節(jié) SHA1 哈希。這個(gè)值幾乎肯定必須被轉(zhuǎn)義。注意,這是
metainfo文件的一個(gè)子串。info-hash必須是.torrent文件中找到的編碼形式的哈希,這與對metainfo文件進(jìn)行 B 解碼,提取info字典并對其進(jìn)行編碼相同,當(dāng)且僅當(dāng) B 解碼器完全驗(yàn)證輸入之后(比如鍵的順序,沒有前導(dǎo) 0)。相反,這意味著客戶端必須拒絕無效的metainfo文件或直接提取子串。它們不得對無效數(shù)據(jù)執(zhí)行解碼-編碼循環(huán)。
peer_id
- 一個(gè)下載器用作其 ID 的長度為 20 的字符串。每個(gè)下載器在新下載開始時(shí)隨機(jī)生成它自己的 ID。這個(gè)值也幾乎肯定必須被轉(zhuǎn)義。
ip
- 一個(gè)可選的參數(shù),它給出了這個(gè) Peer 的 IP 地址(或域名)。如果它與 tracker 位于相同機(jī)器的話,通常被用作源。
port
- 當(dāng)前 Peer 監(jiān)聽的端口號。下載器的常見行為是,嘗試監(jiān)聽端口 6881,但如果那個(gè)端口被占用則嘗試端口 6882,然后是 6883,等等,直到 6889 之后放棄。
uploaded
- 到目前為止上傳的數(shù)據(jù)量,以十進(jìn)制 ascii 編碼。
downloaded
- 到目前為止下載的數(shù)據(jù)量,以十進(jìn)制 ascii 編碼。
left
- 當(dāng)前 Peer 仍然需要下載的字節(jié)數(shù),以十進(jìn)制 ascii 編碼。注意,這個(gè)值不能用文件的長度和已經(jīng)下載的數(shù)據(jù)量來計(jì)算,因?yàn)樗赡苁腔謴?fù)的下載,而且,也有可能下載回來的一些數(shù)據(jù)在完整性檢查時(shí)失敗,而不得不重新下載。
event
- 這是一個(gè)可選的鍵,它映射為
started,completed,或stopped(或empty,與沒有這個(gè)鍵一樣)。如果不存在,則這是定期進(jìn)行的公告請求之一。使用started的公告在下載首次啟動(dòng)時(shí)發(fā)送,使用completed的將在下載完成時(shí)發(fā)送。如果啟動(dòng)時(shí)文件已經(jīng)完成則不會(huì)發(fā)送completed。當(dāng)停止下載時(shí)下載器發(fā)送一個(gè)使用stopped的公告。
注:看了半天也沒看懂,到底如何計(jì)算 info_hash。ip 字段的用作源到底是什么意思?是用作數(shù)據(jù)源么?
由此可見,BitTorrent 客戶端向 Tracker 服務(wù)器發(fā)送的請求數(shù)據(jù),如果以 JSON 格式表示的話,看起來可能像下面這樣:
{
"info_hash":"xxx",
"peer_id":"xxx",
"ip":"xxx",
"port":1234,
"uploaded":5678,
"downloaded":6789,
"left":12345,
"event":"xxx"
}
但具體是什么樣的結(jié)構(gòu)化數(shù)據(jù)表示格式,目前還看不出來。
Tracker 的響應(yīng)也是 B 編碼的字典。如果 Tracker 的響應(yīng)包含 failure reason 鍵,則它是解釋查詢失敗的原因的可讀字符串,且不會(huì)再有其它鍵。否則,它必須包含兩個(gè)鍵:interval,它是下載器在兩次正常的重新請求之間應(yīng)該等待的秒數(shù),和 peers。peers 是字典的列表,即網(wǎng)絡(luò)中的 Peer 信息,列表中的每一項(xiàng)都包含鍵 peer id,ip 和 port,它們分別是 Peer 為自己選擇的 ID,字符串形式的 IP 地址或域名,和端口號。注意,如果事件發(fā)生或它們需要更多 Peers,則下載器可能在非調(diào)度時(shí)間進(jìn)行重請求。
更常見的是,Trackers 返回 Peer 列表的兼容表示,參見 BEP 23。
注:
Tracker 服務(wù)器的響應(yīng)數(shù)據(jù)的格式,說明的還是比較清晰的,即 B 編碼的字典。響應(yīng)數(shù)據(jù)的格式分為兩種,分別是請求失敗時(shí)的響應(yīng),和請求成功時(shí)的響應(yīng)。請求失敗時(shí)的響應(yīng)格式,如果用 JSON 格式來表示的話,大概看起來像這樣:
{
"failure reason":"xxx"
}
請求成功時(shí),響應(yīng)中包含了正在下載相同資源的其它節(jié)點(diǎn)的信息,如果用 JSON 格式來表示這種響應(yīng)的話,大概看起來像這樣:
{
"interval":12345,
"peers":[
{
"peer id":"xxxxxx",
"ip":"xxxxxx",
"port":1234
},
{
"peer id":"xxxxxx",
"ip":"xxxxxx",
"port":1234
},
{
"peer id":"xxxxxx",
"ip":"xxxxxx",
"port":1234
}
]
}
如果你想對 metainfo 文件或 Tracker 查詢做擴(kuò)展,請與 Bram Cohen 合作來確認(rèn)所有擴(kuò)展的兼容性。
通過 UDP Tracker 協(xié)議通告也很常見。
注:
由上面的內(nèi)容不難理解,Tracker 服務(wù)器 BitTorrent 網(wǎng)絡(luò)中的中心化服務(wù)器,主要用于管理網(wǎng)絡(luò)中的節(jié)點(diǎn)。
Peer 協(xié)議
BitTorrent 的 Peer 協(xié)議操作基于 TCP 或 uTP。
Peer 連接是對稱的。消息在兩個(gè)方向上的發(fā)送看起來是一樣的,數(shù)據(jù)可在任一方向上流動(dòng)。
Peer 協(xié)議通過 metainfo 文件中所描述的索引引用文件的片段,索引從零開始。當(dāng) Peer 完成一個(gè)片的下載,并檢查了哈希值的匹配性,它將向它所有的 Peers 通告它具有該片。
連接在任一端包含兩個(gè)狀態(tài)位:阻塞或不阻塞,以及是否感興趣。(注:即與對端的數(shù)據(jù)通道是否暢通,以及對端是否具有自己需要的數(shù)據(jù)。)阻塞是一種通知,在阻塞解除之前將不會(huì)發(fā)送任何數(shù)據(jù)。阻塞背后的原因和常見技術(shù)將在本文后面解釋。
數(shù)據(jù)傳輸發(fā)生在一端需要數(shù)據(jù)而另一端沒有阻塞的時(shí)候。感興趣狀態(tài)必須隨時(shí)保持更新 - 當(dāng)下載者無需在對端處于非阻塞狀態(tài)的話,就向?qū)Χ苏埱髷?shù)據(jù)的時(shí)候,它們必須表達(dá)缺乏興趣,盡管正在被阻塞。正確實(shí)現(xiàn)這一點(diǎn)很棘手,但是下載者可以知道哪些 Peers 會(huì)在解除阻塞時(shí)立即開始下載。
連接開始時(shí)處于阻塞且不感興趣狀態(tài)。
當(dāng)傳輸數(shù)據(jù)時(shí),下載者應(yīng)該一次在隊(duì)列中保持多個(gè)片的請求以獲得較好的 TCP 性能(這被稱為 'pipelining')。另一方面,無法立即寫入 TCP 緩沖區(qū)的請求應(yīng)該放進(jìn)內(nèi)存隊(duì)列中,而不是應(yīng)用級的網(wǎng)絡(luò)緩沖區(qū)中,以使它們在阻塞發(fā)生時(shí)可以被扔掉。(注:內(nèi)存緩沖區(qū)和應(yīng)用級緩沖區(qū)到底分別指什么?)
Peer 線協(xié)議由握手消息及其后永不停止的長度前綴消息流組成。握手消息由字符 9 (十進(jìn)制)及其后面的字符串 BitTorrent protocol 構(gòu)成。前導(dǎo)字符是長度前綴,希望其他新協(xié)議可以做同樣的事情,因此可以在很小的程度上相互區(qū)分。
協(xié)議中后續(xù)發(fā)送的整數(shù)都以四字節(jié)大尾端編碼。
固定頭部之后是 8 個(gè)保留字節(jié),在當(dāng)前所有的實(shí)現(xiàn)中它們都是 0。如果你想使用這些字節(jié)擴(kuò)展協(xié)議,請與 Bram Cohen 合作,以確保所有擴(kuò)展可以兼容地完成。
接下來是 metainfo 文件中 info 值的 B 編碼形式的 20 字節(jié) SHA1 哈希。(這個(gè)值與向 Tracker 服務(wù)器公告的 info_hash 相同,只是在這里它是原始的而不是在這里引用的)。如果兩端沒有發(fā)送相同的值,則切斷連接。一個(gè)可能的例外是,如果下載者想要通過單個(gè)端口執(zhí)行多個(gè)下載,則他們可能會(huì)等待傳入連接首先提供下載哈希,并且如果它在列表中的話則以相同的哈希響應(yīng)。
下載哈希后面的是 20 字節(jié)的 Peer ID,該 Peer ID 由 Tracker 請求報(bào)告,并包含在 Tracker 響應(yīng)的 Peer 列表中。如果接收端的 Peer ID 與發(fā)起端的期待不匹配,則它切斷連接。
這就是握手消息,接下來是長度前綴和消息的交替流。長度為 0 的消息是 keepalives,它們會(huì)被忽略。Keepalives 通常每 2 分鐘發(fā)送一次,但是注意,在預(yù)期數(shù)據(jù)時(shí)可以更快地完成超時(shí)。
Peer 消息
所有的非 keepalive 消息以一個(gè)標(biāo)識消息類型的字節(jié)開始。
可能的值為:
- 0 - choke
- 1 - unchoke
- 2 - interested
- 3 - not interested
- 4 - have
- 5 - bitfield
- 6 - request
- 7 - piece
- 8 - cancel
choke,unchoke,interested,和 not interested 沒有載荷。
'bitfield' 只作為第一條消息發(fā)送。它的載荷是一個(gè)位域,其中下載器已經(jīng)發(fā)送的每個(gè)索引設(shè)置為 1,其它設(shè)置為 0。沒有任何東西的下載器可以跳過 bitfield 消息。位域的第一個(gè)字節(jié)從高位到低位分別對應(yīng)于索引 0 - 7。接下來的一個(gè)是 8 - 15,等等。最后一個(gè)字節(jié)中多余的位設(shè)置為 0。
have 消息的載荷是一個(gè)數(shù)字,是下載器剛剛完成并檢查了哈希值的索引。
request 消息包含索引,起始位置,和長度。后兩個(gè)是字節(jié)偏移量。長度通常是 2 的冪,除非它被文件尾截?cái)唷K挟?dāng)前的實(shí)現(xiàn)使用 2^14 (16 kiB),當(dāng)請求比這個(gè)值更大數(shù)量的數(shù)據(jù)時(shí),連接關(guān)閉。
cancel 消息具有與 request 消息相同的載荷。它們通常僅在下載結(jié)束時(shí)發(fā)送,稱為“結(jié)束游戲模式”。當(dāng)下載快結(jié)束時(shí),最后一些數(shù)據(jù)片傾向于從單個(gè)調(diào)制解調(diào)器線上下載,這需要很長時(shí)間。為了確保最后的數(shù)據(jù)片快速地到達(dá),一旦特定下載器還沒有下載到的所有數(shù)據(jù)片的請求都處于掛起狀態(tài),則它向當(dāng)前正從其下載的每個(gè) Peer 請求所有的數(shù)據(jù)。為了防止這種情況變得特別低效,每次數(shù)據(jù)片到達(dá)時(shí)它都向其它 Peer 發(fā)送 cancel。
piece 消息包含索引,起始位置,和數(shù)據(jù)片。注意,它們隱式地與請求消息相關(guān)聯(lián)。如果 choke 和 unchoke 消息快速連續(xù)地發(fā)送和/或傳輸速度非常慢的話,可能會(huì)有未預(yù)期的數(shù)據(jù)片到達(dá)。
下載器通常以隨機(jī)順序下載數(shù)據(jù)片,這樣可以很好地防止它們擁有任何 Peers 的片的嚴(yán)格子集或超集。
Choking 有幾個(gè)原因。當(dāng)一次性通過多個(gè)連接發(fā)送時(shí),TCP 擁塞控制的表現(xiàn)很差。而且,choking 讓每個(gè) Peer 使用 tit-for-tat-ish 算法以確保它們獲得一致的下載速率。
下面描述的 choking 算法正是當(dāng)前部署的。所有新算法在完全由他們自己組成的網(wǎng)絡(luò)中,以及在主要由這個(gè)算法組成的網(wǎng)絡(luò)中,都能很好地工作是非常重要的。
一個(gè)好的 choking 算法應(yīng)該滿足一些標(biāo)準(zhǔn)。它應(yīng)該限制并發(fā)上傳的數(shù)量以獲得良好的 TCP 性能。它應(yīng)該避免快速地 choking 和 unchoking,這被稱為 抖動(dòng)。它應(yīng)該回應(yīng)讓它下載的 Peer。最后,它應(yīng)該偶爾嘗試使用未使用的連接,以確定它們是否可能比當(dāng)前使用的更好,這稱為樂觀的 unchoking。
當(dāng)前部署的 choking 算法通過僅改變每隔 10 秒鐘 choked 一次的 Peer 避免了抖動(dòng)。它通過解除對其具有最佳下載速率并且感興趣的四個(gè) peers 來實(shí)現(xiàn)往復(fù)和上傳數(shù)量的限制。擁有更高上傳率但不感興趣的 peer 會(huì)被 unchoked,如果它們變得被感興趣則最糟糕的上傳者被 choked。如果下載者擁有一個(gè)完整的文件,它使用它的上傳速率而不是它的下載速率來決定誰 unchoke。
對于樂觀的 unchoking,在任何時(shí)間都有一個(gè)單獨(dú)的 peer,它處于 unchoked 狀態(tài)而無論它的上傳速率是多少(如果感興趣,它被作為四個(gè)允許的下載者中的一個(gè))。哪個(gè) peer 是樂觀的 unchoked 每隔 30 秒輪轉(zhuǎn)一次。為了給他們一個(gè)很好的機(jī)會(huì)獲得一個(gè)完整的片段上傳,新的連接的開始時(shí)間是當(dāng)前樂觀的unchoke的三倍,與旋轉(zhuǎn)中的其他任何地方一樣。