DHT協議解析(1)
阿新 • • 發佈:2018-11-17
BEP-003
BitTorrent 是一個分發檔案的協議( a protocol for distributing files).它根據URL定義內容,與web無縫整合.它相對於普通HTTP的優勢在於當多個下載者下載同一個檔案時,下載者互相也會上傳給對方.這使得檔案資源只需要一些代價的增加就可以服務很多的下載者.
BitTorrent 檔案分發由以下實體組成
- web server
- 靜態 metainfo 檔案
- BitTorrent tracker
- 原始下載者
- 終端使用者web 瀏覽器
- 終端使用者 downloaders
主機按照以下步驟開始服務 :
- 開始執行 tracker(或者已執行)
- 執行一個遠端web伺服器,如apache(或者已執行)
- 在web 伺服器上關聯.torrent 檔案
- 根據檔案和tracker的URL生成metainfo(.torrent)檔案
- 向web server傳送 metainfo
- 在網頁上釋出metainfo 連結
- 原始使用者提供完整的檔案
使用者下載步驟:
- 安裝 BitTorrent
- 瀏覽網頁
- 點選.torrent 連結
- 選擇下載位置
- 等待下載完成
- 通知downloader 退出(期間持續上傳)
bencoding
- 字串編碼是帶有長度字首的,然後後面跟著冒號(:)和原始字串.例如4:spam 相當於’spam’
- 整數編碼由’i’開始然後是數字(10進位制)由’e’結束.例如i3e相當於3, i-3e相當於-3.整數沒有大小限制.i-0e是非法的,除了i0e相當於0,其他任何數字部分由0開始的(如i03e)都是非法的.
- 列表由’l’開始然後是它的元素,最後是’e’.如l4:spam4:eggse 相當於[‘spam’,’eggs’]
- 字典由’d’開始然後是它的鍵值以’e’結束.例如d3:cow3:moo4:spam4:eggse,相當於{‘cow’: ‘moo’, ‘spam’: ‘eggs’}, d4:spaml1:a1:bee 相當於{‘spam’: [‘a’, ‘b’]}. key必須是字串,且排序.
用遞迴思想,b編碼的簡單實現
#!/usr/bin/env python3.5
import itertools
import collections
try:
range = xrange
except NameError:
pass
def encode(obj):
if isinstance(obj, bytes):
#return '{0}:{1}'.format(len(obj), obj)
return b'%i:%s'%(len(obj), obj)
elif isinstance(obj, int):
contents = b'i%ie'%(obj)
return contents
elif isinstance(obj, list):
values = b''.join([encode(o) for o in obj])
return b'l%se'% values
elif isinstance(obj, dict):
items = sorted(obj.items())
values = b''.join([encode(key) + encode(value) for key, value in items])
return b'd%se'%(values)
else:
raise TypeError('Unsupported type: {0}.'.format(type(obj)))
def decode(data):
'''
Bdecodes data into Python built-in types.
'''
return consume(LookaheadIterator(data))
class LookaheadIterator(collections.Iterator):
'''
An iterator that lets you peek at the next item.
'''
def __init__(self, iterator):
self.iterator, self.next_iterator = itertools.tee(iter(iterator))
# Be one step ahead
self._advance()
def _advance(self):
self.next_item = next(self.next_iterator, None)
def __next__(self):
self._advance()
return next(self.iterator)
def consume(stream):
item = stream.next_item
#print(item, type(item))
if item is None:
raise ValueError('Encoding empty data is undefined')
elif item == b'i':
return consume_int(stream)
elif item == b'l':
return consume_list(stream)
elif item == b'd':
return consume_dict(stream)
elif item is not None and item.isdigit():
return consume_str(stream)
else:
raise ValueError('Invalid bencode object type: ', item)
def consume_number(stream):
result = b''
while True:
chunk = stream.next_item
if not chunk.isdigit():
return result
elif result.startswith(b'0'):
raise ValueError('Invalid number')
next(stream)
result += chunk
def consume_int(stream):
if next(stream) != b'i':
raise ValueError()
negative = stream.next_item == b'-'
if negative:
next(stream)
result = int(consume_number(stream))
if negative:
result *= -1
if result == 0:
raise ValueError('Negative zero is not allowed')
if next(stream) != b'e':
raise ValueError('Unterminated integer')
return result
def consume_str(stream):
length = int(consume_number(stream))
if next(stream) != b':':
raise ValueError('Malformed string')
result = b''
for i in range(length):
try:
result += next(stream)
except StopIteration:
raise ValueError('Invalid string length')
return result
def consume_list(stream):
if next(stream) != b'l':
raise ValueError()
l = []
while stream.next_item != b'e':
l.append(consume(stream))
if next(stream) != b'e':
raise ValueError('Unterminated list')
return l
def consume_dict(stream):
if next(stream) != b'd':
raise ValueError()
d = {}
while stream.next_item != b'e':
key = consume(stream)
#pdb.set_trace()
value = consume(stream)
d[key] = value
if next(stream) != b'e':
raise ValueError('Unterminated dictionary')
return d
metainfo 檔案
matainfo 檔案(或者.torrent 檔案)就是被編碼的字典,有以下鍵值,所有 字串必須是utf-8 編碼:
- announce: tracker的url
- info:info dictionary
info dictionary
- name 對應utf-8編碼的字串,僅為建議儲存的檔名或者資料夾.
- piece length 對應為數字,代表檔案分塊大小.為了便於傳輸,除了最後一塊,檔案都被分割為同樣大小的塊.piece length 為2的指數, 最常見的2 18 = 256k
- pieces 對應為字串, 字串長度為20的倍數,每20個對應SHA1 的hash值
還有一個key length 或者 files, 兩者是互斥的,只會存在一個.當length 存在時,代表下載的為單個檔案,否則files存在代表多個檔案 結構儲存在一個字典裡.
- length 存在時代表為單個檔案, 為檔案大小, 單位為bytes
多個檔案的時候,files 為多個字典組成的列表,包含以下key:
- length: 檔案的大小, 單位為bytes.
- path: utf-8 編碼的字串組成的list最後一項為檔名
Tracker HTTP/HTTPS Protocol
client->tracker GET request 引數:
所有引數都被urlencode ,即除了set( 0-9, a-z, A-Z, ‘.’, ‘-‘, ‘_’ , ‘~’),其他的都被轉義為%nn , 其中nn為對應位元組的十六位數值, 例如:
20-byte hash \x12\x34\x56\x78\x9a\xbc\xde\xf1\x23\x45\x67\x89\xab\xcd\xef\x12\x34\x56\x78\x9a,
被轉義為
%124Vx%9A%BC%DE%F1%23Eg%89%AB%CD%EF%124Vx%9A
\x12不在set裡, 被轉義為%12, \x34 對應為4, 轉義為4, \x56 轉義為V…..
- info_hash: 20-bytes,對metainfo中key為info的值使用sha1獲得的hash值
- peer_id:20-bytes 字串用以標識client的id
- port: client監聽的埠
- uploaded: 已經上傳的bytes 數量(從向tracker 傳送started 事件開始)
- downloaded :已經下載的bytes(從向tracker 傳送started 事件開始)
- left: 還需要下載的bytes數量(從向tracker 傳送started 事件開始)
- compact: 當為1時表示 client 接受compact的資料,即peers list 被表示6-bytes 其中前4bytes 表示host, 後2bytes 表示port. 有的tracker只支援compact資料.
- no_peer_id: 表示tracker 可以省略peer id,當compact 被設定的時候這個選項被省略.
- event: 包含started ,stopped, completed
- ip: 可選,當client的地址可以由 http 請求得出的時候這個引數是不需要的.但是當請求從通過代理或者nat的時候是必須的
- numwant: 可選的,client 想從tracker 獲得的peer 的數量.如果省略 則預設為50
- key: 可選的,另外一個身份表示,但是這個不對其他peer 公開.當ip變化的時候用以表示身份.
- trackerid: 可選的,如果上次announce 包含一個tracker id ,需要設定在這裡.
Tracker Response
- failure reason:string, 失敗原因
- warning message:(可選)警告
- interval:client 向tracker 傳送資訊的間隔(秒)
- min interval:(可選的)client 向tracker 傳送的間隔必須低於此.
- track id:client 下次announcements 需要附加這個字串.見上面的request 引數.
- complete: 已經完成下載的,擁有完整檔案的peers.(seeder)
- incomplete: non-seeder peers,leechers.即在下載的peer 數量.
- peers:(字典)
- peer id: 見上request.
- ip: peer 的ip 地址.ipv6(16進位制),ipv4(x.x.x.x 形式),或者dns (string)
- port :peer 埠
- peers:(二進位制形式)見上request中的compact.
舉個例子
使用上面一段程式碼
import hashlib
from tornado.httpclient import HTTPClient
from tornado.httputil import url_concat
import os
def peerid():
#隨機產生peerid
prefix = 'shykoe'.encode('utf-8')
return prefix + os.urandom(20 - len(prefix))
f = open('./42260247f4b773737ee7c0dbdbd54f5a99ba7aa3.torrent','rb')
data = f.read()
data = [bytes([b]) for b in data]
torrent = decode(data)
info = torrent[b'info']
info = encode(info)
hash = hashlib.sha1(info)
print(hash.hexdigest())
#42260247f4b773737ee7c0dbdbd54f5a99ba7aa3
#可以發現跟上面檔名的infohash值是一致的,計算正確.
hashcode = hash.digest()
params = {
'info_hash': hashcode,
'peer_id': peerid(),
'port': 6881,
'uploaded': 0,
'downloaded': 0,
'left': 24998051840,
'compact': 0
}
tracker_url = url_concat('http://explodie.org:6969/announce', params)
client = HTTPClient()
response = client.fetch(tracker_url)
#response.body
resdata = [bytes([b]) for b in response.body]
res = decode(resdata)
#{b'min interval': 1800, b'incomplete': 1, b'peers': [{b'port': 6881, b'ip': b'2400:dd01:1032:f176:2930:d924:73be:547b', b'peer id': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}], b'complete': 0, b'interval': 1800}