客户端与一个peer建立TCP连接后,首先向peer发送握手消息,peer收到握手消息后回应一个握手消息。
l 握手消息是一个长度固定为68字节的消息。消息的格式如下:
<pstrlen><pstr><reserved><info_hash><peer_id>
消息格式中一些参数的含义如表13-9所示。
表13-9 握手消息
参 数 | 含 义 |
pstrlen | pstr的长度,该值固定为19 |
pstr | BitTorrent协议的关键字,即“BitTorrent protocol” |
reserved | 占8字节,用于扩展BT协议,一般这8字节都设置为0。有些BT软件对BT协议进行了某些扩展,因此可能看到有些peer发来的握手消息这8个字节不全为0,不过不必理会,这不会影响正常的通信 |
info_hash | 与发往Tracker的GET请求中的info_hash为同一个值,长度固定为20字节 |
peer_id与发往Tracker的GET请求中的peer_id为同一个值,长度固定为20字节。一般从peer_id可以识别出BT软件的类型,例如,某peer发来的握手消息中peer_id的前8个字节为“-AZ2060-”,则可以断定对方使用的是Azureus;若为“-BCxxxx-”,x为数字,则对方使用的是BitComet。
对于除握手消息之外的其他所有消息,其一般的格式为:
<length prefix><message ID><payload>
length prefix(长度前缀)占4个字节,指明message ID和payload的长度和。message ID(消息编号)占一字节,是一个10进制的整数,指明消息的编号。payload(负载),长度未定,是消息的内容。
l keep_alive消息:<len=0000>
keep_alive消息的长度固定,为4字节,它没有消息编号和负载。如果一段时间内客户端与peer没有交换任何消息,则与这个peer的连接将被关闭。keep_alive消息用于维持这个连接,通常如果2分钟内没有向peer发送任何消息,则发送一个keep_alive消息。
l choke消息:<len=0001><id=0>
choke消息的长度固定,为5字节,消息长度占4个字节,消息编号占1个字节,没有负载。
l unchoke消息:<len=0001><id=1>
unchoke消息的长度固定,为5字节,消息长度占4个字节,消息编号占1个字节,没有负载。客户端每隔一定的时间,通常为10秒,计算一次各个peer的下载速度,如果某peer被解除阻塞,则发送unchoke消息。如果某个peer原先是解除阻塞的,而此次被阻塞,则发送choke消息。
l interested消息:<len=0001><id=2>
interested消息的长度固定,为5字节,消息长度占4个字节,消息编号占1个字节,没有负载。当客户端收到某peer的have消息时,如果发现peer拥有了客户端没有的piece,则发送interested消息告知该peer,客户端对它感兴趣。
l not interested消息:<len=0001><id=3>
not interested消息的长度固定,为5字节,消息长度占4个字节,消息编号占1个字节,没有负载。当客户端下载了某个piece,如果发现客户端拥有了这个piece后,某个peer拥有的所有piece,客户端都拥有,则发送not interested消息给该peer。
l have消息:<len=0005><id=4><piece index>
have消息的长度固定,为9字节,消息长度占4个字节,消息编号占1个字节,负载为4个字节。负载为一个整数,指明下标为index的piece,peer已经拥有。每当客户端下载了一个piece,即将该piece的下标作为have消息的负载构造have消息,并把该消息发送给所有与客户端建立连接的peer。
l bitfield消息:<len=0001+X><id=5><bitfield>
bitfield消息的长度不固定,其中X是bitfield(即位图)的长度。当客户端与peer交换握手消息之后,就交换位图。位图中,每个piece占一位,若该位的值为1,则表明已经拥有该piece;为0则表明该piece尚未下载。具体而言,假定某共享文件共拥有801个piece,则位图为101个字节,位图的第一个字节的最高位指明第一个piece是否拥有,位图的第一个字节的第二高位指明第二个piece是否拥有,依此类推。对于第801个piece,需要单独一个字节,该字节的最高位指明第801个piece是否已被下载,其余的7位放弃不予使用。
l request消息:<len=0013><id=6><index><begin><length>
request消息的长度固定,为17个字节,index是piece的索引,begin是piece内的偏移,length是请求peer发送的数据的长度。当客户端收到某个peer发来的unchoke消息后,即构造request消息,向该peer发送数据请求。前面提到,peer之间交换数据是以slice(长度为16KB的块)为单位的,因此request消息中length的值一般为16K。对于一个256KB的piece,客户端分16次下载,每次下载一个16K的slice。
l piece消息:<len=0009+X><id=7><index><begin><block>
piece消息是另外一个长度不固定的消息,长度前缀中的9是id、index、begin的长度总和,index和begin固定为4字节,X为block的长度,一般为16K。因此对于piece消息,长度前缀加上id通常为00 00 40 09 07。当客户端收到某个peer的request消息后,如果判定当前未将该peer阻塞,且peer请求的slice,客户端已经下载,则发送piece消息将文件数据上传给该peer。
l cancel消息:<len=0013><id<=8><index><begin><length>
cancel消息的长度固定,为17个字节,len、index、begin、length都占4字节。它与request消息对应,作用刚好相反,用于取消对某个slice的数据请求。如果客户端发现,某个piece中的slice,客户端已经下载,而客户端又向其他peer发送了对该slice的请求,则向该peer发送cancel消息,以取消对该slice的请求。事实上,如果算法设计合理,基本不用发送cancel消息,只在某些特殊的情况下才需要发送cancel消息。
l port消息:<len=0003><id=9><listen-port>
port消息的长度固定,为7字节,其中listen-port占两个字节。该消息只在支持DHT的客户端中才会使用,用于指明DHT监听的端口号,一般不必理会,收到该消息时,直接丢弃即可。
2.7 关键算法和策略
1.流水线作业
BT协议作为一种构建在TCP协议上的应用层协议,可以通过流水线作业来提高数据传输的效率。具体而言,当客户端向peer发送数据请求时(即发送request消息),一次请求多个slice(即在一个数据包中发送多个request消息请求多个slice)。假如客户端一次只发送一个slice请求,则peer给客户端发送完一个slice的数据后就进入等待,等待客户端发送新的数据请求。如果一次发送多个slice请求,则peer发送完一个slice后接着发送下一个slice,从而避免了等待,提高了数据传输的效率。事实上,HTTP协议的1.1版本就广泛地使用了流水线作业的思想,大大地提高了浏览器和Web服务器之间的传输效率。
2.片断选择算法
一个良好的片断选择策略对于提高下载速度至关重要,对于提高整个文件共享系统的性能也有重要影响。
片断选择的第一个策略是,一旦向某个peer发送对某个piece中的slice的请求后,则该piece中的其他slice也从该peer处下载,这样可以尽快地下载到一个完整的piece。因为某个peer拥有某个piece中的一个slice,则它必定拥有该piece的其他slice,并且如果peer愿意发送一个slice给客户端,它也应该愿意发送piece中的其他slice给客户端。该策略也被称为严格的优先级。
片断选择的第二个策略是,最少优先。即某个piece在所有peer中的拥有率最低,则优先下载该piece。这么做的优点是,第一,可以防止拥有这个piece的peer突然离开,导致某个piece的缺失,从而当前任何一个参与下载的peer都不能下载到一份完整的文件;第二,如果下载了某些拥有率较低的piece,则其他很多peer会向客户端请求数据,而要想从客户端下载到数据,那些peer就要提供数据给客户端下载,这样对于提高客户端的下载速度也是有帮助的。对于这个共享系统而言,优先下载拥有率较低的piece可以使得整个系统提高每个piece的拥有度,整个系统会趋向于最优。如果所有peer优先下载拥有率较高的piece,会使某些piece的拥有率进一步降低,而拥有这些低拥有率piece的peer一旦离开共享系统,则整个文件会越来越不完整,最后导致许多peer不能下载到一个完整的文件拷贝。
片断选择的第三个策略是,随机选择第一个要下载的piece。开始下载时,不能采用最少优先策略。原因在于,采用最少优先策略,如果某个piece的拥有率很低,那么下载到这个piece就相对较难。如果随机选择一个piece,那么更容易下载到该piece,一旦客户端下载到一个完整的piece,就可以提供给其他peer下载,而由于客户端向其他peer上传数据,会导致其他peer对客户端解除阻塞,从而有利于在起始阶段获得较高的下载速度。当然在下载到一些piece后,客户端应该采用最少优先策略来下载数据,这虽然会导致客户端的下载速度在短期内有所下降,但随后下载速度会有较大提高。
片断选择的第四个策略是,最后阶段模式。有时,从一个传输速度很慢的peer处下载一个piece会花费很长时间,在下载的过程中这不是什么大问题。但在下载接近完成时,如果发生这种情况,会导致客户端迟迟不能下载完成。为了解决这个问题,在最后阶段,客户端向所有peer发送对这个piece的某些slice的请求,一旦收到某个peer发来的slice,则向其他peer发送cancel消息,只从当前这个peer处下载。
3.阻塞算法
BT并不集中分配资源,每个peer有责任尽可能地提高自己的下载速度。peer从它可以连接的peer下载文件,并根据对方提供的下载速率给予同等的上传回报,对于合作者,提供上传服务,对于不合作的,就阻塞对方。阻塞是一种临时拒绝上传的策略,虽然上传停止了,但是下载仍然继续。在解除阻塞时,连接并不需要重新建立。因为阻塞过程中只是拒绝传输piece消息,其他消息,如have消息,interested消息仍可以传输。阻塞算法虽然不是BT协议一部分,但是它对提高性能是必要的。
每个客户端一直与固定数量的peer保持疏通(通常是4个),那么以什么方式来决定是否保持与某个peer疏通呢?通常的做法是,严格地根据当前的下载速度来决定哪些peer应该保持疏通。但计算当前下载速度是个大难题。当前的实现通常是计算最近10秒从每个peer处下载数据的速度。以10秒为间隔重新选择保持疏通(即解除阻塞)的peer,是为了避免频繁地阻塞和解阻塞,造成资源的浪费。实践表明,10秒足以使下载速度达到最大。
如果只是简单地为提供最高下载速率的4个peer提供上载服务,那么就没有办法发现那些空闲的连接是否有更好的下载速度。为了解决这个问题,在任何时候,每个peer都拥有一个称为“optimistic unchoking(优化非阻塞)”peer,这个连接总是保持疏通状态,而不管它的下载速率是多少。每隔30秒,重新选择一个peer作为优化非阻塞peer。30秒足以让该peer的上载能力达到最大。
一旦某个peer完成了下载,它不能再通过下载速率(因为下载速率已经为0了)来决定为哪些peer提供上载了。目前采用的解决办法是,优先选择那些从它这里得到更好下载速率的peer,保持与它们疏通。这样做的理由是尽可能的利用上载带宽。一旦某个peer完成了下载,那么它也就成为了种子。种子拥有一份完整的文件拷贝,并提供给其他peer下载。为了整个系统的性能,每个peer在完成下载后应该作为种子存在一段时间,作为对整个系统的回报。原始种子,即最初提供文件进行共享、并制作生成了种子文件,并把种子文件发布到Web服务器的种子,它至少应该存在到系统中生成另外一个种子时才能离开,否则当前参与下载的所有peer都不能获得一份完整的文件拷贝。
本节对BT协议的解释和分析参考了“BT协议规范”(Bittorrent Protocol Specification)和BT协议设计者所著的“BT性能卓越的原因”(Incentives Build Robustness in BitTorrent)一文。
| 系统结构设计 |
整个系统的模块结构如图13-1所示。
图13-1 系统模块结构图
整个系统各个模块的功能如下。
(1)种子解析:负责解析种子文件,从中获取Tracker服务器的地址,待下载文件的文件名和长度,piece长度,各个piece的hash值。
(2)连接Tracker:根据HTTP协议构造获取Peer地址的请求,与Tracker建立连接,解析Tracker的回应消息,从而获取各个peer的IP地址和端口号。
(3)与peer交换数据:根据peer的IP地址和端口号连接peer、从peer处下载数据并将已下载的数据上传给peer。
(4)出错处理:定义整个系统可能出现的错误类型,并对错误进行处理。
(5)运行日志:记录程序运行的日志,并保存到文件中以备查看和分析。
模块“与peer交换数据”是本系统的核心和主要构成部分,它又可以划分成如下几个子模块。
(1)peer管理:系统为每一个已建立TCP连接的peer构造一个peer结构体。该结构体的主要成员有:peer的IP地址和端口号、与该peer进行通信的套接字、该peer的id、当前所处的状态、发送缓冲区、接收缓冲区、数据请求队列、数据被请求队列、从该peer处已下载的数据量和向该peer上传的数据量、下载速度和上传速度。本模块负责管理peer链表,添加和删除peer结点。
(2)消息处理:peer与peer之间以发送和接收消息的方式进行通信。本模块负责根据当前的状态生成并发送消息,接收并处理消息。BitTorrent协议共定义了12种消息,其中对下载和上传数据最重要的是request消息和piece消息。request消息向peer发送数据请求,指明请求的是哪个piece的哪个slice。Peer接收到request消息后根据当前的状态,决定是否发送数据给对方。如果允许发送,则构造piece消息,数据被封装在该消息中。每当下载完一个正确的piece时,就向所有peer发送have消息通告已获得该piece,其他peer如果没有该piece就可以向peer发送数据请求,每次请求都是以slice为单位。
(3)缓冲管理:如果下载完一个piece就立即写入硬盘,这样会导致频繁读写硬盘,既影响速度(读写磁盘要花费较多的时间),又不利于保护硬盘(频繁读写磁盘会使硬盘寿命缩短)。为了解决这个问题,几乎所有的BT软件都在程序中增加了一个缓冲管理模块。将下载到的数据先缓存起来,等到下载到一定量的数据后再集中写入硬盘。peer请求一个slice的数据时,先将该slice所在的整个piece读入到缓冲区中,下次Peer再请求该piece的其他slice时,只常缓冲区中获取,避免了频繁读写硬盘。本模块负责维护一个16MB的缓冲区(大小可调),将下载到的数据保存在缓冲区中,并在适当时刻写入硬盘的文件中。
(4)位图管理:BT协议采用位图指明当前哪些piece已经下载,哪些piece还没有下载。每个piece占一位,值为0表示该piece还未下载到,为1则表明已经下载到该piece。本模块负责管理位图,客户端与peer建立了连接并进行握手之后,即发送位图给peer告知已下载到哪些piece,同时也接收对方的位图并将其保存在Peer结构体中。每下载到一个piece就更新自己的位图,并发送have消息给所有已建立连接的peer。每当接收到peer发来的have消息就更新该peer的位图。
(5)策略管理:BT协议的设计者为了保证整体性能而制定了许多策略,这些策略虽然没有写入BT协议,但已经成为事实上的标准,BT软件开发者一般都使用这些策略来保证程序的性能。本部分负责策略的管理,主要是计算各个peer的下载和上传速度,根据下载速度选择非阻塞peer,采用随机算法选择优化非阻塞peer,以及实现片断选择策略。
(6)信号处理:在运行过程中,程序可能会接收到一些信号,如SIGINT、SIGTERM,这些信号的默认动作是立即终止程序。这并不是所期望的。在程序终止前需要作一些处理,如释放动态申请的内存、关闭文件描述符、关闭套接字。