客户端源码分析之八:BT对等连接的建立过程
作者:小马哥
日期:2005-01-09
rstevens2008 At hotmail.com
版权所有,未经允许,不得转载
转载请注明出处: http://www.wlm.com.cn/openidea/leobbs.cgi
概要
上一节我们分析了BT客户端与tracker之间的通信过程。通过与 tracker 的通信,客户端获得了参与下载的其它peers 的列表。有了这些 peers 的信息,客户端就可以主动向它们发起连接,为进一步从它们那里获取所需要的文件片断做好准备。这些连接称为“主动连接”。因为连接的发起方是客户端自己。
同时,每个客户端在启动以后,都会监听某个端口,用于接受其它 peers 的连接请求。P2P的核心理念就是“公平、对等”,你向别人有所求(下载),就必须有付出(上传)。客户端在接受外来的连接请求之后,就会产生一个新的连接,称之为“被动连接”,因为连接的发起方是其它 peer。
无论是被动连接,还是主动连接,一旦连接建立之后,双方就要进行“BT对等协议”的握手。握手成功之后,双方才可以通过这个连接互相传递消息了。为什么要进行握手了?主要目的是为了防止一些错误的连接。这就好比地下党接头,暗号对上了,彼此才建立信任。
在这个示意图中,客户端 A 与 其它 peers B、C、D、E 都建立了连接。通过箭头来表示连接建立的方向。A主动与 B、D、E 建立连接;A被动接收C的连接。同时C与D、E与D、B与D之间也都有连接。这样,所有下载者之间就形成了一个网状的结构。
同时,这些下载者都要与 tracker 之间不断通信。
无论是被动连接,还是主动连接,一旦在“BT对等握手”成功之后,它们就没有任何区别了。下载通过这个连接进行,上传也是通过这个连接进行。
本文重点分析BT客户端如何主动向其它 peers 发起连接;BT客户端如何被动接收其它 peers 的连接请求;以及在连接建立成功之后,如何进行BT对等协议握手的过程。
客户端主动发起连接
【Encrypter.py】
上一节的最后,我们看到调用 Encoder::start_connection() 来向其它 peer 发起连接。所以,从这里开始看起。
class Encoder:
# start_connection() 需要两个参数:
dns:对方的ip、port 的组合。
id: 对方的 id。每个BT客户端都要创建一个唯一的 id 号,并且把这个 id 号报告给 tracker。Id号唯一标识了一个客户端。Id 号的计算方式在 download.py 中。
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 是自己,不连接
if id == self.my_id:
return
# 如果已经和这个peer建立了连接,也不再建立连接。
for v in self.connections.values():
if v.id == id:
return
# 如果当前连接数过多,暂时不发起连接
if len(self.connections) >= self.max_initiate:
# self.spares 起到缓存的作用。在当前连接数过多的情况下,把本次要连接的 peer 的 ip、port 缓存起来。一旦当前连接数小于设定值 max_initiate, 则可以从 spares 中取出备用的 peers。
if len(self.spares) < self.max_initiate and dns not in self.spares:
self.spares.append(dns)
return
try:
# 调用 RawServer::start_connection(),发起 TCP 的连接。RawServer的代码分析请参看“服务器源码分析”系列文章,不再赘述
# 返回的 c 是一个 SingleSocket 对象,它封装了 socket 句柄
# 如果出错,抛出 socketerror 异常
c = self.raw_server.start_connection(dns)
# 成功建立 TCP 连接。构造一个 Connection对象,加入 connections 字典中。注意,最后一个参数是 True,它表面这条连接是由客户端主动发起建立的。我们立刻去看 Connection 类的构造
self.connections[c] = Connection(self, c, id, True)
except socketerror:
pass
【Encrypter.py】
class Connection:
② def __init__(self, Encoder, connection, id, is_local):
self.encoder = Encoder
self.connection = connection #这个 connection 是 SingleSocket 对象,就是上面 RawServer::start_connection() 返回的值,它封装了对socket句柄的操作。名字起的不好,容易弄混淆。
self.id = id
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
# 如果由本地发起,那么给对方发送BT对等连接的握手消息。
④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)
客户端被动接受外来连接
客户端在与 tracker 通信的时候,已经把自己的 ip 和监听的 port 报告给了 tracker。这样,其它 peers 就可以通过这个 ip 和 port 来连接它了。例如上图中的C,就主动给 A 发一个连接,从C的角度来说,它是“主动连接”,但从A的角度,它是“被动连接”。
一旦有外来连接请求,就会调用 RawServer::handle_events(),下面是摘录的“Tracker 服务器源码分析之二:RawServer类”中的一段。
最后调用的是 Handle 的 external_connection_made(),对客户端来说,这个Handle 是 Encoder 类对象。所以,外来连接建立成功后,最后调用的是 Encoder:: external_connection_made():
③def external_connection_made(self, connection):
# 同样是创建一个新的 Connection 类,并加入 connections 字典中。但不同之处在于最后一个参数是 False,表明这个连接是由外部发起的。
self.connections[connection] = Connection(self, connection, None, False)
BT对等连接握手:第一步
如果是主动连接,那么一旦连接建立成功之后,就给对方发送一个握手消息,我们折回去看序号为 4 的代码:
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)
在《BT协议规范》中,如此描述握手消息:
对等协议由一个握手开始,后面是循环的消息流,每个消息的前面,都有一个数字来表示消息的长度。握手的过程首先是先发送19,然后发送协议名称“BitTorrent protocol”。19就是“BitTorrent protocol”的长度。
后续的所有的整数,都采用big-endian 来编码为4个字节。
在协议名称之后,是8个保留的字节,这些字节当前都设置为0。
接下来对元文件中的 info 信息,通过 sha1 计算后得到的 hash值,20个字节长。接收消息方,也会对 info 进行一个 hash 运算,如果这两个结果不一样,那么说明双方要下载的文件不一致,会切断连接。
接下来是20个字节的 peer id。
可以看到,最后两项就是 Encoder::download_id 和 Encoder::my_id。download_id是如何得来的?它是首先对 torrent 文件中 info 关键字所包含的信息进行 Bencoding 方式的编码(请看《BT协议规范》关于 Bencoding的介绍),然后再通过 sha 函数计算出的 hash(摘要)值。(相关代码都在 download.py 中)。在一次下载过程中,所有的下载者根据 torrent 文件计算出的这个 download_id应该都是一样的,否则就说明不处于同一个下载过程中。
至于 peer_id,可以看出是可选的。它的计算方式也在 download.py 中:
myid = 'M' + version.replace('.', '-')
myid = myid + ('-' * (8 - len(myid))) + b2a_hex(sha(repr(time()) + ' ' + str(getpid())).digest()[-6:])
seed(myid)
它用来唯一标识一个 peer。
握手过程已经完成了第一步,还需要第二步,接收到对方的握手消息,握手过程才算完成。所以接下去看在有数据到来的时候,是如何处理的。
BT对等连接握手:第二步
当TCP连接上有数据到来的时候, RawServer 会调用到 Encoder:: data_came_in()
【Encoder】
⑤def data_came_in(self, connection, data):
self.connections[connection].data_came_in(data)
进一步调用 Connection::data_came_in()
【Connection】
⑥def data_came_in(self, s):
#这个循环处理用来对BT对等连接中的消息进行分析
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.getvalue()
self.buffer.reset()
self.buffer.truncate()
try:
x = self.next_func(m) #调用消息分析函数,第一个被调用的是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
⑧def read_header_len(self, s):
# 协议的长度是否为 19?
if ord(s) != len(protocol_name):
return None
return len(protocol_name), self.read_header # 下一个处理函数
def read_header(self, s):
# 协议名称是否是“BitTorrent protocol”?
if s != protocol_name:
return None
return 8, self.read_reserved # 下一个处理函数
def read_reserved(self, s):
#8个保留字节
return 20, self.read_download_id # 下一个处理函数
def read_download_id(self, s):
对方 download_id 和自己计算出的是否一致?
if s != self.encoder.download_id:
return None
检查完 download_id,就认为对方已经通过检查了。
这里很关键!!!,需要仔细体会。这实际上就是在被动连接情况下,完成握手过程的处理。如果连接不是由本地发起的(被动接收到一个握手消息),那么给对方回一个握手消息。这里的握手消息发送处理和第一步是一样的
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 #下一个处理函数
def read_peer_id(self, s):
# Connection 类用来管理一个 BT 对等连接。在握手完成之后,就用对方的 peer_id 来唯一标识这个 Connection。这个值被保存在 self.id 中。显然,在握手完成之前,这个 id 还是空值。
if not self.id:
# 对方的peer_id 可千万别跟自己的一样
if s == self.encoder.my_id:
return None
唔,如果 peer_id 已经收到过,也不继续下去了
for v in self.encoder.connections.values():
if s and v.id == s:
return None
用对方的 peer_id 为 self.id 赋值,唯一标识这个 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
# OK,握手完成!!!
self.complete = True
self.encoder.connecter.connection_made(self)
return 4, self.read_len #下一个处理函数。从此以后,就是对其它BT对等消息的处理过程了。这是我们下一节分析的问题。
小结:
这篇文章重点分析了BT客户端主动发起连接和被动接收连接的过程,以及在这两种情况下,如何进行BT对等握手的处理。
在BT对等握手完成之后,连接的双方就可以互相发送BT对等消息了,这是下一节的内容。