BT源代码学习心得(十三):客户端源代码分析(对等客户的连接建立及其握手协议)
Author: wolfenstein
上一次我们分析到了一个客户是如何得获取到对等客户的信息,现在终于要开始建立连接了。这一次我们将分析两个对等客户之间的连接的建立以及连接对象为它们之间通信提供的基础框架设施。
Encoder.start_connection建立到某个对等客户的连接。dns参数是IP地址和端口号,id是对方的peerid。首先检查对方的IP地址是否在banned列表内,如果在,直接返回,就是说不会再和对方建立连接。这就是通过一个黑名单的机制,避免和某些对等客户连接。这个黑名单也有一些生成的策略,后面我们可以看到,通常是发现某个对等客户传来的错误数据过多就将其加入到黑名单中。然后,当然对方id不能等于自己的id,以及在已有的连接中进行查找,不能重复连接。然后是检查连接数,如果连接数大于某个配置值,那么将这个连接的信息暂存入spares列表,日后再取出来。接下来就可以让RawServer进行网络连接了。如果网络连接成功建立,那么一个新的Connection对象就会被创建,并且该网络连接的数据处理对象(data_came_in)也会被交给这个Connection对象。
现在我们来看看网络的另外一头,就是说对方收到连接后会执行什么代码。由于在Multitorrent中定义了一个SingleportListener来侦听本地端口,也就是说,所有的外部网络连接的处理对象都是这唯一的SingleportListener,那么很自然,对方收到连接后,SingleportListener.external_connection_made会被调用。注意到SingleportListener和Encoder都定义在BitTorrent/Encoder.py中,看来作者是认为它们关系比较紧密。
SingleportListener.external_connection_made中所做的事情也是创建一个Connection对象,并且完成网络连接的数据处理对象的重定位工作。
两种不同的情况都会有一个新的Connection对象被创建,但是初始化它们的参数不太相同,另外它们都会被维护到一个字典中,Encoder中的这个字典记录了某个种子文件下载任务的所有连接,而SingleportListener中的这个字典记录了所有外部来的连接(就是说,还不知道应该把它们交由哪个Encoder进行管理)。现在我们来看Connection对象被创建时所做的初始化工作。第一个参数是encoder,就是说该Connection对象属于哪个encoder管理,这个参数也有可能是SingleportListener,第二个参数就是由RawServer创建的SingleSocket对象,它通常是用来作为所有连接的字典中的关键字,另外可以用它来完成具体的网络读写操作。第三个参数是id,指的是对方的peerid,如果是外部连接(SingleportListener处理的),那么这个参数是None,即还不知道对方的peerid,最后一个参数是一个布尔值,说明该连接是本地发起的(Encoder.start_connection)还是外部连入的(SingleportListener.external_connection_made)。开始的初始化工作基本上是对一些变量的初始化,注意到_reader变量,_read_messages()函数是一个多次使用了yield关键字的函数,所以这里_read_messages没有被执行,然后下面的_next_len的赋值部分,使_read_messages()执行了一些,即开始的yield1。这样_next_len就等于1,且_read_messages()函数执行冻结在了这里。最后,如果是主动连接的话,那么就往网络上发送后面的那一串东西。如果不是主动连接就不用了。发送的那些数据就是BT通信协议的握手部分的内容。
现在就可以来看Connection.data_came_in函数是如何处理到来的网络数据。
_next_len表示的是下一条完整的消息的长度,_buffer是缓冲区中暂存的信息(因为还没有达到_next_len的要求),_buffer_len则是缓冲区中的信息的长度。每一次试图从网络数据s中得到组成下一个完整的消息的数据,因此首先计算长度,_next_len-_buffer_len说明要从s中得到多少数据。如果s中没有这么多数据就把s中的数据暂存到缓冲区中,然后就返回。这样下一次调用data_came_in时就可以继续组建需要的数据了。如果s中有足够的数据,则将其组成合适的长度(_next_len),放入_message中,以方便处理。然后让_read_messages()继续往下执行。如果s中还有数据,则while循环要继续进行。我们看到,在data_came_in的这种设计框架下,_read_messages()函数每次yield一个值,当它接下来恢复执行时,_message中就已经有它要的值了。
这样,_read_messages()就可以专门处理协议。我们现在就可以对比本地发起的网络连接的初始化过程中发出的那个握手字符串来分析握手协议。首先yield1,然后一个字节的数据进入了_message,这就是chr(len(protocol_name)),协议名称的长度。通过把这个字符还原成整数,看它是否和协议名称相同。接下来yieldlen(protocol_name),然后进入_message的数据就是protocol_name,检查看它是否是'BitTorrentprotocol',然后yield8,这是8个字节的保留串,不用进行任何处理,继续yield20。这是download_id的值,encoder.download_id是什么呢?就是种子文件的infohash。然后检查self.encoder.download_id,如果是None,那么说明这个Connection对象是SingleportListener建立的,就是说这是网络中来的连接,因此程序运行到这里就可以做的一件事情就是看看这个infohash到底是本地的哪个下载任务,更准确的说,这个Connection对象应该交给哪个Encoder进行管理。所以它调用了encoder.select_torrent(其实就是SingleportListener.select_torrent),这个函数从维护的torrents字典中根据infohash查找对应的Encoder,然后让Encoder.singleport_connection进行Connection的交接。在Encoder.singleport_connection中做的事情包括检查对方的IP是否在banned列表中,否则拒绝其连接。然后将Connection对象添加到自己维护的字典中,并且将其从SingleportListener的字典中删除,然后将Connection的encoder指向自己,这样这个Connection就正式归这个Encoder管理了。再返回到_read_messages()函数中,对encoder.download_id的检查就可以证明以上过程是否成功完成了。下面一个elif则是主动的连接,对方返回的download_id在_message中,如果和自己的encoder.download_id不符,则中断该连接。下面检查是否是本地发起的连接,如果不是本地发起的连接,则也向对方发送握手协议(这样对方的_read_messages()函数也可以开始运行了)。接下来yield20,得到peerid。这是对方的ID,Connection中保留的id都是对方的ID,自己的ID保留在Encoder中。下面的这一段代码对得到peerid进行处理,如果需要则保留到自己的id变量中,并且根据自己的Encoder的Connection字典进行检查,以避免两个对等客户在同一种子文件下载任务中的重复连接。
在握手协议的最后一步调用了Encoder.connection_completed,说明这个连接建立成功,可以正式进行数据的交互了。在这个函数中做的工作就是为这个Connection生成一个Upload和SingleDownload对象,并且把这个Connection交给Choker进行管理。回到_read_messages()中,下面我们可以看到,握手协议已经成功完成,开始传送其它数据。每一条消息都分割成一个四字节长的长度和消息本身,因此while循环中不断的yield4和yieldl,然后_got_message来处理每个消息。
通过这一次的分析,我们知道了两个对等客户之间的连接的握手协议,以及Encoder,Connection,Upload,SingleDownload这些基本对象在连接建立时的基本关系。下一次就可以开始分析BT通信协议中的其它部分。