DHT协议解析(1)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40306953/article/details/78035215

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 *= -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}

猜你喜欢

转载自blog.csdn.net/qq_40306953/article/details/78035215
DHT