版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40306953/article/details/78035215
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}