天天看點

DHT協定解析(1)BEP-003

BEP-003

BitTorrent 是一個分發檔案的協定( a protocol for distributing files).它根據URL定義内容,與web無縫內建.它相對于普通HTTP的優勢在于當多個下載下傳者下載下傳同一個檔案時,下載下傳者互相也會上傳給對方.這使得檔案資源隻需要一些代價的增加就可以服務很多的下載下傳者.

BitTorrent 檔案分發由以下實體組成

  1. web server
  2. 靜态 metainfo 檔案
  3. BitTorrent tracker
  4. 原始下載下傳者
  5. 終端使用者web 浏覽器
  6. 終端使用者 downloaders

主機按照以下步驟開始服務:

  1. 開始運作 tracker(或者已運作)
  2. 運作一個遠端web伺服器,如apache(或者已運作)
  3. 在web 伺服器上關聯.torrent 檔案
  4. 根據檔案和tracker的URL生成metainfo(.torrent)檔案
  5. 向web server發送 metainfo
  6. 在網頁上釋出metainfo 連結
  7. 原始使用者提供完整的檔案

使用者下載下傳步驟:

  1. 安裝 BitTorrent
  2. 浏覽網頁
  3. 點選.torrent 連結
  4. 選擇下載下傳位置
  5. 等待下載下傳完成
  6. 通知downloader 退出(期間持續上傳)

bencoding

  1. 字元串編碼是帶有長度字首的,然後後面跟着冒号(:)和原始字元串.例如4:spam 相當于’spam’
  2. 整數編碼由’i’開始然後是數字(10進制)由’e’結束.例如i3e相當于3, i-3e相當于-3.整數沒有大小限制.i-0e是非法的,除了i0e相當于0,其他任何數字部分由0開始的(如i03e)都是非法的.
  3. 清單由’l’開始然後是它的元素,最後是’e’.如l4:spam4:eggse 相當于[‘spam’,’eggs’]
  4. 字典由’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 *= -

        if result == :
            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.

舉個例子

DHT協定解析(1)BEP-003
DHT協定解析(1)BEP-003

使用上面一段代碼

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( - 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': ,
    'uploaded': ,
    'downloaded': ,
    'left': ,
    'compact': 
    }
    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}
           

繼續閱讀