eliasson/pieces: An experimental BitTorrent client in Python 3.5 (github.com)
了解BitTorrent:BitTorrent 简介 - 知乎 (zhihu.com)
总结项目结构:
- pieces.py:启动程序,调用main()
- cli.py:main(),解析运行参数并生成
TorrentClient
并运行下载任务 - client.py:核心,实现
TorrentClient
,完成整个下载过程,主要实现访问tracker获取peer列表的过程。实现Block
和Piece
、PieceManager
,用于管理传输来的响应数据 - protocol.py:
PeerConnection
,用于对每个peer进行连接。PeerStreamIterator
异步解析字节流中BitTorrent协议信息。PeerMessage
,基类,派生类有Handshake
、KeepAlive
、BitField
、Interested
、NotInterested
、Choke
、Unchoke
、Have
、Request
、Piece
、Cancel
- tracker.py:封装tracker的响应信息,并实现连接tracker的方法
- torrent.py:使用BenCode处理种子文件
- becoding.py:实现BenCode的编解码
程序运行逻辑:
解析运行参数获得种子文件路径,处理种子文件获得tracker地址等信息,创建TorrentClient
,使用异步IO库asyncio
,创建TorrentClient.start()
为协程的任务,使用signal.signal()
处理中断信号,异步执行任务
loop = asyncio.get_event_loop()
client = TorrentClient(Torrent(args.torrent))
task = loop.create_task(client.start())
def signal_handler(*_):
logging.info('Exiting, please wait until everything is shutdown...')
client.stop()
task.cancel()
signal.signal(signal.SIGINT, signal_handler)
任务内容是构建容纳多个PeerConnection
的TorrentClient.peers
数组:这些连接(此时还不知道peer地址)会异步地监控available_peers
队列。然后进入死循环,根据默认间隔和之后tracker发来的呼叫间隔对tracker进行不断连接,从而实时更新available_peers
,直到检测到完成下载
async def start(self):
self.peers = [PeerConnection('''连接信息''')
for _ in range(MAX_PEER_CONNECTIONS)]
while True:
# 终止条件
current = time.time()
if (not previous) or (previous + interval < current):
# 与tacker连接
if response:
# 更新呼叫间隔(由tacker传回)
self._empty_queue()
for peer in response.peers:
self.available_peers.put_nowait(peer)
else:
await asyncio.sleep(5)
self.stop()
每个peer连接监控available_peers
队列,安全地取出tracker发来的peer的ip和port,异步连接,完成握手(根据种子文件中的哈希值确定要传输的pieces)后发送开始信号(Interested
)开始数据传输,传输来的数据先经过PeerStreamIterator
解析后异步地对每段消息进行处理(如:传来的是BitField
时就调用piece_manager的添加peer方法,从而确定这个peer有哪些piece;传来的是Piece
就调用回调函数_on_block_retrieved()
将传来的piece写入文件)
self.future = asyncio.ensure_future(self._start())
async def _start(self):
while 'stopped' not in self.my_state:
ip, port = await self.queue.get()
try:
self.reader, self.writer = await asyncio.open_connection(ip, port)
buffer = await self._handshake()
self.my_state.append('choked')
# 让对方知道我们有兴趣下载片段
await self._send_interested()
self.my_state.append('interested')
# 只要连接打开并传输数据,就开始作为消息流读取响应
async for message in PeerStreamIterator(self.reader, buffer):
# 根据message的类型进行处理
except ...
self.cancel()
PeerStreamIterator
是一个异步迭代器,通过读取套接字数据到缓冲池,然后分析缓冲池的message_id
确定消息类型并返回对应类型的对象。
async def __anext__(self):
# 从套接字读取数据。当我们有足够的数据来解析时,解析它并返回消息。在那之前,继续从流中读取
while True:
try:
data = await self.reader.read(PeerStreamIterator.CHUNK_SIZE)
if data:
self.buffer += data
message = self.parse()
if message:
return message
else:
logging.debug('No data read from stream')
if self.buffer:
message = self.parse()
if message:
return message
raise StopAsyncIteration()
except ...
def parse(self):
header_length = 4
if len(self.buffer) > 4: # 4 bytes is needed to identify the message
message_length = struct.unpack('>I', self.buffer[0:4])[0]
if message_length == 0:
return KeepAlive()
if len(self.buffer) >= message_length:
message_id = struct.unpack('>b', self.buffer[4:5])[0]
def _consume():
"""Consume the current message from the read buffer"""
self.buffer = self.buffer[header_length + message_length:]
def _data():
""""从读缓冲区中提取当前消息"""
return self.buffer[:header_length + message_length]
if message_id is PeerMessage.BitField:
data = _data()
_consume()
return BitField.decode(data)
elif message_id is PeerMessage.Interested:
# 之后分别列举可能的情况然后获取数据返回对应类型的对象
else:
logging.info('Unsupported message!')
else:
logging.debug('Not enough in buffer in order to parse')
return None
细枝末节:
Signal模块
signal.signal(signalnum, handler):
- signalnum:信号量,具体参看python文档
- handler:信号处理程序,可以是自定义的函数,也可以是特殊值 signal.SIG_IGN、 signal.SIG_DFL之一
信号量signal.SIGPIPE:忽略默认的SIGPIPE处理函数(SIGPIPE默认处理为管道/套接字出错时终止进程,这里将其忽略),因此管道和套接字上的写错误可以像普通的 Python 异常一样报告
信号量signal.SIGINT:转换为 KeyboardInterrupt 异常(一般为ctrl+c引发的中断)。
——https://blog.csdn.net/sunjintaoxxx/article/details/122195042
def signal_handler(*_): # 原本应该是 XXX(signum, frame) 使用带星号参数
logging.info('Exiting, please wait until everything is shutdown...')
client.stop()
task.cancel()
signal.signal(signal.SIGINT, signal_handler)
create_task
loop = asyncio.get_event_loop()
task = loop.create_task(任务函数) # 任务函数内含循环,称为协程
try:
loop.run_until_complete(task)
except CancelledError:
logging.warning('Event loop was canceled')
事件循环的run_until_complete方法运行事件循环时,当其中的全部任务完成后,会自动停止循环;若想无限运行事件循环,可使用asyncio提供的run_forever方法
这样就会阻塞在这里,直到协程完成
ensure_future
也是asyncio的一个方法,同样可以创建任务,区别在于:
那么用 ensure_future 还是 create_task 呢?先对比一下函数声明:
asyncio.ensure_future(coro_or_future, *, loop=None) BaseEventLoop.create_task(coro)
显然,ensure_future 除了接受 coroutine 作为参数,还接受 future 作为参数。看 ensure_future 的代码,会发现 ensure_future 内部在某些条件下会调用 create_task,综上所述:
encure_future: 最高层的函数,推荐使用!
create_task: 在确定参数是 coroutine 的情况下可以使用。
——https://www.jianshu.com/p/ff1747d736cb
self.future = asyncio.ensure_future(self._start())
在PeerConnection
的__init__
函数中创建任务,同时异步地开启任务的执行,通过future
对任务进行控制,如if not self.future.done():
、self.future.cancel()
异步迭代器
异步迭代器在Python中的作用是允许以异步的方式逐个返回元素,而不是一次性返回所有元素。这对于处理大量数据或需要等待IO操作完成的情况非常有用。
class AsyncIterator:
def __init__(self, ...):
...
async def __aiter_(self):
# 返回一个迭代器,通常是 return self
async def __anext__(self):
# 返回下一个元素
# 使用
async def main():
async for item in AsyncIterator([1, 2, 3, 4, 5]):
print(item)
struct.pack / unpack