上一篇博客中,客户端已连接到ed2k网络及客户端与服务器交互的eMule源码梳理,这里将开始搜索资源并下载及客户端与客户端交互的eMule源码梳理
emule 源码下载地址
http://download.csdn.net/detail/huang_rong12/9506732
搜索资源并下载,这是一个即包含和和服务器交互还包含与另一些客户端交互。所以会先说明和服务端交互的情况(搜索资源,选择资源),在说明下载(与客户端交互)。
搜索资源是发送关键词到服务器,服务器会根据关键词返回信息,当然这些信息是通过消息传递的具体消息如下:
6.2.9 搜索请求
客户发送给服务器的。消息使用用户的一个搜索字符串搜索一个文件。消息大小是可变的。搜索字符串包括布尔运算符’AND’,’OR’, ’NOT’。用户可以详细设置文件的类型大小也可以设置开始位置(例如:展示给我至少5个其他客户端的结果)。
名称 | 占用字节数 | 默认值 | 说明 |
Protocol | 1 | 0xE3 |
|
Size | 4 |
| 不包括标题和size字段的信息大小 |
Type | 1 | 0x16 | OP_SEARCHRESULT opcode |
Parsed search string | 不定 | NA | 下面所描述的搜索字符串结构 |
Result list | 不定 | NA | 搜索结果列表 |
Parsed search string | 不定 | NA | 解析搜索字符串,格式如下 |
File Type Constraint | 不定 | NA | 可选的。一个字符串约束。字符串值是Audio”, ”Video”, ”Pro” or ”Image”三者之一。类型域分别对应0x1 0x0 0x3 |
Min Size Constraint | 不定 | NA | 可选择的,一个整型约束。以兆字节计算的文件大小。类型域有4位:0x1 0x1 0x0 0x2 |
Max Size constraint | 不定 | NA | 可选择的,一个整型约束。以兆字节计算的文件大小。类型域有4位:0x2 0x1 0x0 0x2 |
Availability Constraint | 不定 | NA | 可选择的,一个整型约束。搜索文件客户数量的最小上限,类型域有4位:0x1 0x10x0 0x15 |
Filename Extension constrain | 不定 | NA | 可选择的,一个整型约束。类型域有3位:0x1 0x0 0x3 |
解析搜索字符串格式
解析字符串编码是通过二差树和’AND’,’OR’ ,’NOT’布尔运算符以及字符串操作数。二差树是按照先序编码进行的。操作是编码两位字节值是
The tree is encoded in pre-order . The operators0x0, 0x100, 0x200分别代表了’AND’, ’OR’ 和’NOT’。字符串按照TLV格式进行编码是一个以字节的值和一个两字节的长度。注意当字符串是以各自的时候它代表字符串操作(没有运算符)。以后的eMule 编码版本中指通过单独的字符串’AND’来编码搜索表达式,由空格代替’AND’。由’AND’运算符将连续的单词分开来组成一个句子。
可选择的约束格式
约束数列条目。每一个条目有’AND’描述符开始(2-byte0x00)紧跟着是编码约束。因此一个完整的搜索行格式是:<’search-string’ AND constraint1 AND constraint2 etc>就想下面图例中描述的一样。编码被分成三部分。
1.类型–一个字节的描述,表明是字符串(0x2)还是整型约束(0x3)。
2.值–一个定长字符串编码或者一个4字节的整型值
3.类型–一个3或4字节的约束类型描述(看上面的主要表)
图 6.1: 查找字符串编码实例
6.2.10 搜索结果
服务器发送给客户的搜索请求应答消息。这个消息通常是压缩的。大小不固定。
名称 | 占用字节数 | 默认值 | 说明 |
Protocol | 1 | 0xE3 |
|
Size | 4 |
| 不包括标题和size字段的信息大小 |
Type | 1 | 0x16 | OP_SEARCHRESULT opcode |
Result Count | 4 | NA | 这个消息中搜索结果的个数 |
Result list | 不定 | NA | 搜索结果列表 |
搜索结果列表项目的格式
下面的表格描述了一个搜索结果列表项目的格式。每一个搜索结果包含了一个唯一表示这个文件的哈西数和两外一个又有该文件的客户。还有几个描述文件属性的标记。这些标记将在下面描述:
名称 | 占用字节数 | 默认值 | 说明 |
File Hash | 16 | NA | 一个哈西值,唯一的标示了一个文件 |
Client ID | 4 | NA | 一个拥有该文件的peer的客户ID |
Client Port | 2 | 0x16 | 拥有文的客户端得端口号 |
Tag Count | 4 | NA | 后面的描述标记的个数 |
Tag list | 不定 | NA | 描述标记列表 |
注意大多数标记的位置是可以变动的,并不是固定的。标记编码的规则在本章开始处已经详细描述了。
名称 | 标记名称 | 标记类型 | 说明 |
File name | Integer 0x01 | String |
|
File size | Integer 0x02 | Integer |
|
File type | Integer 0x03 | String |
|
File format | Integer 0x04 | String |
|
Sources | Integer 0x15 | Integer | 这个文件可获得的资源数 |
Artist | String ”Artist” | String |
|
Album | String ”Album” | String |
|
Title | String ”Title” | String |
|
Length | String ”length” | Integer |
|
Bitrate | String ”bitrate” | Intege |
|
Codec | String ”codec” | Integer |
|
表6.1: 搜索结果标记列表
在eMule源码中和搜索相关的代码分析如下
搜索信息集-CSearchList
CSearchList是emule中的搜索列表,掌管emule中所有的搜索请求。CSearchFile是这个列表中的元素,代表了一次搜索的相关信息。它们的关系和之前描述的已知文件和已知文件列表有一些类似的地方。CSearchList的主要任务就是对其一个叫做list的类型为CSearchFile列表的内部变量进行维护,提供很方便得往这个列表中添加,删除,查询,变更等操作的接口。另外,每一个搜索都有一个ID,是一个32位的整数。CSearchList中记录了每个搜索目前搜到的文件个数和源的个数(m_foundFilesCount和m_foundSourcesCount)。
CSearchFile是CAbstractFile的另一个子类(CKnownFile也是),它保存了某个文件和搜索相关的信息,而不是这个文件本身的信息(这些信息在CAbstractFile中已经包括了),这些和搜索有关的信息就是都在哪些机器上有这个文件,以及哪个服务器上搜到的这个文件。甚至还可以向搜索文件添加预览。在这个类的定义中嵌套定义了两个简单的结构SServer和SClient,表示了该搜索文件的可能来源,服务器或者其它客户端。m_aClients和m_aServers是这两个简单结构的一个数组,CSearchFile自然也提供了对这个数组的操作的接口,方便CSearchList使用。
CSearchList对外提供了搜索表达的接口,即每当有一个新的搜索提交时CSearchList::NewSearch会建立一个新的搜索项,但是此时还没有任何对应的搜索文件,因此只是在文件个数和搜索ID的对应表(m_foundFilesCount和m_foundSourcesCount)中建立新的项目。另外当有搜索结果返回时ProcessSearchAnswer或ProcessUDPSearchAnswer能够对返回的包直接做处理,创建相应的搜索文件信息CSearchFile对象,并加入到自己的列表中。当然,要把重复的搜索结果去除,发现同一个hash的文件的多个源时也会给它们建立一个二级列表(CSearchFile::m_list_parent)。现在我们可以看出,CSearchList只负责和搜索有关的信息的储存和读取,本身并不进行搜索。
得到返回数据以后就可以下载了。这时候涉及的消息如下:
6.2.11 获得资源
客户向服务器发送一个请求来获得文件资源(其他的客户)。消息的大小是22字节。
名称 | 占用字节数 | 默认值 | 说明 |
Protocol | 1 | 0xE3 |
|
Size | 4 |
| 不包括标题和size字段的信息大小 |
Type | 1 | 0x19 | OP_ GETSOURCES opcode |
File hash | 16 | NA | 请求的文件哈西数 |
6.2.12 建立资源
服务器发送给客户端一个包含客户端所请求的文件资源(其他的客户)的消息。消息大小是不定的。
名称 | 占用字节数 | 默认值 | 说明 |
Protocol | 1 | 0xE3 |
|
Size | 4 |
| 不包括标题和size字段的信息大小 |
Type | 1 | 0x42 | OP_ FOUNDSOURCES opcode |
File hash | 16 | NA | 请求的文件哈西数 |
Sources Count | 1 | NA | 这个消息中的资源数 |
List of sources | 不定 | NA | 资源列表 |
资源列表项目的格式
下面的表格描述了资源列表项目的格式。每一个资源包括客户所拥有要下载文件的细节。
名称 | 占用字节数 | 默认值 | 说明 |
Client ID | 4 | NA | 拥有文件的eMule peer得Client ID |
Client Port | 2 | NA | 拥有文件的客户的端口 |
这些消息完成以后文件下载基本结束了(以上这些是必须的消息,还有一些消息是没介绍到的。)。前面已说明客户端与服务器交互的源码分析。这里不再说明,这里仅说明客户端与客户端的交互的eMule源码。
emule的通信协议-客户端和客户端之间的通信概述
客户端和客户端之间的TCP通信由CListenSocket和CClientReqSocket完成。这也是提供网络服务的应用程序的典型写法。其中CListenSocket只是CAsyncSocketEx的子类,只负责监听某个TCP端口。它只是内部有一个CClientReqSocket类的列表。而CClientReqSocket是CEMSocket的子类,因此它能够自动完成emule的packet识别工作。它有ProcessPacket和ProcessExtPacket来处理客户端和客户端之间的包,其中前者是经典的eDonkey协议的包,后者是emule扩展协议的包。
CListenSocket和CClientReqSocket类之间的关系和前面分析的列表类和它对应的成员类的关系是相似的,CListenSocket提供对自身的CClientReqSocket列表中的元素的增加,查询,删除等操作。同时也维护关于这些成员的一些统计信息。我们注意到CListenSocket在其构造函数中就把自己添加到CListenSocket类(theApp.listensocket,该类的唯一实际示例)的列表中。
CClientReqSocket类和CUpDownClient类之间存在着对应关系。它们都表示了另外一个客户端的一些信息,但是CClientReqSocket类主要侧重在网络数据方面,即负责两边的互相通信,而CUpDownClient类负责的是从逻辑上对网络另一边的一个客户端进行表达。由于ed2k网络中文件是放在网络中的客户端上,当一个客户端请求下载资源时,整个过程中可能会涉及网络中多个客户端要上传文件(之所以是多个,是由于文件在ed2k网络中可能是分段的,至于分段详细细节后面会详细说明。)而且同一个客户端可能要把文件上传到不同另外的客户端(这时就会出现排队问题,先传给哪个)。接下来对于这些问题,会一个个解释,在ed2k中有一种信用机制,为了避免只下载不分享的问题,ed2k中分享文件个数与信用挂钩,所以出现排队时会选择信用较高的先上传。
下载任务即部分文件的表示
CPartFile类是emule中用来表示一个下载任务的类。从它的名字也可以看出来,这就是一个还没有完成的文件。当一个下载任务被创建时,emule会在下载目录中创建两个文件,以三位数字加后缀part的文件,例如001.part,002.part等。还有一个以同样的数字加上.part.met的文件,表示的是对应文件的元信息。part文件会创建得和原始文件大小一样,当下载完成后,文件名会修改成它本来的名称。而事实上,诸如这个文件原来叫什么名称,修改日期等等信息都在对应的.part.met元文件中。.part.met中还包含了该文件中那些部分已经下载完成的信息。
CPartFile类中Gap_Struct来表示文件的下载情况,一个Gap_Struct就是一个坑,它表示该文件从多少字节的偏移到多少字节偏移是一个坑。下载的过程就是一个不断填坑的过程。CPartFile类中有个成员变量gaplist就是该文件目前的坑的状况列表。需要主要的是有时填了坑的中间部分后,会把一个坑变成两个坑。坑的列表也会被存进.part.met中。
CPartFile类的代码很庞大,但是这是必须的。首先,它的创建就有几种可能,从搜索文件CSearchFile中创建,这种情况发生在用户搜索到他想要的文件后点击下载时发生。从一个包含了ed2k链接的字符串中创建,它会提取出该ed2k链接中的信息,并用来创建CPartFile。剩下的一种,就是当emule程序重启后,恢复以前的下载任务。这时就是去下载目录中寻找那些.part和.met文件了。另外它还需要不断得处理下载到的数据,为了减少磁盘开销,使用了Requested_Block_Struct结构来暂存写入的数据。它内部维护一个CUpDownClient的列表,如果知道了该文件的一个新的来源信息,就会创建一个对应的CUpDownClient。后者是emule中代码量最大的类。它还要把它的状态用彩色的条装物显示出来提供给GUI。
最后提一下它的Process方法。该方法是emule中为了尽量减少线程的使用而采取的一种有一些类似于轮询的机制。其它很多类中也有Process方法,这个方法要做的事情就是在一些和日常运行有关的事情,例如检查为了下载该文件而链接到自己的各个客户端的状态,向它们发送下载请求等。
下载任务队列
CDownloadQueue是下载队列类。这个队列中的项目是CPartFile指针。因此和emule中出现的很多其它的列表类一样,它需要能够提供对这个列表中的元素进行增加,查询,删除的功能。例如查询的时候能够根据该文件的hashID或者索引来进行查询。CDownloadQueue同时还要完成一些统计工作。
和其它的列表类不一样的是,它的所有元素的信息并不是集中存放于一个文件,而是对应于每一个下载任务,单独得存放在一个元信息文件(.part.met)中,因此当该类进行初始化的时候,它需要寻找所有可能的下载路径,从那些路径中找到所有的.part.met文件,并且试图用这些文件来生成CPartFile类,并且将这些通过.part.met文件正确生成的CPartFile类添加到自己的列表中,同样,在退出时,所有的下载任务的元信息也是自行保存,不会合成为一个文件。
CDownloadQueue中的Process方法的主要任务就是把它的列表中的CPartFile类中的Process方法都调一遍,另外主要的一些关于下载情况的统计信息也是在每一轮的Process后进行更新的。从这里我们也可以看出Process方法在emule中的意义,就是一个需要经常执行的方法,通过经常执行它们来完成日常工作,而且所有的这些Process方法肯定是顺序执行,因此可以减少很多多线程的同步之类的问题。emule中已经尽量减少了多线程的使用,但是在很多地方如果多线程是不可避免的话,也不会排斥。
上传任务队列
CUploadQueue是上传队列类。这个列表类中只有以CUpDownClient为元素的列表,它和其它列表类还有一个很大的不同就是它所保存的信息都不需要持久化,即不需要在当前的emule退出后还记住自己正在给谁上传文件,然后下次上线的时候再继续给他们传,这在大部分情况下是没有意义的。
上传队列类列表中有两个列表,上传列表和排队列表。当一个收到一个新的下载请求后,它会把对应的客户端先添加到排队列表中,以后再根据情况,把它们不断添加到上传列表中。在这里,信誉机制将会对此产生影响。
CUploadQueue的Process方法就相对简单了,那就是向上传队列中的所有客户端依次发送数据,而排队的客户端是不会得到这个机会的。另外它还需要完成关于上传方面的一些统计信息。
另外我们还需要注意在CUploadQueue的构造函数里面,创建了一个以100毫秒为间隔的定时器,这个定时器成为以上所有的Process所需要的基础。我们看它的UploadTimer就可以看出这一点。这里面充斥了各个类的Process方法的执行,其中包括以前我们提到的一些类,但是没有提到它们的Process方法,因为其过于简单,基本上就只是更新了一下要保存的信息。
emule中代码量最大的类CUpDownClient
CUpDownClient类的作用是从逻辑上表示一个其它的客户端的各种信息,它是emule中代码量最大的类。我们注意到,定义它的头文件是UpDownClient.h,但是却没有对应的CUpDownClient.cpp,而它的实现,都分散到BaseClient.cpp,DownloadClient.cpp,PeerCacheClient.cpp,UploadClient.cpp和URLClient.cpp中。
BaseClient.cpp中实现的是该类的一些基本的功能,包括基本的各种状态信息的获取和设置,以及按照要求处理和发送各种请求。在这里,逻辑实现和网络进行了区分,CUpDownClient类本身不从网络接受或者发送消息,它只是提供各种请求的处理接口,以及在发送请求时,构造好相应的Packet,并交给自己对应的网络套接字发出去。
DownloadClient.cpp中实现的是和下载相关的功能,它包括了各种下载请求的发送以及相应的数据的接收。另外还有一个A4AF的机制,它是emule中的一个机制,因为一个客户端在同一个时间内只能向另外一个客户端请求同一个文件。这样,对于很多个下载任务(CPartFile),有可能出现它们的源(即有该文件的客户端)有部分重叠的现象,而这时,如果其它下载任务正在从这个源下载,那么当前的下载任务就不能从这个源下载了。但是emule允许用户对其手动进行控制,如对下载任务的优先级进行区分,这样他就可以将一个源从另外一个下载任务那里切换过来。A4AF其实就是ask for another file的简称。
UploadClient.cpp中实现的是上传相关功能,即接受进来的下载请求,并且生成相应的文件块发送出去。
PeerCacheClient.cpp实现的是和PeerCache相关的功能,PeerCache是一个由Joltid公司开发的技术,它可以允许你从ISP提供的一些快照服务器上快速得上传或者下载一些文件(或者是一部分),这个技术的好处是可以减少骨干网络的带宽消耗,将部分本来需要在骨干网上走的流量转移到ISP的内部。当然这个功能需要ISP的配合。如果发现ISP提供了这项服务的话,emule会利用它来减少骨干网的带宽消耗。
URLClient.cpp实现的功能是利用http协议对原有的emule协议进行包装,以便使它能够尽可能地穿越更多的网络的防火墙。
emule常规部分小结
emule中还有其它的很多类,它们使得emule的功能更加的强大和完善。有很多类在前面没有提到,但是不代表它没有作用。而且即时是前面提到的类也只是大体的介绍,它们之间互相配合的一些细节没有体现。但是这些细节应该已经可以通过对它们的大体的功能的了解而更加容易被把握。至于GUI的设计,它也最终是要对应到某个功能实现类的数据的。
对于emule中的通信协议只是大体得描述了一下它的数据包的格式,但是并没有详细得描述它的每一个Opcode对应的包的意义,因为我认为这是没有必要的,在知道通信协议的格式以及处理它们的代码所在的位置后,可以很简单的通过追踪某条消息的前因后果把整个通信协议都分析出来。
这里再稍微提一下在emule中使用到的其它类及其功能。我们可以看到,如果单纯只是为了能够搜到以及下载到文件的话,有不少类是可以精简的,但是,正是由于它们的存在,使得emule的功能更加的完善。CIPFilter,IP地址过滤器,通过识别各种类型的IP地址过滤信息,它能够把不希望连接的网络地址过滤掉,emule中所有需要连接网络的地方使用的都是统一的过滤数据。CWebServer能够在本地打开一个Web服务器,然后你可以通过浏览器来控制你的emule。CScheduler能够实现下载任务的定时下载。CPeerCacheFinder为前面提到的PeerCache技术的主控制类。另外,emule还内置了一个IRC客户端,一个主要成员函数都为静态的CPartFileConvert类,能够对其它版本的驴的下载文件进行转换。它甚至还提供了一个自动处理zip和rar的类CArchiveRecovery。
到这里已经将eMule中ed2k资源的基本下载过程介绍完毕,但是eMule中还有一些机制(分段及信誉)。后面会针对这些机制进行分析梳理。