BT客户端分析.docx
- 文档编号:30202791
- 上传时间:2023-08-07
- 格式:DOCX
- 页数:84
- 大小:67.14KB
BT客户端分析.docx
《BT客户端分析.docx》由会员分享,可在线阅读,更多相关《BT客户端分析.docx(84页珍藏版)》请在冰豆网上搜索。
BT客户端分析
12楼大中小发表于2005-7-510:
27 只看该作者
作者:
小马哥
日期:
2004-6-24
概述:
相对于tracker服务器来说,BT客户端要复杂的多,BramCohen花了一年fulltime的时间来完成BT,我估计其中大部分时间是用在BT客户端的实现和调试上了。
由于BT客户端涉及的代码比较多,我不能再象分析tracker服务器那样,走上来就深入到细节之中去,那样的话,我写的晕晕糊糊,大家看起来也不知所云。
所以第一篇文章先来谈谈客户端的功能、相关协议,以及客户端的总体架构和相关的类的层次结构。
这样,从整体上把握之后,大家在自己分析代码的过程中,就能做到胸有成竹。
客户端的功能:
不看代码,只根据BT的相关原理,大致可以推测,客户端需要完成以下功能:
1、解析torrent文件,获取要下载的文件的详细信息,并在磁盘上创建空文件。
2、与tracker服务器建立连接,并交互消息。
3、根据从tracker得到的信息,跟其它peers建立连接,并下载需要的文件片断
4、监听某端口,等待其它peers的连接,并提供文件片断的上传。
相关协议:
对客户端来说,它需要处理两种协议:
1、与tracker服务器交互的trackHTTP协议。
2、与其它peers交互的BT对等协议。
总体架构:
从总体上来看,BT客户端实际是以一个服务器的形式在运行。
这一点似乎有些难以理解,但确实是这样。
为什么是一个服务器了?
客户端的主要功能是下载文件,但作为一种P2P软件,同时它必须提供上传服务,也就是它必须守候在某一个端口上,等待其它peers的连接请求。
从这一点上来说,它必须以一个服务器的形式运行。
我们在后面实际分析代码的时候,可以看到,客户端复用了RawServer类用来实现网络服务器。
客户端的代码,是从download.py开始的,首先完成功能1,之后就进入服务器循环,在每一次循环过程中,完成功能2、3、4。
其中,Rerequester类负责完成功能2,它通过RawServer:
:
add_task(),向RawServer添加自己的任务函数,这个任务函数,每隔一段时间与tracker服务器进行通信。
而Encoder、Connecter等多个类组合在一起,完成功能3和4。
类层次结构:
BT客户端涉及的类比较多,我首先大致描述一下这些类的功能,然后给出它们的一个层次结构。
1、RawServer:
负责实现网络服务器
2、Rerequester:
负责和tracker通信。
它调用RawServer:
:
add_task(),向RawServer添加自己的任务函数Rerequester:
:
c()。
3、Encoder:
一种Handler类(在分析tracker服务器时候提到),负责处理与其它peers建立连接和以及对读取的数据按照BT对等协议进行分析。
Encoder类在Encrypter.py中,该文件中,还有一个Connection类,而在Connecter.py文件中,也有一个Connection类,这两个同名的Connection类有些蹊跷,为了区分,我把它们重新命名为E-Connection和C-Connection。
3.1、E-Connection:
负责TCP层次上的连接工作
这两个Connection是有区别的,这是因为BT对等协议需要在两个层次上建立连接。
首先是TCP层次上的连接,也就是经过TCP的三次握手之后,建立连接,这个连接由E-Connection来管理。
在Encoder:
:
external_connection_made()函数中可以看到,一旦有外部连接到来,则创建一个E-Connection类。
3.2、C-Connection:
管理对等协议层次上的连接。
在TCP连接之上,是BT对等协议的连接,它需要经过BT对等协议的两次“握手”,握手的细节大家去看BT对等协议。
过程是这样的:
为了便于述说,我们假设一个BT客户端为A,另一个客户端为X。
如果是X主动向A发起连接,那么在TCP连接建立之后,A立刻利用这个连接向X发送BT对等协议的“握手”消息。
同样,X在连接一旦建立之后,向A发送BT对等协议的“握手”消息。
A一旦接收到X的“握手”消息,那么它就认为“握手”成功,建立了BT对等协议层次上的连接。
我把它叫做“对等连接”。
A发送了一个消息,同时接收了一个消息,所以这个握手过程是两次“握手”。
同样,对X来说,因为连接是它主动发起的,所以它在发送完“握手”消息之后,就等待A的“握手”消息,如果收到,那么它也认为“对等连接”建立了。
一旦“对等连接”建立之后,双方就可以通过这个连接传递消息了。
这样,原来我所疑惑的一个问题也就有了答案。
就是:
如果X需要从A这里下载数据,那么它会同A建立一个连接。
假如A又希望从X那里下载数据,它需不需要重新向X发起另外一个连接了?
答案显然是不用,它会利用已有的一条连接。
也就是说,不管是X主动向A发起的连接,还是A主动向X发起的连接,一旦建立之后,它们的效果是一样的。
这个同我们平时做C/S结构的网络开发是有区别的。
我们可以看到在E-Connection的初始化函数中,会主动连接的另一方发送“握手”消息,在E-Connection:
:
data_came_in()中,会首先对对方的“握手”消息进行处理。
这正是我上面所描述的情形。
在E-Connection:
:
read_peer_id()中,是对“握手”消息的最后一项peerid进行处理,一旦正确无误,那么就认为“对等连接”完成,
self.encoder.connecter.connection_made(self)
在Connecter:
:
connection_made()函数中,就创建了管理“对等连接”的C-Connectinon类。
所以,更高一层的“对等连接”是由C-Connection来管理的。
3.3、Connecter:
连接器,管理下载、上传、阻塞、片断选择、读写磁盘等等。
下载和上传不是孤立的,它们之间相互影响。
下载需要有片断选择算法,上传的时候要考虑阻塞,片断下载之后,要写到磁盘上。
上传的时候,也需要从磁盘读取。
这些任务,是由Connecter来统一调度的。
类层次结构,我用缩进来表示一种包含关系。
Encoder:
E-Connection
C-Connection
Upload
SingleDownloader
Connecter
Choker:
负责阻塞的管理
Downloader:
SingleDownloader
Picker:
片断选择策略
StorageWrapper:
先写这些吧,有什么我再补充进来。
TOP
13楼大中小发表于2005-7-510:
28 只看该作者
作者:
小马哥
日期:
2004-6-28
由于Storage类比较简单,我直接在源码基础上进行注释。
掌握Storage,为进一步分析StorageWrapper类打下基础。
几点说明:
1、 Storage类封装了对磁盘文件的读和写的操作。
2、 BT既支持单个文件的下载,也支持多个文件,包括可以有子目录。
但是它并不是以文件为单位进行下载和上传的,而是以“文件片断”为单位。
这可以在BT协议规范以及另一篇讲BT技术的文章中看到。
所以,对于多个文件的情况,它也是当作一个拼接起来的“大文件”来处理的。
例如,有文件aaa和bbb,大小分别是400和1000,那么它看作一个大小为1400的大文件,并以此来进行片断划分。
3、 文件在下载过程中,同时提供上传,所以是以读写方式打开的,wb+和rb+都指的读写方式。
在下载完毕之后,改为只读方式。
4、 由于下载可能中断,所以在Storage初始化的时候,磁盘上可能已经存在文件的部分数据,必须检查一下文件的大小。
为了便于描述,我们把完整文件的大小称为“实际长度”,把文件当前的大小成为“当前长度”。
classStorage:
#files是一个二元组的列表(list),二元组包含了文件名称和长度,例如:
[(“aaa”,100),(“bbb”,200)]
def__init__(self,files,open,exists,getsize):
self.ranges=[]
#注意,这里是0l,后面的l表示类型是长整形,而不是01。
total=0l
so_far=0l
forfile,lengthinfiles:
iflength!
=0:
#ranges是一个三元组列表,三元组的格式是:
在“整个”文件的起始位置、结束位置、文件名。
BT在处理多个文件的时候,是把它们看作一个拼接起来的大文件。
self.ranges.append((total,total+length,file))
total+=length
#so_far是实际存在的文件的总长度,好像没有起作用
ifexists(file):
l=getsize(file)
ifl>length:
l=length
so_far+=l
#如果文件长度为0,则创建一个空文件
elifnotexists(file):
open(file,'wb').close()
#begins是一个列表,用来保存每个文件的起始位置
self.begins=[i[0]foriinself.ranges]
self.total_length=total
self.handles={}
self.whandles={}
self.tops={}
#对于每一个文件,,,
forfile,lengthinfiles:
#如果文件已经存在
ifexists(file):
l=getsize(file)
#如果文件长度不一致,说明还没有下载完全,则以读写(rb+)的方式打开文件。
ifl!
=length:
handles是一个字典,用来保存所有被打开文件(无论是只读还是读写)的句柄
whandles是一个字典,用来记录对应文件是否是以写的方式打开(读写也是一种写)。
self.handles[file]=open(file,'rb+')
self.whandles[file]=1(这里是数字1,而不是字母l)
#如果文件长度大于实际长度,那么应该是出错了,截断它。
ifl>length:
self.handles[file].truncate(length)
如果文件长度和实际长度一致,那么下载已经完成,以只读方式打开。
else:
self.handles[file]=open(file,'rb')
#tops是一个字典,保存对应文件的“当前长度”。
self.tops[file]=l(这里是字母l,不是数字1)
#如果文件并不存在,那么以读写(w+)的方式打开
else:
self.handles[file]=open(file,'wb+')
self.whandles[file]=1
#判断起始位置为pos,长度为length的文件片断,在Storage初始化之前,是否就已经存在于磁盘上了。
这个函数后面分析StoageWrapper类的时候会再提到。
如果已经存在,那么返回true,否则为false。
注意:
如果这个片断的部分数据已经存在于磁盘上的话,那么也返回false。
在分析StorageWrapper的时候,才发现这里分析的不对。
这个函数意思应该是:
判断起始位置为pos,长度为length的文件片断,在Storage初始化之前,是否已经在磁盘上分配了空间。
例如,大小为1024k的文件,如果获得了第1个片断(从256k到512k),那么这时候,磁盘上文件的大小是512k(也就是分配了512k),尽管第0个片断(从0到256k)还没有获得,但磁盘上会保留这个“空洞”。
defwas_preallocated(self,pos,length):
forfile,begin,endinself._intervals(pos,length):
ifself.tops.get(file,0)<end:
returnFalse
returnTrue
#将所有原来以读写方式打开的文件,改成只读方式打开
defset_readonly(self):
#mayraiseIOErrororOSError
forfileinself.whandles.keys():
old=self.handles[file]
old.flush()
old.close()
self.handles[file]=open(file,'rb')
#获取所有文件的总长度
defget_total_length(self):
returnself.total_length
这个函数意思是检查起始位置为pos,大小为amount的片断实际位置在哪里?
例如,假设有两个文件,aaa和bbb,大小分别是400和1000,那么pos为300,amount为200的文件片断属于哪个文件了?
它分别属于两个文件,所以返回的是
[(“aaa”,300,400),(“bbb”,0,100)],
也就是它既包含了aaa文件中从300到400这段数据,也包含了bbb文件从0到100这段数据。
def_intervals(self,pos,amount):
r=[]
#stop是这个片断的结束位置。
stop=pos+amount
#通过这个函数,可以首先定位在哪个文件中,注意,可能在多个文件中(如果某个文件过小,那么,一段数据可能跨越几个文件)
#通过例子来解释下面这句,假设begins=[100,200,400,1000],而pos=250,那么bisect_right(self.begins,pos)返回的是2,而p=bisect_right(self.begins,pos)–1就是1,这表示起始位置为250的文件“片断”,它至少属于第1个文件(从0开始算起),也就是起始为200的文件。
p=bisect_right(self.begins,pos)–1
#r是一个三元组的列表,三元组格式是(文件名,在该文件的起始位置,在该文件的结束位置)。
whilep<len(self.ranges)andself.ranges[p][0]<stop:
begin,end,file=self.ranges[p]
r.append((file,max(pos,begin)-begin,min(end,stop)-begin))
p+=1
returnr
#把从pos开始,amount长的数据从文件中读出来,转换成一个字符串
defread(self,pos,amount):
r=[]
forfile,pos,endinself._intervals(pos,amount):
h=self.handles[file]
h.seek(pos)
r.append(h.read(end-pos))
#把list转换为一个字符串
return''.join(r)
#把一段字符串写到相应的磁盘文件中。
defwrite(self,pos,s):
#mightraiseanIOError
total=0
forfile,begin,endinself._intervals(pos,len(s)):
#如果该文件并不是以写的方式打开的,那么改成读写的方式打开
ifnotself.whandles.has_key(file):
self.handles[file].close()
self.handles[file]=open(file,'rb+')
self.whandles[file]=1
h=self.handles[file]
#通过seek函数移动文件指针,可以看出来,文件不是按照顺序来写的,因为所获取的文件片断是随机的,所以写也是随机的。
#这里有一个疑问,假设获得了第二个文件片断,起始是1000,大小是500,而第一个片断还没有获得,那么文件指针要移动到1000处,并写500个字节。
这时候,文件的大小应该是1500,尽管前面1000个字节是“空洞”。
那么如果,直到结束,都没有获得第一个片断,又如何检测出来了?
(通过检查total?
)
h.seek(begin)
h.write(s[total:
total+end-begin])
total+=end-begin
#关闭所有打开文件
defclose(self):
forhinself.handles.values():
h.close()
wish.lijt@QQ:
11242538callme
TOP
14楼大中小发表于2005-7-510:
28 只看该作者
作者:
小马哥
日期:
2004-6-30
StorageWrapper的作用:
把文件片断进一步切割为子片断,并且为这些子片断发送request消息。
在获得子片断后,将数据写入磁盘。
请结合Storage类的分析来看。
几点说明:
1、 为了获取传输性能,BT把文件片断切割为多个子片断。
2、 BT为获取一个子片断,需要向拥有该子片断的peer发送request消息(关于request消息,参见《BT协议规范》)。
3、例如一个256k大小的片断,索引号是10,被划分为16个16k大小的子片断。
那么需要为这16个子片断分别产生一个request消息。
这些request消息在发出之前,以list的形式保存在inactive_requests这个list中。
例如对这个片断,就保存在inactive_requests下标为10(片断的索引号)的地方,值是如下的list:
[(0,16k),(16k,16k),(32k,16k),(48k,16k),(64k,16k),(80k,16k),(96k,16k),(112k,16k),(128k,16
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- BT 客户端 分析