posts - 56,  comments - 12,  trackbacks - 0

概要
  上一节我们分析了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.s():
                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.get()
            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.s():
                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对等消息了,这是下一节的内容。
posted on 2007-01-19 00:24 苦笑枯 阅读(633) 评论(0)  编辑  收藏 所属分类: P2P

只有注册用户登录后才能发表评论。


网站导航:
 
收藏来自互联网,仅供学习。若有侵权,请与我联系!

<2007年1月>
31123456
78910111213
14151617181920
21222324252627
28293031123
45678910

常用链接

留言簿(2)

随笔分类(56)

随笔档案(56)

搜索

  •  

最新评论

阅读排行榜

评论排行榜