我在大四上学期时曾做过BT客户端的移植工作。当时程序有一些bug,需要查看源代码才能解决。然后便进入了先是痛苦万分然后茅塞顿开最后十分享受的代码阅读。
互联网上讨论BT的的不少,但真正涉及底层协议细节和实现方案的不多。有感于当初资料匮乏的窘境,决心把自己的分析写出来,若此篇文档能够帮助到您,则是我万分的荣幸。
这是文档的Initial Draft,想必是错误多多。我很愿和大家一起讨论,改进这份文档。
虽然研究过底层代码,但自己并不是BT下载爱好者。以后也很有可能不会从事跟BT有关的工作。时间长了,一些东西难免会忘,若有力所不及的地方,请多见谅。
1.1 为什么要写这份文档
BitTorrent点对点文件传输协议(以下简称BT协议)及其客户端应用大行其道的今天,各种各样的客户端不胜枚举(可以参看http://wiki.theory.org/BitTorrentApplications), 而各种各样的BT技术论坛讨论的却都是有关客户端软件如何使用的问题,有关底层协议细节和实现方案的讨论少之又少。我碰巧有机会研究过一阵BT协议的原 理,也看过一部分源代码(CTorrent),虽然现在不再继续BT方面的研究了,但有感于当初看代码时遇到的资料的匮乏的窘境,便决心把自己的理解和心 得写出来,算是自己的一份总结(这也是我的本科毕业论文),也希望帮助对BT协议实现有兴趣的人尽快上手,少走弯路。
有关BT协议的论述主要有三篇文章:
1,BT官方网站上的协议解释:http://www.bittorrent.org/protocol.html。
2,Bittorrent Protocol Specification,http://wiki.theory.org/BitTorrentSpecification。
3,Incentives Build Robustness in BitTorrent,http://www.bittorrent.com/bittorrentecon.pdf。
这三篇文章从不同方面给出了BT协议从算法到实现的一个较为简略的描述。为了更深入地理解BT协议,自己动手写一个BT客户端或阅读一个BT客户端的源代码的工作是必不可少的。
1.2 客户端的选择
Bram Cohen是BT协议的创建者。根据这份协议,他写了BT的第一个客户端,也就是BitTorrent公司的产品:BitTorrent。可以说, BitTorrent的源码和BT协议是门当户对,要理解协议,先从BitTorrent的源码开始是最好不过的了。
但Bram Cohen是用Python语言写的BitTorrent,这给很多不懂Python的人(我也在内)带来了很多麻烦:为了看懂一份源码而去新学一份计算机编程语言是不是有些不值得呢?
好在BT客户端是如此之多,我们有很大的选择空间。除了Python,还有Java(主要是Azureus,国外非常流行的多平台的客户端)和C++(其它大部分客户端)写成的程序。
经过多方比较,我选择了CTorrent这个客户端。虽然CTorrent是用C++写成的,但仅仅算是一个轻量级(light- weighted)的C++软件。它的库函数依赖型很小,只用到了Open SSL库用来计算哈希值,所以可以工作在Linux, FreeBSD和MacOS平台。CTorrent没有图形界面,工作在命令行模式。
另外,libtorrent(http://www.rasterbar.com/products/libtorrent.html)也是一个值得一看的客户端。Libtorrent用到了很多C++的模板库(主要是boost),客户端的性能非常好,而且还提供库函数给其它程序调用。只是作者的C++水平实在太低,对这种重量级的软件掌握不了。
1.3 CTorrent简介
CTorrent是由YuHong写的一个BT客户端。它的代码大部分都可以看作是C代码,只是用到了C++的类概念,还有一小部分构造函数,析构函数,函数和操作符重载的代码。不懂C++的人只需有一些C++的基本知识就完全能看懂源代码了。CTorrent的主页是http://ctorrent.sourceforge.net,它遵循GPL。
作者在CTorrent主页上称自己为YuHong,这里有一篇他写完CTorrent后发的帖子:http://www.freebsdchina.org/forum/viewtopic.php?p=39082,想必是中国人吧。
用户在使用时发现CTorrent有一些bug,一个比较明显的例子是CTorrent下载完成后不会立即把缓存中的数据写入硬盘,这样如果按下 Ctrl-C结束程序的话会造成数据的不完整。CTorrent的最新版本是1.3.4(2004年9月7日发布),作者后面就没有再发布新版本,软件的 一些问题也没有得到修正。
虽然有一些bug,但得益于CTorrent是开源项目,很快就有人为CTorrent写了一些补丁(http://sourceforge.net/tracker/?group_id=91688&atid=598034)。其中一个叫Dennis Holmes的人贡献颇多,他为CTorrent打了很多patch,然后重新发布,取名为Enhanced CTorrent。
Enhanced CTorrent的主页是http://www.rahul.net/dholmes/ctorrent。目前已经更新到了ctorrent-dhn2版本,这个版本配合Dennis Holmes用Perl写的一个CTorrent Control Server,可以实现对Enhanced CTorrent运行状况的监控。
这篇CTorrent的源码分析是基于ctorrent-dhn1.2版本的,原因是由于我查看Enhanced CTorrent较早,那时还没有ctorrent-dhn2版本,再加上自己偷懒,没有赶在ctorrent-dhn2发布之前把文章写完……比较而 言,dnh1.2版本已经是一个相对稳定的版本了,dnh2的改进主要是在性能方面,而非bug fix(容我再强词夺理一句,我简略看过dnh2版本的代码,在dnh1.2的基础上,看懂dnh2是没有问题的)。
另外,Dennis Holmes虽然重新发布了CTorrent,但他本人对原作者是极为尊敬的。在他的dnh版本中,原封不动地保留了原先代码的痕迹,自己的改动也加上了 相应的注释。虽然CTorrent有一些bug,但正如Dennis Holmes所言:谁又说其它客户端没有bug呢?我的这篇源码分析也统一称CTorrent和Enhanced CTorrent为CTorrent,只有在需要两个版本比较时才区分开来。
2. 准备工作
2.1 知识储备
要看懂CTorrent源码和本篇源码分析,读者需要具备如下知识:
1,前面列举的BT协议的大致了解。
2,网络socket编程方面的基本知识,主要是select()函数的使用。
3,至少会C语言,了解C++的基本使用方法(主要是类,构造函数,析构函数和重载)。
2.2 我对本篇源码分析的说明
1, 源代码中如果出现一些乱码(特别是在终端中查看时),设置:
$export LANG=C
即可看到原作者写的中文注释。
2, 源码解说一般采取流程图的形式,有一些函数的具体功能不是很集中,画流程图也表示不出前后联系来,就直接写了步骤分析。有些源码比较晦涩的,会直接分析源代码。
3, 源代码中的全部变量都有分析。大部分函数都有说明,少数特别简单的函数和见名知意的函数没有说明。
4, 源代码中看似简单的表述实际蕴含着及其严格的操作要求(例如宏P_HANDSHAKE的意思是可以进行握手通信了,而不是正在进行握手通信或者已经完成握手通信了)。所以必须正确理解源代码各个宏,变量,函数的确切含义,才能真正理解程序的流程和作用。
5, 分析源码的最终目的是彻底理解BT协议的实现结构,以及BT通信性能卓越的原因。虽然程序中涉及BT协议算法的只有几个函数,但这几个函数是在其它大量代 码的基础上构建的。一些有关种子文件的制作和解析的代码虽然看似和BT通信关系不大,但若前面的基础没有理解正确,会给后面的算法分析带来很大的麻烦。
6, 原作者的C语言技巧相当高,enjoy it!
7, 本文中“函数”指的是当前正在分析的函数,而“程序”指的是整个CTorrent程序。
8, 本文中“消息”指的是peer发来的固定格式的消息,例如piece消息,bitfield消息等。“数据”指的是客户端要下载的东西,例如一个游戏,一段视频等。
9, 英文中种子文件有很多说法,如.torrent file, metainfo file,本文中均用它们的中文名:种子文件。
10,英文中关于BT协议的最小数据单元有很多说法,如slice,block,subpiece,本文中使用CTorrent源代码中的说法:slice。
3. 总述
3.1 CTorrent的命令行参数的意义
-h/-H:显示帮助命令
-x:只解码并显示种子文件信息,不下载。
-c:只检查已下载的数据,不下载。
-v:打开debug调试输出。
下载选项:
-e int 下载完毕后的做种时间(单位:小时),默认为72小时。
-p port 绑定端口,默认为2706。
-s save_as 重命名下载的文件,若是下载的是多个文件,则sava_as是包含多文件的目录。
-C cache_size 缓存大小,默认为16MB。
-f 强制做种模式,不进行SHA1 HASH检查。
-b bf_filename piece位图文件名,详见BitField::SetReferFile()。
-M max_peers 客户端最多与多少个peer通信。
-m min_peers 客户端至少与多少个peer通信。
-n file_number 多文件下,选择哪个文件去下载(例如第二个文件file_number就为2)。
-D rate 限制最大下载速率(单位:KB/s)。
-U rate 限制最大上传速率(单位:KB/s)。
-P peer_id 客户端通信的ID,默认为-CD0102-。
下载数据文件示例:
ctorrent -s new_filename -e 12 -C 32 -p 6881 eg.torrent
制作种子文件示例:
ctorrent -t file_to_make.avi -s a.torrent -u protocol://address/announce
3.2 CTorrent的状态栏的意义
CTorrent运行时输出格式如下:
$ / 1/10/40 [3/148/148] 2MB,1MB | 48,20K/s | 80,40K E:0,1
各项意义为:
/:表明客户端正在工作的符号,以”- / | /”循环。
1:种子数目。
10:客户端正在通信的非种子的peer数目。
40:tracker服务器知道的peer数,也是整个bt通信群的peer数。
3:客户端已经下载的piece数目。
148:数据文件全部的piece数目。
148:客户端可以得到的piece数目,若此数小于全部piece数目则不会下载到完整的数据。
2MB:客户端已经下载的数据量。
1MB:客户端正在上传的数据量。
48:客户端的平均下载速率(KB/s)。
20:客户端的平均上传速率(KB/s)。
80:客户端的即时下载速率(KB/s)。
40:客户端的即时上传速率(KB/s)。
0:客户端与tracker服务器通信失败的次数。
1:客户端与tracker服务器通信成功的次数。
3.3 各个类实现的具体实例
CTorrent程序使用了C++面向对象的特性。在程序中有一些类的实例(instance),分别代表了一个BT通信群中的各个对象。
3.3.1 BTCONTENT
BTCONTENT是btContent类实现的实例。它在程序中代表种子文件和本地数据文件。
3.3.2 PENDINGQUEUE
PENDINGQUEUE是PendingQueue类实现的实例。它在程序中代表由于与peer的暂时通信中断而搁置等待的slice链表的队列。
3.3.3 IPQUEUE
IPQUEUE是IpList类实现的实例。它在程序中代表从tracker服务器传来的peer列表的链表。
3.3.4 Self
Self是btBasic类实现的实例。它在程序中代表客户端自己。
3.3.5 WORLD
WORLD是PeerList类实现的实例。它在程序中代表所有正在与客户端通信的peer的链表
3.3.6 Tracker
Tracker是btTracker类实现的实例。它在程序中代表tracker服务器。
BT下载之所以性能出众是由BT协议所规定的一系列机制所保证 的。判断一个BT下载软件性能优秀与否则是看这个软件对BT协议中下载机制的执行情况。BT协议主要规定了两大类机制保证其性能(详细信息请参照” Incentives Build Robustness in BitTorrent”):
3.4.1 Piece选择机制
3.4.1.1 初始模式(Initial Mode):Random First Piece。
当客户端刚开始运行时,它一个完整的piece也没有,这时需要尽快下载到一个piece以便可以提供上传服务。此时的算法为:第一个随机piece。客户端会随机找到一个piece,然后下载。
CTorrent随机选择piece,而且更进一步采取了一种加速下载的办法:虽然此时客户端没有piece,但应该有向其它peer的申请slice的队列了。客户端只要比较这些队列哪个最短,优先下载最短的队列即可最快获得第一个piece。
函数PeerList::Who_Can_Duplicate()实现了此算法的代码。
3.4.1.2 一般模式(Normal Mode):Strict Priority和Rarest First。
1,严格优先(Strict Priority)
一旦某个slice被申请,则这个slice所在的piece中的其它slice会先于其它piece的slice被申请。这样做可以尽量使最先申请的piece最先被下载完毕。
这条规则看似简单而且公平,但实现起来非常困难:BT协议规定一个piece中的多个slice可以向多个peer申请,而客户端又同时从多个peer处申请了多个piece中的slice,这么多数据传输队列同时进行,要保证严格优先是非常困难的。
CTorrent在设计时采取了一种比较简单的方法:一旦向某个peer申请了某个slice,则这个piece中的所有slice均向这个 peer申请。为了保证尽快将一个piece下载完成,CTorrent会找出当前正在与之通信的那个peer(正在通信的peer通常比较活跃),然后 把所有slice请求队列中最慢的那个队列找出来,交给这个peer去下载。
函数PeerList::Who_Can_Abandon()实现了此算法的代码。
2,最少优先(Rarest First)
客户端下载时选择所有peer拥有最少的那个piece优先下载。
函数BitField::Random()是有关piece选择机制的代码,但它只是随机选择了piece,没有实现最少优先。
3.4.1.3 结束模式(Endgame Mode)
由于每一个piece只向一个peer申请,当peer数大于还没有申请的piece数时,客户端便进入了结束模式。此时客户端可以向所有的peer发送还没有下载完毕的slice的请求,以便尽快下载完毕好做种子。
CTorrent变相实现了这个算法,它会找出当前正在与之通信的那个peer(正在通信的peer通常比较活跃),然后把所有slice请求队列中最长的那个队列找出来,交给这个peer去下载。
函数PeerList::Who_Can_Duplicate()实现了此算法的代码。
函数PeerList::Who_Can_Duplicate()和PeerList::Who_Can_Abandon()的调用环境均是函数btPeer::RequestPiece(),应将这三个函数一起查看才能清楚piece选择机制的实现。
3.4.2 Choking算法
Choking, Unchoking, Optimistic Unchoking三个算法是保证BT下载公平的基石,即“一报还一报”,“人人为我,我为人人”。具体实现请参照PeerList::UnChokeCheck()。
总的来说,CTorrent程序简单而巧妙地实现了BT协议中的保证下载性能的核心算法。只是最少优先算法没有实现,会给BT通信群的稳定性带来一定的影响。不过,这个问题已经在CTorrent-dnh2版本中得到了改正和优化。
点对点(Peer to Peer)通信是BT协议最大的特色,它充分利用了互联网上各个下载终端的带宽,使得由服务器上传速率有限所带来的瓶颈问题得以解决。但除此以外,BT协 议本身还通过piece选择机制优化了下载:由于数据以slice的形式分块下载,一般一个slice只有32KB,即使所有的peer的上传速率都很 慢,但slice是如此之小,以至于从一个很慢的peer处获得一个slice所需时间也极少,BT客户端只需合理安排对slice的请求,在较活跃的 peer和下载较慢的slice中作出相应的调整和搭配,即可获得较高的下载速率。
打个比方,火车站检票口处会有多个检票员(peer)在检票。每个人(slice)手里都拿着一张票,排成很多条并排的队伍(slice队列)等 候检票。由于检票员的检票速度有快有慢,控制中心(客户端程序)只需适时作出调整,将较长的队列分配给较快的检票员即可做到全体乘客的快速通过。
总结起来,BT协议的精髓便是通过化整为零,主动选择来充分利用各个下载终端的带宽,配合以相应的公平机制,保证整个BT通信群的高性能和高稳定性。
此部分文档较多,详情请见文档下载部分。
就像Eric Raymond在他的《大教堂和市集》中所说的,一个一致而稳定的系统(linux)奇迹般地从那个有着各种不同议程和方法的乱哄哄的市集中产生了。 CTorrent充分体现了这样一条道路:它刚发布时并不稳定,性能也不佳,但因为它是开源的,很多热心人都加入到讨论中来,找bug,改代码――现在的 CTorrent已经今非昔比,它性能出色,资源占用极低,并且代码浅显易懂,易于移植(您可以毫不费力地把它移植到机顶盒,PDA甚至Windows 中),是一个非常出色的客户端。CTorrent的所有发布版本,都遵循GPL并有相应的代码发放,您可以自由地阅读,修改,重新发布……只要它们遵守 GPL。大家受惠其中,又回报于它,这样一种其乐融融的开发和维护模式,让我如痴如醉……
但在这美好的憧憬中,已经出现了一些危险的因素:BT客户端是如此之多,有些客户端自己定义了一些优化性能的机制,如果很多人都使用这样的客户 端,会带来非常出色的性能。但由于这些客户端并没有开放源代码(这并没有什么不对,源码开放与否是开发者的自由),也没有说明它们到底是使用了何种机制来 提高性能,这就导致了其它客户端与其通信时不能达到最优的效果。一旦各种各样的客户端形成了各自用户群上的优势,基于相同BT协议的BT客户端软件必然会 走上一条相互不能很好兼容的道路。或许,分裂,是一条不该有的归宿。
但我相信,这一切,不是开源之罪。
BT协议实现了大家共享数据,互通有无,每个人都无私奉献自己的数据的人类理想。在这个实现过程中,BT协议力保公平:一报还一报,上传越快,下载 也就越快。同时,也以宽容之心对待一些由于各种原因不能提供上传的客户端,为它们提供下载。可以说,BT协议是一个从协议内容本身就体现开源精神的协议, 而不是像其它一些协议,只是在协议的实现形式(即软件)上遵循开源,这是BT协议的特殊之处。
自由的软件需要自由的文档,本文使用GNU自由文件许可证(GNU Free Documentation License)。还是那句老话:作者水平有限,错误在所难免,若您发现了任何问题或有任何建议,欢迎与我联系。除此之外,请所有通过阅读本篇文档而对 BT协议和CTorrent客户端有所熟悉的人注意:不得随便改变客户端程序做损人利己的事情!
我在阅读和分析CTorrent源代码的过程中,除了分析程序本身有哪些可以改进的地方,还想到了一些如何改写程序以显著加快下载速率的方法(但 是这些方法是建立在对其它客户端的损害之上的)。一个熟悉CTorrent代码的人完全可以写出一个对自己有利而损害他人利益的客户端程序,从而在BT下 载时获益颇多。我想,这种想法应该永远被禁止付诸实践――虽然这种规范仅仅是在道义上的,但这对一个真正的程序员来说已经足够了。
我以最诚挚的敬意向BT协议的作者Bram Cohen,CTorrent的作者YuHong,Enhanced CTorrent的作者Dennis Holmes,以及所有为CTorrent作出贡献的人表示感谢,他们教给了我精神,思想,技巧和方法。同时,没有我的恩师方元老师的大力支持,这篇源码 分析只能是空中楼阁――感谢所有人!
最后,以Bram Cohen的一句话结束”CTorrent 程序源码分析”:
I decided I finally wanted to work on a project that people would actually use, would actually work and would actually be fun
5.5 结语
5.4 我的敬意
5.3 本篇文档的版权和莫做害群之马
5.2 BT的精神:共享,公平和宽容
5.1 开源和BitTorrent,不得不说的话
5. 后记
4. 源代码分析
3.4 BT协议的特性和CTorrent的实现情况