??xml version="1.0" encoding="utf-8" standalone="yes"?> 在这个示意图中,客户?A ?其它 peers B、C、D、E 都徏立了q接。通过头来表C接徏立的方向。Ad?B、D、E 建立q接QA被动接收C的连接。同时C与D、E与D、B与D之间也都有连接。这P所有下载者之间就形成了一个网状的l构?br> 同时Q这些下载者都要与 tracker 之间不断通信?br> 无论是被动连接,q是dq接Q一旦在“BT对等握手”成功之后Q它们就没有M区别了。下载通过q个q接q行Q上传也是通过q个q接q行?br> 客户端主动发赯?br>【Encrypter.py?br> 上一节的最后,我们看到调用 Encoder::start_connection() 来向其它 peer 发vq接。所以,从这里开始看赗?/p>
class Encoder: 客户端被动接受外来连?br>?
L(fng)在与 tracker 通信的时候,已经把自q ip 和监听的 port 报告l了 tracker。这P其它 peers 可以通过q个
ip ?port 来连接它了。例如上图中的CQ就dl?A 发一个连接,从C的角度来_它是“dq接”Q但从A的角度,它是“被动q接”?br>一旦有外来q接hQ就会调?RawServer::handle_events()Q下面是摘录?#8220;Tracker 服务器源码分析之二:RawServerc?#8221;中的一Dc?/p>
最
后调用的?Handle ?external_connection_made()Q对客户端来_q个Handle ?Encoder
cd象。所以,外来q接建立成功后,最后调用的?Encoder:: external_connection_made()Q?/p>
③def external_connection_made(self, connection): BT对等q接握手Q第一?/p>
如果是主动连接,那么一旦连接徏立成功之后,qҎ(gu)发送一个握手消息,我们折回ȝ序号?4 的代码: 在《BT协议规范》中Q如此描q握手消息: 它用来唯一标识一?peer?br>握手q程已经完成了第一步,q需要第二步Q接收到Ҏ(gu)的握手消息,握手q程才算完成。所以接下去看在有数据到来的时候,是如何处理的?/p>
BT对等q接握手Q第二步 【Encoder?br>⑤def data_came_in(self, connection, data): q一步调?Connection::data_came_in() 【Connection?br>⑥def data_came_in(self, s): ⑧def read_header_len(self, s): def read_header(self, s): def read_reserved(self, s): def read_download_id(self, s): def read_peer_id(self, s):
q一节我们分析BT客户端与 tracker 服务器的通信q程。在整个下蝲q程中,客户端必M断的?tracker
通信Q报告自q状态,同时也从 tracker 那里获取当前参与下蝲的其?peers 的信息(ip地址、端口等Q。一旦不能与 tracker
通信Q则下蝲q程无法l箋下去?br> 客户端与 tracker 之间采用 HTTP 协议通信Q客L(fng)把状态信息放?
HTTP协议的GET命o的参C传递给 trackerQtracker 则把 peers列表{信息放?中传递给
客户端。这U设计非常简捷y妙。采用HTTP协议也更使得I越防火墙的L更容易(不过实际应用中,tracker通常不会使用 80 端口Q?br> 请务必参考服务器端源码分析的几篇相关文章Q论坛中有)?br> Rerequester cd权负责与 tracker 的通信dQ我们首先看客户端主E序 download.py中相应的代码Q?/p>
【download.py?br>①rerequest = Rerequester(response['announce'], config['rerequest_interval'], Rerequester
cȝ构造函C传递了一大堆参数Q重Ҏ(gu) rawserver.add_task?
encoder.start_connectionQrawserver和encoder
q两个对象,在上一节中Q我们已l看C它们的构造过E)Q分别对应RawServer::add_task() ?
Encoder::start_connection()Q稍后会看到它们的作用?br>一切从 Rerequest::begin() 开始?/p>
【rerequester.py?/p>
class Rerequester: ?nbsp;def c(self): ?nbsp; def begin(self): ?nbsp; def announce(self, event = None): # 函数中可以嵌套定义函敎ͼq是 python 语法特别的地斏V?br> # 调用 RawServer::add_task() ?checkfail() 加入CQ务队列中。在 timeout 旉之后Q检查向 tracker 发的GET命o是否p|?br> def checkfail(self = self, set = set): # 创徏一个线E,执行 Rerequester::rerequest() ?nbsp; def rerequest(self, url, set): ?nbsp; def postrequest(self, data): # Ҏ(gu) tracker 的响应,调整BT客户端的参数?br> self.announce_interval = r.get('interval', self.announce_interval) # 其它下载者的信息保存?peers 数组中?br> p = r['peers']
class Connection: def get_ip(self): def get_id(self): def is_locally_initiated(self): def is_flushed(self): ⑦def read_header_len(self, s): def read_header(self, s): def read_reserved(self, s): def read_download_id(self, s): def read_peer_id(self, s): “BT对等q接”的握手过E正式宣告完成,此后Q双方就可以通过q个q接互相发送消息了?br> self.complete = True 调用Connecter::connection_made()Q这个函数的意义Q我们到分析 Connecter cȝ时候,再记得分析?br> self.encoder.connecter.connection_made(self) 下面q入 BT 消息的处理过E?br> return 4, self.read_len def read_len(self, s): 消息处理Q交l了 Connecter::got_message()Q所以下一我们要分析 Connecter cR?br> def read_message(self, s): def read_dead(self, s): def close(self): def sever(self): def send_message(self, message):
q入协议分析循环。。?br> while True: self.next_len表示按照BT对等协议规范Q下一D要分析的数据的长度 ?buffer 中取出数据,q些数据是q一步协议分析所需要的数据。然后把 buffer 清空?br> m = self.buffer.get() next_func 是用于q一步协议分析的函数?br>q回?x 是一个二元组Q包含了下一步协议分析的数据长度和协议分析函数。这PŞ成了一个协议分析@环?br> try: class Encoder: Z保持q接不因时而被关闭Q所以可能需要随机的发送一些空消息Q它的目的纯_Ҏ(gu)Z保证q接?#8220;zd” ③主动向Ҏ(gu)发v一个连接,q个函数什么时候调用? 如果当前q接敎ͼ已经过讑֮?#8220;最大发赯接数”Q那么就暂时不徏立连接?br> self.connections[c] = Connection(self, c, id, True) def ever_got_incoming(self): ①在 RawServer 中,当从外部发v的一个TCP成功建立后,调用此函数?br>q里传递进来的参数 connection ?SingleSocket cd def connection_flushed(self, connection): 关闭q接的时候调用此函数 BT客户端的 main() 函数Q?br> C
和c++的可执行E序Q通常都有一?main() 函数Q一切从q里开始。?python q种解释性语aQƈ没有 main()
函数的概c你传递给解释器一?.py
扩展名的python源码文gQ解释器׃序去解释执行这个文件中的代码。所以,BT客户端的执行Q是从最先被 python
解释器解释执行的那个文g开始的。这个文件应该是 btdownloadheadless.pyQ至于谁又来通知?python
解释器执行这个文件的Q我们以后再讨论?/p>
【btdownloadheadless.py】(在BT源码的根目录下) # 所有的 python的入门书c中Q都会告诉你下面q段代码的含义。这是最先被解释执行的代码所在?br>①if __name__ == '__main__':
④rawserver = RawServer(doneflag, config['timeout_check_interval'], config['timeout'], errorfunc # 选择一个可用的端口作ؓ监听端口 encoder = Encoder(connecter, rawserver, rawserver.listen_forever(encoder) ?
L(fng)的核心类是 RawServerQ这个类我在“服务器端源码分析”Q可在论坛中扑ֈQ文章中分析q它的作用,q里不再赘述。通过调用它的
listen_forever() 函数QBT
客户端就q入了一个@环之中。此后,所有的下蝲、上传、文件存储以及其它工作都在这一ơ次的@环之中完成?br>注意刎ͼ在进入@环之前,调用?
RawServer::bind() 函数QBT客户端会选择一个可用的端口Q然后通过监听q个端口Q从而可以接受其?peer
的连接请求(也就是BT对等q接Q。这个端口可以称?#8220;监听端口”。如果你有网l服务器的编E经验,从它的@环处理逻辑、监听端口的处理可以知道Q这昄
是一个典型的|络服务器的表现。所以,我在以前的文章中曄说过QBT客户端同时也是一个服务器?br>一旦在监听端口上有某个peer发来q接?
求,BT客户端会创徏一个新的端口,q个端口用来与请求者徏立连接;有几个peerhq接Q就会创建几个端口。我们可以说q是一?#8220;被动端口”。在循环
的处理过E中QBT客户端还会根据情况,向其?
peerd发出q接hQ每个连接也需要一个端口,q些端口可以UCؓ“d端口”。其实,q些都是|络~程中最基本的概念,不清楚的朋友q是首先要去?
看这斚w的书c?br>q样Q就有了一?#8220;监听端口”Q几?#8220;被动端口”和几?#8220;d端口”QBT客户端通过 select()
函数来监视这些端口,一?#8220;监听端口”或?#8220;被动端口”上有数据到来Q或者有数据需要从“d端口”上发送出去,那么select()
都可以及时感知ƈq行处理Q其实是一个轮询的q程Q,q就UCؓ“I/O多\复用”。如?#8220;被动端口”上有数据到来Q那么这是BT对等q接的协议数据,需?
按照BT对等协议q行分析QƈҎ(gu)分析l果q行相应处理Q这个分析处理的工作是?Encoder cL完成的。所以在
listen_forever() 函数中传递的参数是一?Encoder cd象。关于对 Encoder
cȝ分析Q以后分析到BT对等协议的处理的时候再说?/p>
结Q?br> 通过q篇文章Q我们了解了BT客户端是从哪里执行的Q相应的源码在哪
里。同时我们知道了BT客户端是以一个服务器循环的Ş式运行的Q它需要监听一个端口,用于接受其它peers的连接请求;在@环过E中Q它通过
select 来实?#8220;I/O多\复用”Qƈ?Encoder cL完成BT对等协议的分析处理?/p>
上一节我们分析了BT客户端与tracker之间的通信q程。通过?tracker 的通信Q客L(fng)获得了参与下载的其它peers
的列表。有了这?peers
的信息,客户端就可以d向它们发赯接,一步从它们那里获取所需要的文g片断做好准备。这些连接称?#8220;dq接”。因接的发vҎ(gu)客户端自己?br>
同时Q每个客L(fng)在启动以后,都会监听某个端口Q用于接受其?peers
的连接请求。P2P的核心理念就?#8220;公^、对{?#8221;Q你向别人有所求(下蝲Q,必L付出Q上传)。客L(fng)在接受外来的q接h之后Q就会生一个新的连
接,UC?#8220;被动q接”Q因接的发vҎ(gu)其它 peer?br> 无论是被动连接,q是dq接Q一旦连接徏立之后,双方pq行“BT对等协议”的握手。握手成功之后,双方才可以通过q个q接互相传递消息了。ؓ什么要q行握手了?主要目的是ؓ了防止一些错误的q接。这好比地下党接头Q暗号对上了Q彼此才建立信Q?/p>
本文重点分析BT客户端如何主动向其它 peers 发vq接QBT客户端如何被动接收其?peers 的连接请求;以及在连接徏立成功之后,如何q行BT对等协议握手的过E?/p>
# start_connection() 需要两个参敎ͼ
dnsQ对方的ip、port 的组合?br>idQ?Ҏ(gu)?id。每个BT客户端都要创Z个唯一?id Pq且把这?id h告给 tracker。Id号唯一标识了一个客L(fng)。Id L(fng)计算方式?download.py 中?br>myid = 'M' + version.replace('.', '-')
myid = myid + ('-' * (8 - len(myid))) + b2a_hex(sha(repr(time()) + ' ' +
str(getpid())).digest()[-6:])
seed(myid)
①def start_connection(self, dns, id):
if id:
# 如果 id 是自己,不连?br> if id == self.my_id:
return
# 如果已经和这个peer建立了连接,也不再徏立连接?br> for v in self.connections.s():
if v.id == id:
return
# 如果当前q接数过多,暂时不发赯?br> if len(self.connections) >= self.max_initiate:
# self.spares 起到~存的作用。在当前q接数过多的情况下,把本ơ要q接?peer ?ip、port ~存h。一旦当前连接数于讑֮?max_initiateQ?则可以从 spares 中取出备用的 peers?br> if len(self.spares) < self.max_initiate and dns not in self.spares:
self.spares.append(dns)
return
try:
# 调用 RawServer::start_connection()Q发?TCP 的连接。RawServer的代码分析请参看“服务器源码分?#8221;pd文章Q不再赘q?br># q回?c 是一?SingleSocket 对象Q它?yu)装?socket 句柄
# 如果出错Q抛?socketerror 异常
c = self.raw_server.start_connection(dns)
# 成功建立 TCP q接。构造一?Connection对象Q加?connections 字典中。注意,最后一个参数是 TrueQ它表面q条q接是由客户端主动发起徏立的。我们立d?Connection cȝ构?br> self.connections[c] = Connection(self, c, id, True)
except socketerror:
pass
【Encrypter.py?br>class Connection:
?nbsp; def __init__(self, Encoder, connection, id, is_local):
self.encoder = Encoder
self.connection = connection #q个 connection ?SingleSocket 对象Q就是上?
RawServer::start_connection() q回的|它封装了对socket句柄的操作。名字v的不好,Ҏ(gu)弄淆?br> self.id = id
self.locally_initiated = is_local #q个q接是否由本地发P
self.complete = False
self.closed = False
self.buffer = StringIO()
self.next_len = 1
⑦self.next_func = self.read_header_len
# 如果由本地发P那么l对方发送BT对等q接的握手消息?br> ④if self.locally_initiated:
connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id)
if self.id is not None:
connection.write(self.encoder.my_id)
# 同样是创Z个新?Connection c,q加?connections 字典中。但不同之处在于最后一个参数是 FalseQ表明这个连接是由外部发L(fng)?br>self.connections[connection] = Connection(self, connection, None, False)
if self.locally_initiated:
connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id)
if self.id is not None:
connection.write(self.encoder.my_id)
对等协议׃个握手开始,后面是@环的消息,每个消息的前面,都有一个数字来表示消息的长度。握手的q程首先是先发?9Q然后发送协议名U?#8220;BitTorrent protocol”?9是“BitTorrent protocol”的长度?br>后箋的所有的整数Q都采用big-endian 来编码ؓ4个字节?br>在协议名UC后,?个保留的字节Q这些字节当前都讄??br>接下来对元文件中?info 信息Q通过 sha1 计算后得到的 hash|20个字节长。接收消息方Q也会对 info q行一?hash q算Q如果这两个l果不一P那么说明双方要下载的文g不一_会切断连接?br>接下来是20个字节的 peer id?br>?
以看刎ͼ最后两就?Encoder::download_id ?
Encoder::my_id。download_id是如何得来的Q它是首先对 torrent 文g?info 关键字所包含的信息进?
Bencoding 方式的编码(L(fng)《BT协议规范》关?Bencoding的介l)Q然后再通过 sha 函数计算出的
hashQ摘要)倹{(相关代码都在 download.py 中)。在一ơ下载过E中Q所有的下蝲者根?torrent 文g计算出的q个
download_id应该都是一L(fng)Q否则就说明不处于同一个下载过E中?br>至于 peer_idQ可以看出是可选的。它的计方式也?download.py 中:
myid = 'M' + version.replace('.', '-')
myid = myid + ('-' * (8 - len(myid))) + b2a_hex(sha(repr(time()) + ' ' + str(getpid())).digest()[-6:])
seed(myid)
当TCPq接上有数据到来的时候, RawServer 会调用到 Encoder:: data_came_in()
self.connections[connection].data_came_in(data)
#q个循环处理用来对BT对等q接中的消息q行分析
while True:
if self.closed:
return
i = self.next_len - self.buffer.tell()
if i > len(s):
self.buffer.write(s)
return
self.buffer.write(s[:i])
s = s[i:]
m = self.buffer.get()
self.buffer.reset()
self.buffer.truncate()
try:
x = self.next_func(m) #调用消息分析函数Q第一个被调用的是read_header_len
except:
self.next_len, self.next_func = 1, self.read_dead
raise
if x is None:
self.close()
return
self.next_len, self.next_func = x
# 协议的长度是否ؓ 19Q?br> if ord(s) != len(protocol_name):
return None
return len(protocol_name), self.read_header # 下一个处理函?/p>
# 协议名称是否?#8220;BitTorrent protocol”Q?br> if s != protocol_name:
return None
return 8, self.read_reserved # 下一个处理函?/p>
#8个保留字?br> return 20, self.read_download_id # 下一个处理函?/p>
Ҏ(gu) download_id 和自p出的是否一_
if s != self.encoder.download_id:
return None
查完 download_idQ就认ؓҎ(gu)已经通过查了?br>q里很关键!Q!Q需要仔l体会。这实际上就是在被动q接情况下,完成握手q程的处理。如果连接不是由本地发v的(被动接收C个握手消息)Q那么给Ҏ(gu)回一个握手消息。这里的握手消息发送处理和W一步是一L(fng)
if not self.locally_initiated:
self.connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id + self.encoder.my_id)
return 20, self.read_peer_id #下一个处理函?/p>
# Connection cȝ来管理一?BT 对等q接。在握手完成之后Q就用对方的 peer_id 来唯一标识q个 Connection。这个D保存?self.id 中。显Ӟ在握手完成之前,q个 id q是I倹{?br> if not self.id:
# Ҏ(gu)的peer_id 可千万别跟自q一?br> if s == self.encoder.my_id:
return None
唔,如果 peer_id 已经收到q,也不l箋下去?br> for v in self.encoder.connections.s():
if s and v.id == s:
return None
用对方的 peer_id ?self.id 赋|唯一标识q个 Connection
self.id = s
if self.locally_initiated:
self.connection.write(self.encoder.my_id)
else:
self.encoder.everinc = True
else:
if s != self.id:
return None
# OKQ握手完成!Q!
self.complete = True
self.encoder.connecter.connection_made(self)
return 4, self.read_len #下一个处理函数。从此以后,是对其它BT对等消息的处理过E了。这是我们下一节分析的问题?/p>
结Q?br> q篇文章重点分析了BT客户端主动发赯接和被动接收q接的过E,以及在这两种情况下,如何q行BT对等握手的处理?br> 在BT对等握手完成之后Q连接的双方可以互相发送BT对等消息了,q是下一节的内容?
上一节我们分析了BT客户端主E序的框Ӟ一个@环的服务器程序。在q个循环q程中,客户端要完成如下dQ?br>? ?tracker ? 务器通信Q汇报自q状态,? 时获取其?peers 的信息;
? 接受其它 peers 的连接请求;
? d向某?peers 发vq接hQ?br>? 对BT对等协议的分析处理;
? 启用片断选择法Q? 通过q些d或被动的q接M载所需要的片断Q?br>? 为其?peers 提供片断上传Q启用阻塞算法,? d某些 peers 的上传请求;
? 下载的片断写入盘Q?br>? 其它dQ?/p>
rawserver.add_task, connecter.how_many_connections,
config['min_peers'], encoder.start_connection,
rawserver.add_task, storagewrapper.get_amount_left,
upmeasure.get_total, downmeasure.get_total, listen_port,
config['ip'], myid, infohash, config['http_timeout'], errorfunc,
config['max_initiate'], doneflag, upmeasure.get_rate,
downmeasure.get_rate,
encoder.ever_got_incoming
②rerequest.begin()
# q个构造函C递了一大堆参数Q现在没必要搞的那么清楚Q关键是 sched ?connect q两个参敎ͼ对照前面的代码,很容易知道它们是什么?br> def __init__(self, url, interval, sched, howmany, minpeers,
connect, externalsched, amount_left, up, down,
port, ip, myid, infohash, timeout, errorfunc, maxpeers, doneflag,
upratefunc, downratefunc, ever_got_incoming):
self.url = ('%s?info_hash=%s&peer_id=%s&port=%s&key=%s' %
(url, quote(infohash), quote(myid), str(port),
b2a_hex(''.join([chr(randrange(256)) for i in xrange(4)]))))
if ip != '':
self.url += '&ip=' + quote(ip)
self.interval = interval
self.last = None
self.trackerid = None
self.announce_interval = 30 * 60
self.sched = sched # Q》RawServer::add_task()
self.howmany = howmany # 当前有多个q接 peer
self.minpeers = minpeers
self.connect = connect #Q?Encoder::start_connection()
self.externalsched = externalsched
self.amount_left = amount_left
self.up = up
self.down = down
self.timeout = timeout
self.errorfunc = errorfunc
self.maxpeers = maxpeers
self.doneflag = doneflag
self.upratefunc = upratefunc
self.downratefunc = downratefunc
self.ever_got_incoming = ever_got_incoming
self.last_failed = True
self.last_time = 0
# 再调用一?add_task()Q把自n加入到客L(fng)的Q务队列中Q这样就形成了Q务@环,每隔一D|_׃执行q个函数。从而,客户端与 tracker 的通信能够持箋的进行下厅R?br> self.sched(self.c, self.interval)
if self.ever_got_incoming():
# 如果曄接受到过外来的连接,那么说明q个客户端是可以接受外来q接的,也就是说很可能处?#8220;公网”之中Q所以ƈ非很q切的需要从 tracker
那里获取 peers 列表。(既可以主动连接别人,也可以接受别人的q接Q所以有点不紧不慢的味道Q?br> getmore = self.howmany() <= self.minpeers / 3
else:
# 如果从来没有接受到过外来的连接,那么很有可能处于内网之中Q根本无法接受外来的q接。这U情况下Q只有主动的去连接外部的 peersQ所以要可能的?tracker h更多?peers。(只能去连接别人,所以更q切一些)?br> getmore = self.howmany() < self.minpeers
if getmore or time() - self.last_time > self.announce_interval:
# 如果有必要从 tracker 那里获取 peers 列表Q且d执行旉已到Q则调用 announce() 开始与 tracker 通信
self.announce()
# 调用前面提到?RawServer::add_task()Q把 Rerequester::c() 加入?rawserver对象的Q务队列中
self.sched(self.c, self.interval)
# 宣布?tracker 的通信开始。。。?br> self.announce(0)
self.last_time = time()
# 传递给 tracker 的信息,攑֜ HTTP GET 命o的参C。所以,首先是构造这个参数?br> # 传递给 tracker 的信息,必须包括 uploaded、downloaded、left 信息。其它入 last、trackerid、numwant、compact、event 是可选的。这些参数的解释L(fng) BT协议规范?br> s = ('%s&uploaded=%s&downloaded=%s&left=%s' %
(self.url, str(self.up()), str(self.down()),
str(self.amount_left())))
if self.last is not None:
s += '&last=' + quote(str(self.last))
if self.trackerid is not None:
s += '&trackerid=' + quote(str(self.trackerid))
if self.howmany() >= self.maxpeers:
s += '&numwant=0'
else:
s += '&compact=1'
if event != None:
s += '&event=' + ['started', 'completed', 'stopped'][event]
set = SetOnce().set
if set():
if self.last_failed and self.upratefunc() < 100 and self.downratefunc() < 100:
self.errorfunc('Problem connecting to tracker - timeout exceeded')
self.last_failed = True
self.sched(checkfail, self.timeout)
# 可见Q客L(fng)?tracker之间的通信是由单独的线E完成的
Thread(target = self.rerequest, args = [s, set]).start()
try:
# 调用 urlopen() Q向 tracker 发送命?br>h = urlopen(url)
# read() q回 tracker 的响应数据?br> r = h.read()
h.close()
if set():
def add(self = self, r = r):
self.last_failed = False
# ?tracker 响应回来的数据,?postrequest() 处理?br>self.postrequest(r)
# externalsched() 同样是调?RawServer::add_task()Q这P׃U程调用 postrequest() 函数Q处?tracker 响应的数据。(不要忘记Q我们现在是处在一个单独的子线E中。)
self.externalsched(add, 0)
except (IOError, error), e:
if set():
def fail(self = self, r = 'Problem connecting to tracker - ' + str(e)):
if self.last_failed:
self.errorfunc(r)
self.last_failed = True
self.externalsched(fail, 0)
try:
# Ҏ(gu)务器响应的数据解码。请参看BT协议规范?br>r = bdecode(data)
# ?peers 有效性?
check_peers(r)
if r.has_key('failure reason'):
self.errorfunc('rejected by tracker - ' + r['failure reason'])
else:
if r.has_key('warning message'):
self.errorfunc('warning from tracker - ' + r['warning message'])
self.interval = r.get('min interval', self.interval)
self.trackerid = r.get('tracker id', self.trackerid)
self.last = r.get('last')
peers = []
if type(p) == type(''):
for x in xrange(0, len(p), 6):
ip = '.'.join([str(ord(i)) for i in p[x:x+4]])
port = (ord(p[x+4]) << 8) | ord(p[x+5])
peers.append((ip, port, None))
else:
for x in p:
peers.append((x['ip'], x['port'], x.get('peer id')))
ps = len(peers) + self.howmany()
if ps < self.maxpeers:
if self.doneflag.isSet():
if r.get('num peers', 1000) - r.get('done peers', 0) > ps * 1.2:
self.last = None
else:
if r.get('num peers', 1000) > ps * 1.2:
self.last = None
# q里?connectQ即前面的Encoder::start_connection()Q?br> # xQ已l成功的完成了一ơ与 tracker 服务器的通信Qƈ且从它那里获取了 peers 列表Q下面就与这?peers 挨个建立q接Q至于徏立连接的q程Q就要追t到 Encoder cM了,且听下回分解?br> for x in peers:
# x[0]:ip, x[1]:port, x[2]:id
self.connect((x[0], x[1]), x[2])
except Error, e:
if data != '':
self.errorfunc('bad data from tracker - ' + str(e))
结Q?br>
q篇文章Q我们重点分析了BT客户端与 tracker 服务器通信的过E。我们知道了Q客L(fng)要每隔一D|_去q接 tracker
一ơ,它是以一个单独的U程执行的。这个线E在接受?tracker 的响应数据后Q交l主U程Q既ȝ序)来进行分析。主E序从响应数据中Q获?
peers 列表。然后调?Encoder::start_connection() 挨个与这?peers 试建立q接?/p>
]]>
②def __init__(self, Encoder, connection, id, is_local):
self.encoder = Encoder
self.connection = connection
如果是本地发赯接,那么 id 是对方的 idQ否?id ?None
self.id = id
如果q接是由本地发v的,那么 is_local ?TrueQ否则ؓ False
self.locally_initiated = is_local
self.complete = False
self.closed = False
self.buffer = StringIO()
self.next_len = 1
self.next_func = self.read_header_len
如果q接是由本地d发v建立的,那么需要向Ҏ(gu)发送一个握手消息。(如果不是由本C动发L(fng)Q那么就是被动徏立的Q那么不能在q里发送握手消息,而必d分析完对方的握手消息之后Q再d应一个握手西消息Q请看read_download_id() 中的处理?br> if self.locally_initiated:
connection.write(chr(len(protocol_name)) + protocol_name +
(chr(0) * 8) + self.encoder.download_id)
if self.id is not None:
connection.write(self.encoder.my_id)
return self.connection.get_ip()
return self.id
return self.locally_initiated
return self.connection.is_flushed()
if ord(s) != len(protocol_name):
return None
return len(protocol_name), self.read_header
if s != protocol_name:
return None
return 8, self.read_reserved
return 20, self.read_download_id
if s != self.encoder.download_id:
return None
q一步很重要Q?如果q接是由Ҏ(gu)发v的,那么Q给Ҏ(gu)发送一个握手消息。ؓ什么不在读完了 peer id 之后才发送这个消息了Q这是因?peer id 是可选的Q所以只要分析完 download id 之后Q就要立卛_送握手消息?br> if not self.locally_initiated:
self.connection.write(chr(len(protocol_name)) +
protocol_name +
(chr(0) * 8) +
self.encoder.download_id + self.encoder.my_id)
return 20, self.read_peer_id
if not self.id:
如果 peer id 是自己,那么出错?br> if s == self.encoder.my_id:
return None
for v in self.encoder.connections.s():
如果已经跟该 peer 建立了连接了Q那么也出错?br> if s and v.id == s:
return None
self.id = s
if self.locally_initiated:
self.connection.write(self.encoder.my_id)
else:
self.encoder.everinc = True
else:
如果 peer id ?xxx 不符Q那么出错了?br> if s != self.id:
return None
l = toint(s)
if l > self.encoder.max_len:
return None
return l, self.read_message
if s != '':
self.encoder.connecter.got_message(self, s)
return 4, self.read_len
return None
if not self.closed:
self.connection.close()
self.sever()
self.closed = True
del self.encoder.connections[self.connection]
if self.complete:
self.encoder.connecter.connection_lost(self)
self.connection.write(tobinary(len(message)) + message)
⑤在 Encoder::data_came_in() 中调用下面这个函敎ͼ表示某个q接上有数据可读。如果有数据可读Q那么我们就按照 BT 对等协议的规范来q行分析。。?br>def data_came_in(self, s):
if self.closed:
return
self.buffer.tell() 表示~冲Z剩下数据的长?br>那么 i pC:Z完成接下来的协议分析Q还需要多数据?
i = self.next_len - self.buffer.tell()
如果 i 大于所d的数据的长度Q那表示数据q没有读够,无法l箋协议分析Q需要等d_多的数据才能l箋Q所以只能退出?br> if i > len(s):
self.buffer.write(s)
return
否则表示q次d的数据已l够完成一步协议分析?br>只取满q一步协议分析的数据攑օ buffer 中(因ؓ buffer中可能还有上一步协议分析后留下的一些数据,要加在一PQ剩下的数据保留?s 中?br> self.buffer.write(s[:i])
s = s[i:]
self.buffer.reset()
self.buffer.truncate()
x = self.next_func(m)
except:
self.next_len, self.next_func = 1, self.read_dead
raise
if x is None:
self.close()
return
?x 中分解出 next_len?next_func?br>self.next_len, self.next_func = x
⑥那么BT对等协议分析的第一步是什么了Q?br>L(fng)初始化函敎ͼ
self.next_len = 1
self.next_func = self.read_header_len
昄Q第一步协议分析是?read_header_len() 来完成的?br>在BT源码中,有多处采用了q种协议分析的处理方式?/p>
def __init__(self, connecter, raw_server, my_id, max_len,
schedulefunc, keepalive_delay, download_id,
max_initiate = 40):
self.raw_server = raw_server
self.connecter = connecter
self.my_id = my_id
self.max_len = max_len
self.schedulefunc = schedulefunc
self.keepalive_delay = keepalive_delay
self.download_id = download_id
最大发L(fng)q接?br> self.max_initiate = max_initiate
self.everinc = False
self.connections = {}
self.spares = []
schedulefunc(self.send_keepalives, keepalive_delay)
def send_keepalives(self):
self.schedulefunc(self.send_keepalives, self.keepalive_delay)
for c in self.connections.s():
if c.complete:
c.send_message('')
L(fng) download.py ?Rerequester cȝ初始化函敎ͼ其中传递的一个参数是 encoder.start_connection?br>再看 Rerequester.py 中,Rerequester::postrequest() 的最后,
for x in peers:
self.connect((x[0], x[1]), x[2])
q里调用?connect() 是初始化的时候传递进来的 encoder.start_connectionQ也是下面q个函数了?br>也就是说Q当客户端从 tracker 服务器那里获取了 peers 列表之后Q就逐一向这?peers d发vq接?br> def start_connection(self, dns, id):
if id:
跟自׃用徏立连接?br> if id == self.my_id:
return
如果已经与对方徏立vq接Q也不再建立q接
for v in self.connections.s():
if v.id == id:
return
if len(self.connections) >= self.max_initiate:
如果I闲q接数还于 “最大发赯接数”Q那么把Ҏ(gu)?ip 先放到spares中,{以后网l稍微空闲一点的时候,再从 spares 中取出来Q实际去建立q接?br> if len(self.spares) < self.max_initiate and dns not in self.spares:
self.spares.append(dns)
return
try:
调用 RawServer::start_connection 与对方徏立TCPq接
c = self.raw_server.start_connection(dns)
创徏 Connection 对象Q加入到 connections 字典中,注意Q最后一个参数是 TrueQ表C是q个q接是由本地d发v的。这P?Connection 的初始化函数中,会与Ҏ(gu)q行 BT 对等协议的握手?/p>
except socketerror:
pass
q个内部函数好像没有用到
def _start_connection(self, dns, id):
def foo(self=self, dns=dns, id=id):
self.start_connection(dns, id)
self.schedulefunc(foo, 0)
def got_id(self, connection):
for v in self.connections.s():
if connection is not v and connection.id == v.id:
connection.close()
return
self.connecter.connection_made(connection)
return self.everinc
def external_connection_made(self, connection):
创徏一?Connection 对象Q加入到 connections 字典中?br> self.connections[connection] = Connection(self, connection, None, False)
c = self.connections[connection]
if c.complete:
self.connecter.connection_flushed(c)
def connection_lost(self, connection):
self.connections[connection].sever()
关闭一个连接之后,q接数量可能没有达?#8220;最大连接数”Q所以如?spares 中有一些等待徏立的 ip Q现在可以取出来Q主动向Ҏ(gu)发vq接?br> while len(self.connections) < self.max_initiate and self.spares:
self.start_connection(self.spares.pop(), None)
self.connections[connection].data_came_in(data)
]]>
②def run(params):
try:
import curses
curses.initscr()
cols = curses.COLS
# endwin() De-initialize the library, and return terminal to normal status
curses.endwin()
except:
cols = 80
h = HeadlessDisplayer()
# 调用 download.py 中的 download 函数Q我们的重点{Ud download.py 文gQ注意,该文件及以后要分析的代码都在 BT源码?BitTorrent 子目录下?br>?nbsp; download(params, h.chooseFile, h.display, h.finished, h.error, Event(), cols, h.newpath)
if not h.done:
h.failed()
run(argv[1:])
【download.py?br>q个文g中,最主要的就?download() 函数Q客L(fng)所有的一切都是从q里开始。这个函C码比较多Q我必须挑出其中最重要的代码,其它的部分,后箋再分析?/p>
= errorfunc, maxconnects = config['max_allow_in'])
for listen_port in xrange(config['minport'], config['maxport'] + 1):
try:
rawserver.bind(listen_port, config['bind'])
break
except socketerror, e:
pass
else:
errorfunc("Couldn't listen - " + str(e))
return
myid, config['max_message_length'], rawserver.add_task,
config['keepalive_interval'], infohash, config['max_initiate'])
]]>
]]>
作者:马?br>日期Q?004-6-24
概述Q?br>相对?tracker 服务器来_BT客户端要复杂的多QBram Cohen ׃一q?full time 的时间来完成 BTQ我估计其中大部分时间是用在 BT 客户端的实现和调试上了?br>? ?BT 客户端涉及的代码比较多,我不能再象分?tracker 服务器那PC来就深入到细节之中去Q那L(fng)话,我写的晕晕糊p,大家看v来也不知所云。所以第一文章先来谈谈客L(fng)的功能、相兛_议,以及客户端的 M架构和相关的cȝ层次l构。这P从整体上把握之后Q大家在自己分析代码的过E中Q就能做到胸有成竏V?/p>
客户端的功能Q?/p>
不看代码Q只Ҏ(gu) BT 的相兛_理,大致可以推测Q客L(fng)需要完成以下功能:
1、解?torrent 文gQ获取要下蝲的文件的详细信息Qƈ在磁盘上创徏I文件?br>2、与 tracker服务?建立q接Qƈ交互消息?br>3、根据从 tracker 得到的信息,跟其?peers 建立q接Qƈ下蝲需要的文g片断
4、监听某端口Q等待其它peers 的连接,q提供文件片断的上传?/p>
相关协议Q?br>对客L(fng)来说Q它需要处理两U协议:
1、与 tracker 服务器交互的 track HTTP协议?br>2、与其它 peers 交互?BT 对等协议?/p>
M架构Q?br>从M上来看,BT客户端实际是以一个服务器的Ş式在q行。这一点似乎有些难以理解,但确实是q样?br>Z么是一个服务器了?
?
L(fng)的主要功能是下蝲文gQ但作ؓ一UP2P软gQ同时它必须提供上传服务Q也是它必d候在某一个端口上Q等待其它peers
的连接请求。从q一点上来说Q它必须以一个服务器的Ş式运行。我们在后面实际分析代码的时候,可以看到Q客L(fng)复用?RawServer
cȝ来实现网l服务器?br>客户端的代码Q是?download.py
开始的Q首先完成功?Q之后就q入服务器@环,在每一ơ@环过E中Q完成功?2??。其中,Rerequester c负责完成功?Q它通过
RawServer::add_task()Q向 RawServer d自己的Q务函敎ͼq个d函数Q每隔一D|间与 tracker
服务器进行通信。而Encoder、Connecter {多个类l合在一P完成功能3??/p>
cdơ结构:
BT 客户端涉及的cL较多Q我首先大致描述一下这些类的功能,然后l出它们的一个层ơ结构?/p>
1、RawServerQ负责实现网l服务器
2、RerequesterQ负责和 tracker 通信。它调用 RawServer::add_task() Q向 RawServer d自己的Q务函?Rerequester::c()?/p>
3、EncoderQ一U?Handlerc(在分?tracker 服务器时候提刎ͼQ负责处理与其它peers建立q接和以及对d的数据按照BT对等协议q行分析?/p>
Encoder cdEncrypter.py中,该文件中Q还有一?Connection c,而在 Connecter.py 文g中,也有一? Connection c,q两个同名的 Connection cL些蹊PZ区分Q我把它们重新命名ؓ E-Connection ? C-Connection?/p>
3.1、E-ConnectionQ负?TCP 层次上的q接工作
q两?
Connection 是有区别的,q是因ؓBT对等协议需要在两个层次上徏立连接。首先是 TCP 层次上的q接Q也是l过 TCP
的三ơ握手之后,建立q接Q这个连接由 E-Connection 来管理。在 Encoder::
external_connection_made() 函数中可以看刎ͼ一旦有外部q接到来Q则创徏一?E-Connection cR?/p>
3.2、C-ConnectionQ管理对{协议层ơ上的连接?br>?TCP q接之上Q是 BT对等协议的连接,它需要经qBT对等协议的两?#8220;握手”Q握手的l节大家ȝBT对等协议。过E是q样的:
Z便于q说Q我们假设一个BT客户端ؓ AQ另一个客L(fng)?X?br>?
果是Xd向A发vq接Q那么在TCPq接建立之后QA立刻利用q个q接向X发送BT对等协议?#8220;握手”消息。同PX在连接一旦徏立之后,?
A发送BT对等协议?#8220;握手”消息。A一旦接收到X?#8220;握手”消息Q那么它?yu)p?#8220;握手”成功Q徏立了BT对等协议层次上的q接。我把它叫做“对等q?
?#8221;。A 发送了一个消息,同时接收了一个消息,所以这个握手过E是两次“握手”?br>同样Q对X 来说Q因接是它主动发L(fng)Q所以它在发送完“握手”消息之后Q就{待A?#8220;握手”消息Q如果收刎ͼ那么它也认ؓ“对等q接”建立了?br>一?#8220;对等q接”建立之后Q双方就可以通过q个q接传递消息了?/p>
q样Q原来我所疑惑的一个问题也有了答案。就是:如果 X 需要从 A q里下蝲数据Q那么它会同 A 建立一个连接。假?A 又希望从 X 那里下蝲数据Q它需不需要重新向 X 发v另外一个连接了Q答案显然是不用Q它会利用已有的一条连接?/p>
也就是说Q不是Xd向A发v的连接,q是 A d?X发v的连接,一旦徏立之后,它们的效果是一L(fng)。这个同我们qx?C/Sl构的网l开发是有区别的?/p>
我们可以看到?E-Connection的初始化函数中,会主动连接的另一方发?#8220;握手”消息Q在 E-Connection::data_came_in() 中,会首先对Ҏ(gu)?#8220;握手”消息q行处理。这正是我上面所描述的情形?br>?E-Connection::read_peer_id() 中,是对“握手”消息的最后一?peer idq行处理Q一旦正无误,那么p?#8220;对等q接”完成Q?/p>
self.encoder.connecter.connection_made(self)
?Connecter::connection_made() 函数中,创Z理“对等q接”?C-ConnectinoncR所以,更高一层的“对等q接”是由 C-Connection 来管理的?/p>
3.3、ConnecterQ连接器Q管理下载、上传、阻塞、片断选择、读写磁盘等{?/p>
下蝲和上传不是孤立的Q它们之间相互媄响。下载需要有片断选择法Q上传的时候要考虑dQ片断下载之后,要写到磁盘上。上传的时候,也需要从盘d?br>q些dQ是?Connecter 来统一调度的?/p>
cdơ结构,我用~进来表CZU包含关pR?/p>
Encoder:
E-Connection
C-Connection
Upload
SingleDownloader
Connecter
ChokerQ负责阻塞的理
DownloaderQ?br> SingleDownloader
PickerQ片断选择{略
StorageWrapperQ?/p>
先写q些吧,有什么我再补充进来?/p>
RawServer q个cȝ作用是实C个网l服务器。关于网l编E的知识Q?/span> unix |络~程Q卷 1 》是最l典的书c,你如果对q块不了解,抽时间看看这本书?/span> RawServer 实现的是一U事件多路复用、非d的网l模型。它使用的是 poll() Q而不是我们常见的 select() Q关?/span> poll ?/span> select 的比较,也在?/span> unix |络~程Q卷 1 》中有介l)函数Q处理过E大致是q样的:
首先创徏一个监?/span> socket Q然后将q个 socket 加入 poll 的事件源Q?/span>
随后q入服务处理循环Q即Q?/span>
调用 poll() 函数Q这个函CdQ直到网l上有某些事件发生或者超时才q回l调用者;
?/span> poll() q回之后Q先查一下是否有没有处理的Q务,如果有,那么先完成这些Q务。然后根据事件类型进行处理?/span>
如果是连接请求(监听 socket 上的 POLLIN 事gQ到来,?/span> accept q个hQ如?/span> accept 成功Q那么就和一?/span> client 建立了连接,于是?/span> accept() 新创建的 socket 加入 poll 的事件源Q?/span>
如果在已l徏立的q接上(q接 socket 上的 POLLIN 事gQ,有数据可读,那么数据从 client 端读q来Q做q一步处理;
如果已经建立的连接已l准备好Q连?/span> socket 上的 POLLOUT 事gQ,可以发送数据,则检查是否有数据需要发送,如果有,那么发送数据给 client 端?/span>
Q所以, tracker 是一个单q程的服务器Qƈ没有用到U程。)
Bram Cohen 认ؓ软g的可l护性非帔R要,使代码易于维护的重要一条就是设计可重用的类Q?/span> RawServer 在设计的时候,充分考虑C可重用性,集中表现在两个地方:
1?span style="font-family: 'Times New Roman'; font-style: normal; font-variant: normal; font-weight: normal; font-size: 7pt; line-height: normal; font-size-adjust: none; font-stretch: normal;">
网l?/span> I/O 和数据分析处理分R?/span>|络服务器的事g多\复用、网l?/span> I/O 部分通常是固定不变的Q而数据在d之后Q进行分析处理的q程则是可变的?/span> RawServer 可变的数据处理工作Q交l另外一个抽象的c?/span> Handler Q实际上q没有这么一个类Q来处理。比如,?/span> tracker 服务器的实现中,具体使用的就?/span> HTTPHandler c,而在 以后要分析?/span> BT client 实现代码中,用到的具体的 Handler ?/span> Encoder cR?/span>
2?span style="font-family: 'Times New Roman'; font-style: normal; font-variant: normal; font-weight: normal; font-size: 7pt; line-height: normal; font-size-adjust: none; font-stretch: normal;">
采用d队列来抽象出d处理的过E?/span>RawServer l护了一个Q务队?/span> unscheduled_tasks Q实际是一个二元组?/span> list Q二元组的第一Ҏ(gu)一个函敎ͼW二Ҏ(gu)时旉Q。在初始化的时候,首先向这个队列中加入一个Q务: scan_for_timeouts() Q这P每隔一D|_服务器就会去查一下是否有q接时。如果有其它
RawServer 的成员函CQ对外暴露的有:
u __init__ Q(初始化函敎ͼ
u add_task() Q?/span>
在Q务列表中增加一Q务(一个Q务是一个函C及一个指定的时旉的组合)
u bind() Q?/span>
首先创徏一?/span> socket Q然后设|?/span> socket 的属性: SO_REUSEADDR ?/span> IP_TOS, Q这两个属性的具体含义请参考?/span> unix |络~程Q卷 1 》,另外q将 socket 讄为非d的。相对于d?/span> socket 来说Q非d?/span> socket 在网l?/span> I/O 性能上要提高许多Q但是与此同Ӟ~程的复杂度也要提高一些。象 tracker q种可能同时要处理成千上万个q发q接的服务器Q只能采用非d?/span> socket ?/span>
然后该 socket 和指?/span> ip 已经端口l定Q?/span>
最后把q个 socket 加入 poll 的事件源?/span>
u start_connection() Q?/span>
对外d建立一个连接,q个函数在处?/span> NAT I越的时候用CQ我们后面分析到 NAT I越的时候,再具体讲解?/span>
u listen_forever() Q?/span>
q个函数的功能就是实C我在前面描述的网l服务器的处理过E。我们看刎ͼ它唯一的参数是 handler Q?/span> handler 的作用就是封装了Ҏ(gu)据的具体处理?/span>
listen_forever() 把对|络事g的处理过E,交给?/span> handle_events() ?/span>
其它函数Q包?/span> handle_events() Q都是内部函敎ͼ也就是外部不会直接来调用q些函数Q?/span> Python 没有 c++ 那样 public ?/span> protected ?/span> private q样的保护机Ӟ python cȝ内部函数命名的惯例是以下划线开始,例如 RawServer 中的 _close_dead() {?/span>
u handle_events() Q?/span>
事g处理q程Q主要是Ҏ(gu)三种不同的网l事件分别处理,一是连接事Ӟ二是M件、三是写事g?/span>
if sock == self.server.fileno()
q段代码判断发生事g?/span> socket 是否是监?/span> socket Q如果是Q那么说明是q接事g?/span>
q接事g的处理:
通过 accept 来接受连接,q将新徏立的 socket 讄为非d?/span>
判断当前q接数是否已l达C最大|Z限制q发q接的数目,在初始化 RawServer 的时候,需要指定最大连接数目)Q如果已l达到最大|那么关闭q个新徏的连接?/span>
否则Q根据新?/span> socket 创徏一?/span> SingleSocket 对象Q( SingleSocket 装了对 socket 的操作。)这个对象加入内部的列表 single_sockets 中,以备后用?/span>
这个新 socket 加入 poll 的事件源
最后,调用 Handler ?/span> external_connection_made() 函数Q关于这个函敎ͼ在后面分?/span> HTTPHandler 时再讨论?/span>
if (event & POLLIN) != 0:
q段代码判断是否是读事g
M件的处理Q?/span>
首先h一下连接的最后更新时?/span> Q?/span> last_hit Q?/span>
然后d数据Q?/span>
如果什么也没读刎ͼ那么说明q接被关闭了Q在|络~程中,如果一个连接正常的被关闭,那么Q也会触发读事gQ只不过什么也M刎ͼ
否则Q调?/span> Handler ?/span> data_came_in() 函数来处理读到的数据?/span>
if (event & POLLOUT) != 0 and s.socket is not None and not s.is_flushed():
q段代码判断是否是写事gQ而且实有数据需要发送。在一个连接可以写的时候,׃发生写事件?/span>
写事件的处理Q?/span>
实际代码是在 SingleSocket ?/span> try_write() 函数中?/span>
在一个非d的连接上发送指定大的数据Q很可能在一ơ发送过E中Q数据没有被完全发送出去(只发送了一部分Q就q回了,所以,每次 write 之后Q必d断是否完全发送了数据。如果没有发送完Q那么下ơ有M件的时候,q得回来l箋发送未完得数据。这也是q个函数叫做 try_write 的原因吧?/span>
try_write() 在最后,要重新设|?/span> poll 的事件源。如果数据全部发送完毕了Q那么只需要监听读事gQ?/span> POLLIN Q否则,既要监听MӞ也要监听写事Ӟ POLLOUT Q,q样Q一旦连接变的可写,可以l箋剩下的数据发送出厅R?/span>
u scan_for_timeouts() Q?/span>
d处理函数Q它首先把自w加入未处理d队列中,q样Q经q一D|_可以保证q个函数再次被调用,从而达到周期性调用的效果?/span>
它检查每个连接是否超q指定时间没有被hQ如果是Q则该连接可能已l僵死,那么它关闭这个连接?/span>
u pop_unscheduled() Q?/span>
从Q务列表中弹出一个未处理的Q务?/span>
?/span> RawServer 配合使用的是 SingleSocket c,q是一个辅助类Q主要目的是装?/span> socket 的处理吧。包括数据的发送,都交l它来处理了。这个类比较单,大家可以自己ȝQ我׃|嗦了?/span>
以上是对 RasServer 的具体实现的一个分析,可能读者看的还是晕晕糊p,没办法,q是必须自己ȝ源代码,然后在遇到问题的时候,回头再来看这文章,才会有帮助。如果不亲自看源码,l究是纸上谈c?/span>
我们再来结一下?/span>
RawServer 装了网l服务器的实现细节,它实C一U事件多路处理、非d的网l模型。它主要负责建立新的q接Q从|络d和发送数据,而对d的数据的具体处理工作Q交l?/span> Handler cL处理Q从而把|络 I/O 和数据处理分d来,使得 RawServer 可以重用?/span> Handler cL在调?/span> listen_forever() 的时候,p用者传递进来的Q具体到 tracker 服务器,是 HTTPHandler 。有?/span> RawServer Q?/span> tracker 可以作Z个网l服务器q行了?/span>
下一节,我们开始分析具体实?/span> tracker HTTP 协议处理?/span> HTTPHandler cd Tracker cR?/span>
上一我们讲刎ͼ RawServer 只负责网l?/span> I/O Q也是从网l上d和发送数据,至于d的数据如何分析,以及应该发送什么样的数据,则交l?/span> Handler cL处理。如果是?/span> c++ 来实现的话,那么 Handler 应该是一个接口类Q提供几个虚函数作ؓ接口Q,但是 python 动态语a的特性,q不需要专门定义这么一个接口类Q所以实际上q没?/span> Handler q么一个类。Q何一个提供了以下成员函数的类Q都可以作ؓ一?/span> Handler cL?/span> RawServer 配合Q它们是Q?/span>
external_connection_made() Q在建立新的q接的时候被调用
data_came_in() Q连接上有数据可ȝ时候被调用
connection_flushed() Q当在某个连接上发送完数据之后被调?/span>
HTTPHandler 是q样一?/span> Handler c,它具备以上接口?/span>
HTTPHandler 代码很少Q因为它把主要工作又交给 HTTPConnection 了?/span>
我们?/span> HTTPHandler cȝq几个函敎ͼ
l external_connection_made() Q?/span>
每当新来一个连接的时候,创Z?/span> HTTPConnection cR?/span>
l data_came_in() Q?/span>
当连接上有数据可ȝ时候,调用 HTTPConnection::data_came_in() 。我们接下去?/span> HTTPConnection::data_came_in() ?/span>
我们知道Q?/span> BT client 端与 tracker 服务器之间是通过 tracke HTTP 协议来进行通信的?/span> HTTP 协议分ؓhQ?/span> request Q和响应Q?/span> response Q,具体的协议请看相关的 RFC 文档。我q里单讲一下?/span>
?/span> tracke 服务器来_它读到的数据?/span> client 端的 HTTP h?/span>
HTTP h以行为单位,行的l束W是“回R换行”Q也是 ascii 字符 “ \r ”?#8220; \n ”?/span>
W一行是h?/span> URL Q例如:
GET /announce?ip=aaaaa;port=bbbbbbb HTTP/1.0
q行数据被空格分Z部分Q?/span>
W一部分 GET 表示命oQ其它命令还?/span> POST ?/span> HEAD {等Q常用的是 GET 了?/span>
W二部分是请求的 URL Q这里是 /announce?ip=aaaaa;port=bbbbbbb 。如果是普通的上网览|页Q那?/span> URL 是我们要看的网在?/span> web 服务器上的相对\径。但是,q里?/span> URL 仅仅是交互信息的一U方式, client 端把要报告给 tracker 的信息,攑֜ URL 中,例子里面?/span> ip ?/span> port Q更详细的信息请?#8220; BT 协议规范”?/span> tracker 协议部分?/span>
W三部分?/span> HTTP 协议的版本号Q在E序中忽略?/span>
接下来的每一行,都是 HTTP 协议的消息头部分Q例如:
Host:www.sina.com.cn
Accept-encoding:gzip
通过消息_ tracker 服务器可以知?/span> client 端的一些信息,q其中比较重要的是 Accept-encoding Q如果是 gzip Q那么说?/span> client 可以?/span> gzip 格式的数据进行解压,那么 tracker 服务器就可以考虑?/span> gzip 把响应数据压~之后再传回去,以减网l流量。我们可以在代码中看到相应的处理?/span>
在消息头的最后,是一个空行,表示消息头结束了。对 GET ?/span> HEAD 命o来说Q消息头的结束,也就意味着整个 client 端的hl束了。而对 POST 命o来说Q可能后面还跟着其它数据。由于我们的 tracker 服务器只接受 GET ?/span> HEAD 命oQ所以在协议处理q程中,如果遇到IQ那么就表示处理l束?/span>
HTTPConnection::data_came_in() 用一个@环来q行协议分析Q?/span>
首先是寻找行l束W号Q?/span>
i = self.buf.index('\n')
Q我认ؓ仅仅?/span> “ \n ”q不严}Q应该找 “ \r\n ”q个序列Q?/span>
如果没有扑ֈQ那?/span> index() 函数会抛Z个异常,而异常的处理是返?/span> True Q表C数据不够,需要l读数据?/span>
如果扑ֈ了,那么 i 之前的字W串是完整的一行。于是调用协议处理函敎ͼ代码是:
self.next_func = self.next_func(val)
?/span> HTTPConnection 的初始化的时候,有这么一行代码:
self.next_func = self.read_type
next_func 是用来保存协议处理函数的Q所以,W一个被调用的协议处理函数就?/span> read_type() 。它用来分析 client 端请求的W一行。在 read_type() 的最后,我们看到Q?/span>
return self.read_header
q样Q在下一ơ调?/span> next_func 的时候,是调用 read_header() 了,也就是对 HTTP 协议的消息头q行分析?/span>
下面先看 read_type() Q?/span>
它首先把 GET 命o中的 URL 部分保存?/span> self.path 中,因ؓq是 client 端最关键的信息,后面要用到?/span>
然后查一下是否是 GET 或?/span> HEAD 命oQ如果不是,那么说明数据有错误。返?/span> None Q否?/span> return self.read_header
接下来我们看 read_header() Q?/span>
q其中,最重要的就是对I的处理,因ؓ前面说了Q空行表C协议分析结束?/span>
在检查完 client 端是否支?/span> gzip ~码之后Q调用:
r = self.handler.getfunc(self, self.path, self.headers)
通过一层层往后追查,发现 getfunc() 实际?/span> Tracker::get() Q也是_真正?/span> client 端发来的hq行分析Q以及决定如何响应,是由 Tracker 来决定的。是的,q个 Tracker 在我?/span> tracker 服务器源码分析系列的W一文章中已l看C。在创徏 RawServer 之后Q马上就创徏了一?/span> Tracker 对象。所以,要了?/span> tracker 服务器到底是如何工作的,需要我们深入进d?/span> Tracker c,那就是我们下一文章的工作了?/span>
在调用完 Tracker::get() 之后Q返回的是决定响应给 client 端的数据Q?/span>
if r is not None:
self.answer(r)
最后,调用 answer() 来把q些数据发送给 client 端?/span>
?/span> answer() 的分析,我们在下一分?/span> Tracker cȝ文章中一q讲解?/span>
l connection_flushed() Q?/span>
tracker 服务器用的是非阻塞的|络 I/O Q所以不能保证在一ơ发送数据的操作中,把要发送的数据全部发送出厅R?/span>
q个函数Q检查在某个q接上需要发送的数据Q是否已l全部被发送出MQ如果是的话Q那么关闭这个连接的发送端。(Z么仅仅关闭发送端Q而不是完全关闭这个连接了Q疑惑)?/span>
client ?/span> tracker 发一?/span> HTTP ?/span> GET hQƈ把它自己的信息放?/span> GET 的参CQ这个请求的大致意思是Q我?/span> xxx Q一个唯一?/span> id Q,我想下蝲 yyy 文gQ我?/span> ip ?/span> aaa Q我用的端口?/span> bbb 。。?/span>
tracker Ҏ(gu)有下载者的信息q行l护Q当它收C个请求后Q首先把Ҏ(gu)的信息记录下来(如果已经记录在案Q那么就查是否需要更斎ͼQ然后将一部分Qƈ非全部,Ҏ(gu)讄的参数已l下载者的hQ参与下载同一个文Ӟ一?/span> tracker 服务器可能同时维护多个文件的下蝲Q的下蝲者的信息q回l对斏V?/span>
Client 在收?/span> tracker 的响应后Q就能获取其它下载者的信息Q那么它?yu)可以根据这些信息,与其它下载者徏立连接,从它们那里下载文件片断?/span>
关于 client ?/span> tracker 之间通信协议的细节,?#8220; BT 协议规范”中已l给出,q里不再重复。下面我们具体分?/span> tracker 服务器的实现l节?/span>
从哪里开始?
要徏立一?/span> tracker 服务器,只要q行 bttrack.py E序p了,它最需要一个参敎ͼ是 –dfile Q这个参数指定了保存下蝲信息的文件?/span> Bttrack.py 调用 track.py 中的 track() 函数。因此,我们跟踪?/span> track.py 中去?/span> track() 函数?/span>
Track.py Q?/span> track()
q个函数首先对命令行的参数进行检查;然后这些参C存到 config 字典中。在 BT 中所有的工具E序Q都有类似的处理方式?/span>
接下来的代码Q?/span>
r = RawServer(Event(), config['timeout_check_interval'], config['socket_timeout'])
t = Tracker(config, r)
r.bind(config['port'], config['bind'], True)
r.listen_forever(HTTPHandler(t.get, config['min_time_between_log_flushes']))
t.save_dfile()
首先是创Z?/span> RawServer 对象Q这是一个服务器对象Q它?yu)实C个网l服务器的一些细节封装v来。不?/span> tracker 服务器用C RawServer Q我们以后还可以看到Q由于每?/span> client 端也需要给其它 client 提供下蝲服务Q因此也同时是一个服务器Q?/span> client 的实CQ也用到?/span> RawServer Q这P RawServer 的代码得C重用。关?/span> RawServer 的详l实玎ͼ在后面的节中进行分析?/span>
接着是创Z?/span> Tracker 对象?/span>
然后?/span> RawServer l定在指定的端口上(通过命o行传递进来)?/span>
最后,调用 RawServer::listen_forever() 函数Q得服务器投入q行?/span>
最后,在服务器因某些原因结束运行以后,调用 Tracker::save_dfile() 保存下蝲信息。这P一旦服务器再次投入q行Q可以恢复当前的状态?/span>
其它信息Q?/span>
1?span style="font-family: 'Times New Roman'; font-style: normal; font-variant: normal; font-weight: normal; font-size: 7pt; line-height: normal; font-size-adjust: none; font-stretch: normal;">
BT 源码的分布:?/span> BT 的源码展开之后Q可以看到有一?/span> python E序Q还有一些说明文件等{,此外q有一?/span> BitTorrent 目录。这?/span> python E序Q实际是一些小工具Q比如制?/span> file ?/span> btmakefile.py 、运?/span> tracker 服务器的 bttrack.py 、运?/span> BT client 端的 btdownloadheadless.py {等。而这些程序中Q用到的一?/span> python cȝ实现Q都攑֜子目?/span> BitTorrent 下面。我们的分析工作Q通常是从工具E序入手Q比?/span> bttrack.py Q而随着分析的展开Q则重点是看 BitTorrenet 子目录下的代码?/span>
BT 作?/span> Bram Cohen 在谈到如何开发可l护的代码的一文章中Q?/span> http://www.advogato.org/article/258.html Q,其中提到的一条就是开发一些小工具以简化工作,我想 BT 的这U源码结构,也正是作者思想的一U体现吧?/span>
2?span style="font-family: 'Times New Roman'; font-style: normal; font-variant: normal; font-weight: normal; font-size: 7pt; line-height: normal; font-size-adjust: none; font-stretch: normal;">
我们看到Q?/span> python 和我们以前接触的 c/c++ 不一L(fng)W一个地方就是它的函数在定义的时候,不用指定参数cd。既然这P那么Q在调用函数的时候,你可以传递Q意类型的参数q来。例如这L(fng)函数Q?/span>def foo(arg):
print type(arg)
你可以这h调用Q?/span>
a = 100
b = “hello world”
foo(a)
foo(b)
输出l果是:
<type ‘int’>
<type ‘str’>
q是因ؓQ第一ơ调?/span> foo() 的时候,传递的是一个整数类型,而第二次调用的时候,传递的是一个字W串cd?/span>
q种参数h动态类型的Ҏ(gu),?/span> c/c++ {传l的语言是所不具备的。这也是 python 被称为动态语a的一个原因吧?/span> C++ 的高U特性模板,虽然也得参数类型可以动态化Q但使用hQ远没有 python q么单方ѝ?/span>