eMule源代码解析(五)

emule中的Kademlia代码总体描述

当emule中开始使用Kademlia网络后,便不再会有中心服务器失效这样的问题了,因为在这个网络中,没有中心服务器,或者说,所有的用户都是服务器,所有的用户也是客户端,从而完完全全得实现了P2P。接下来讲针对emule中的Kademlia网络进行分析,会有一节进行原理方面的分析。另外的几节将会根据emule中实现Kademlia所使用的不同的类分别进行讲述。其中:

CKademlia是整个Kademlia网络的主控类,可以直接开始或者停止Kademlia网,并且含有Process方法来处理日常事务。

CPrefs负责处理自身的Kademlia相关信息,如自身的ID等。

CRoutingZone,CRoutingBin和CContact三个类组成了每个节点所了解的联系信息以及由这些联系信息所组成的数据结构。

CKademliaUDPListener负责处理网络信息。

CIndexed负责处理本地存储的索引信息。

CSearch,CSearchManager负责处理和搜索有关的操作,其中前者表示的是一个单一的搜索任务,后者负责对所有搜索任务进行处理。

CUInt128负责处理一个128位的长整数,并且内置其各种运算。前面已经提到过。

emule中的Kademlia的基本原理

Kademlia是一种结构化的覆盖网络(Structured Overlay Network),所谓的覆盖网络,就是一种在物理的Internet上面再次构建的虚拟网络,所有参与的节点都知道一部分其它节点的IP地址,这些节点称为它的邻居,如果需要查找什么东西,它先在本地寻找,如果找不到,就把这个查询转发到它的邻居处,希望能够有可能查找到相应的结果。覆盖网络里面分成了结构化和非结构化的两种情况,它们的区别在于每个节点知道哪些其它节点的信息是否有特定的规律。在非结构化的覆盖网中,每个节点的邻居状况没有特定的规律。因此在非结构化网络中,如果要进行查询,会采取一种叫做泛洪(flooding)的方法,每个节点如果在本地没有查找到想要的结果,会把查找请求转发到它的邻居中,然后再通过邻居的邻居这种方式来进行一步步的查找。但是这种方法如果处理不好,会造成整个网络的消息负载过大。已经有不少文章对于优化非结构化覆盖网络中的查询进行了很深入的探讨。

对于结构化的覆盖网络,它的特点是每个节点它会选择和哪些节点做邻居是有一定的规律的,从而在进行搜索的时候,节点把搜索请求进行转发的时候它能够通过一定的规律进行选择把请求转发到哪些邻居节点上。这样同时也能减少搜索代价。结构化的覆盖网络通常要求每一个节点随机生成一个ID,用以判断各个节点之间的关系。这个ID和它所在的物理网络必须是没有关系的。

对于Kademlia网络来说,这个ID是一个128位的数值,所有的节点都用这个ID来衡量自己与其它节点的逻辑距离。而逻辑距离的计算方法就是将两个节点进行异或(XOR)操作。在Kademlia网络的形成过程中,每个节点选择邻居的原则是离自己逻辑距离越近的节点越有可能被加入到自己的邻居节点列表中,具体来说就是在每次新得到一个节点的信息的时候,是否把它加入到自己的邻居节点列表是根据距离的远近来处理的。后面分析具体程序的代码时会有说明。

结构化的网络的好处就是如果我们要寻找一个距离某个ID逻辑距离足够近的节点,我们可以保证在O(logn)级别的跳数找到。只要先寻找自己已知的离目标ID逻辑距离足够断的节点,然后再问它知不知道更近的,然后就这样下去。因此在搜索的时候也是这样,当需要发布资源的时候,把文件进行hash,这样就能够计算出一个128位的ID,或者把关键字进行hash。然后寻找到离这个结果逻辑距离最近的节点,把文件或者关键字的信息发送给它,让它存起来。当有人要搜索同样的东西的时候,由于它用的是同一个hash算法,因此能够计算出对应的ID,并且去搜索那些和这个ID逻辑距离相近的节点,因为它知道,如果网络中真有这些资源的话,这些节点是最有可能知道这些信息的。由此我们可以看出,结构化的网络的资源查找效率是很高的,但是它和非结构化的覆盖网络比起来,缺点是不能进行复杂查询,即只能通过简单的关键字或者文件的hash值进行查找。非结构化的网络的查找本身就是随意转发的,每个收到的查询请求的节点都对本地的资源掌握的很清楚,因此自然可以支持复杂查询,但是显然非结构化的网络支持的复杂查询不太可能动员所有的节点都来做这一动作。目前还没有方法能够把两种覆盖网络的优点结合起来,我也非常想知道这样的一种方法。

emule中的Kademlia的基础设施类

Kademlia的主控类是CKademlia,它负责启动和关闭整个Kademlia网的相关代码。在它的Process函数中,会处理和Kademlia网相关的事务,例如隔一段时间检查某个区间的节点数是否过少,如果是则寻找一些新的节点。另外经常对自己的邻居进行检查等,这些都是属于需要进行日常安排的工作。所有搜索任务的日常处理也需要它来调度。它还作为Kademlia网的代表,向emule其它部分的代码返回Kademlia网的一些统计信息。

另一个基础设施类是CPrefs,它和emule普通代码中的CPreferences作用类似,但是CPrefs只保留和Kademlia网相关的,需要长期保存的本地信息。具体到这个版本来说,主要就是本地的ID。

还有一个很重要的基础设施就是CUInt128,实现对128位的ID的各种处理,前面的部分已经提到。

emule中的Kademlia的联系人列表管理

CRoutingZone,CRoutingBin和CContact三个类组成了联系人列表数据结构。它要达到我们搜索的要求,即搜索到目标的时间要能够接受,而且所占用的空间也要能够接受。

首先CContact类包含的是一个联系人的信息,主要包括对方的IP地址,ID,TCP端口,UDP端口,kad版本号和其健康程度(m_byType)。其中健康程度有0-4五个等级。刚刚加入的联系人,也就是健康状况未知的,这个数值设置为3。系统会经常通过与各个联系人进行联系的方式对其进行健康状况检查,经常能够联系上的联系人,这个数值会慢慢减少到0。而很就没有联系的,这个数值会慢慢增加,如果增加到4后再过一段时间未能成功联系上的,则将会被从联系人列表中删除。

CRoutingBin类包含一个CContact的列表(typedef std::list<CContact*> ContactList;)。这里要注意的是要访问联系人的信息必须通过某个CRoutingBin,CRoutingZone内部是不直接包含联系人信息的。可以把新的联系人信息往一个特定的CRoutingBin中加,当然也可以进行联系人查找。它也提供方法能够寻找出离某个ID距离最近的联系人,并给出这样的一个列表。这是相当重要的。最后,一个CRoutingBin类中能够包含的CContact的数量也是有限制的。(在Kademila名字空间中,定义了#define K 10)

CRoutingZone类处于联系人数据结构的最上层,直接为Kademlia网提供操作接口。该类的结构为一个二叉树,内含两个CRoutingZone指向它的左子树和右子树,另外也包含一个CRoutingBin类型的指针。但是只有在当前的CRoutingZone类为整个二叉树的叶节点时,这个指向CRoutingBin类型的指针才有意义。(CRoutingZone *m_pSubZones[2]; CRoutingZone *m_pSuperZone;)这个二叉树的特点是,每个节点以下的所有联系人的ID都包含一个共同前缀,节点的层数越深,这个共同前缀越长。例如,根节点的左子树的所有的节点的ID一定有一个前缀"0",而右子树的所有节点一定有前缀"1"。同样,根节点的左子树的右子树下的所有节点的ID一定有前缀"01",等等,依此类推。我们设想一下节点不断得往这个二叉树添加的过程。刚开始只有一个根节点,它也就是叶节点,这时它内部的CRoutingBin是有意义的,当联系人信息不断得被添加进去以后,这个CRoutingBin的容量满了,这时要进行的就是一个分裂的操作。这时,会添加两个左子节点和右子节点,然后把自身的CRoutingBin中的联系人信息按照它们的前缀特点分别复制往左节点和右节点,最后把自身的CRoutingBin废除掉,这样这个分裂过程就完了。当分裂完成后,就会再次试图添加该联系人信息,此时会试图按照它的ID,把它添加到对应的子树中。但是并不是所有的这种情况节点都会发生分裂,因为如果允许任意分裂的话,本地所需存储的节点信息数量就会急剧上升。这里,自身ID的作用就体现了。只有当自身ID和当前准备分裂的节点有共同前缀时,这个节点才会分裂,而如果判断到一个节点不能分裂,而它的CRoutingBin又满掉了,那么就会拒绝添加联系人信息。

我们可以看出,在以上政策的进行下,离自身ID逻辑距离越近(也就是共同前缀越长)的联系人信息越有可能被加入,因为它所对应的节点越有可能因为分裂而获得更多的子节点,也就对应了更多的容量。这样,在Kademlia网中,每一个参与者知道的其它参与者信息中,离自己逻辑距离越近的参与者比例越高。由于在搜索的时候也只需要不断得寻找更近的ID,而且每一步都一定会有进展,所以寻找到目标ID所需要的时间上的代价是O(logn),从这个二叉树的结构来看,我们也可以看到,由于只有部分节点会分裂,所以实质上存储所需要的空间代价也是O(logn)。

实际上CRoutingZone在实现时和理论上的Kademlia有一些区别,如从根节点开始,有一个最低分裂层数,也就是说,如果层数过低的话,是永远允许分裂的,这样它知道的其它地区的联系人信息就能够稍微多一些。

emule中的Kademlia网络消息处理

CKademliaUDPListener负责处理所有和Kademlia网相关的消息。前面已经对emule的通信协议的基本情况做了一个大概的描述,我们就可以知道,CKademliaUDPListener处理的消息一定是只和Kademlia网相关的,分拣工作已经在emule的普通UDP客户端处理代码那里处理好了。具体的消息格式前面也有一些介绍,下面会就一些具体的消息分类做说明。

首先是健康检查方面的消息,这样的消息就是一般的ping-pong机制。对应的消息有KADEMLIA_HELLO_REQ和KADEMLIA_HELLO_RES。当对本地联系人信息列表进行检查时,会对它们发出KADEMLIA_HELLO_REQ消息,然后处理收到的KADEMLIA_HELLO_RES消息。

最常用的消息是节点搜索消息,在Kademlia网络中,进行节点搜索是日常应用所需要传输的主要消息,它的实现方式是迭代式的搜索。这种方式就是说当开始搜索某个ID时,在本地联系人信息列表中查找到距离最近的联系人,然后向它们发出搜索请求,这样通常都能够得到一些距离更近的联系人信息,然后再向它们发送搜索请求,通过不断得进行这样的搜索查询,就能够得到距离目标ID最近的那些联系人信息。这里对应的消息代码是KADEMLIA_REQ和KADEMLIA_RES。(这两个消息代码,跟来更新路由表的)

接下来就是对内容进行发布或者搜索。这一点结合后面的CIndexed类的分析可以知道得更加清楚。emule中存储在Kademlia网中的信息主要有三类:文件源,关键字信息和文件的评论。文件源对应的是每一个具体的文件,每个文件都用它的内容的hash值作为该文件的唯一标示,一条文件源信息就是一条关于某人拥有某个特定的文件的这样一个事实。一条关键字信息则是该关键字对应了某个文件这样一个事实。很显然,一个关键字可能会对应多个文件,而一个特定的文件的文件源也很有可能不止一个。但是它们的索引都以固定的hash算法作为依据,这样使得搜索和发布都变得很简单。

我们来看发布过程。每个emule客户端把自己的共享文件的底细已经摸清楚了,在传统的有中心索引服务器的场景里,它把自己的所有文件的信息都上传到中心索引服务器里。但是在Kademlia网里,它就需要分散传播了,它首先做的事情是把文件名进行切词,即从文件名中分解出一个一个的关键词出来,它切词的方法非常简单,就是在文件名中寻找那些有分割符含义的字符,如下划线等,然后把文件名切开。计算出这些关键字的hash值后,它把这些关键字信息发布到对应的联系人那里。并且把文件信息也发布到和文件内容hash值接近的联系人那里。对应的消息是KADEMLIA_PUBLISH_REQ和KADEMLIA_PUBLISH_RES(这两个消息代码,用来发布共享文件的)。另外emule允许用户对某个文件发表评论,评论的信息单独保存,但是原理也是一样的。

当用户使用Kademlia网络来进行搜索并且下载文件的时候,首先是对一个关键词进行搜索,由于使用的是同样的hash算法,这样它只要找到ID值和计算出来的hash值结果相近的联系人信息后,它就可以直接向它们发送搜索特定关键词的请求了。如果得到了返回信息,那么搜索者就知道了这个关键词对应了多少文件,然后把这些文件的信息都列出来。当用户决定下载某个文件的时候,针对这一特定文件的搜索过程就开始了,这一次如果搜索成功,那么返回的就是这个文件的文件源信息。这样emule接下来就只需要按照这些信息去连接相应的地址,并且使用传统的emule协议去和它们协商下载文件了。这里对应的消息是KADEMLIA_SEARCH_REQ和KADEMLIA_SEARCH_RES(这两个消息代码,用来搜索文件的)。

实际的实现中有Kademlia2这种协议,它的原理是一样的,只有协议代码和具体的消息格式不一样,例如KADEMLIA_REQ和KADEMLIA_RES对应了KADEMLIA2_HELLO_REQ和KADEMLIA2_HELLO_RES,但是后者在具体的消息中包含了比前者丰富一些的信息。在实现的时候0.47c更加倾向于使用Kademlia2,而0.47a更加倾向于使用Kademlia。当然,它们两种协议都能够处理。另外,0.47c增加了一个对于已发出的请求的追踪的特性,就是一个包含TrackPackets_Struct类型的列表,这里面详细纪录了什么时间曾经对哪个IP发出过那种opcode对应的请求。为什么要这样呢?这是为了防止针对DHT的一种路由污染攻击,因为在搜索联系人的时候,如果搜索到了一些联系人信息,也会试图把它先加入到本地的联系人信息列表中。这样如果有人想恶意攻击的话,它只要不断得往它想攻击的emule客户端发送KADEMLIA_RES,并且在消息的内容中包含大量的虚假联系人信息,就可以使对方的联系人信息列表中充满垃圾。这样,由于缺少正确有效的联系人信息,它的Kademlia网功能基本上就废了。而在0.47c里面增加的这个特性,就会对那种还没有发出请求就收到回应的情况直接无视,从而避免被愚弄。

emule中的Kademlia的分布式索引管理

Kademlia网络的最大的好处是把原来需要存储到中心索引服务器中的信息分散存储到各个客户端当中,如果要说得更加准确一点,那我们就可以说它把这些信息分散得存储到各个emule客户端的CIndexed类当中。我们可以具体开始看CIndexed的设计,看它是如何完成这一工作的。在这之前我们要稍微详细得说一下emule发布到Kademlia网络中的信息的各种类型。

一个文件源信息是一个文件内容的hash值和拥有这个文件的客户端的IP地址,各种端口号以及其它信息之间的对应关系。而一个关键词信息则是该关键词和它对应的文件之间的关系。在关键词信息中,它对应的文件信息要更加详细,通常包括这个文件的文件名,文件大小,文件内容的hash值,如果是MP3或者其它媒体文件,还会包含包括作者,生产时间,文件长度(这个长度是用时间来衡量的媒体文件的播放长度),流派等等tag信息。其中文件内容的hash值用来区分该关键词对应的不同文件。

CIndexed中利用了一系列的Map来存储这些对应信息,CMap是MFC中实现标准STL中的map的模板类,CIndexed中包含了四个这样的类,分别用来存储文件源信息,关键词信息,文件评论信息以及负载信息。其中文件评论信息是不长久保存的,而其它的信息都会在退出的时候写到文件中,下次重新启动emule时再重新调入。另外负载信息不是等其它联系人来发布的,而是根据文件源信息和关键词信息的发布情况自行进行动态调整的。每一次收到发布信息时,对应的ID的负载会增大,这一事实会在回应消息(KADEMLIA_PUBLISH_RES)中体现。

CIndexed中的信息会经常进行检查,每隔三十分钟它会把自己存储的所有信息中太老的信息清除掉。其中文件源信息的保存时间为五小时,关键词信息为二十四小时,文件评论的信息保存时间也为二十四小时。因此文件的发布和关键词也要周期性得反复进行。其实这对于整个Kademlia网络的稳定性也是有好处的,因为每一次联系都会试图把对方添加到自己的联系人列表中,或者在联系人列表中标注上一次见到对方的时间。

CIndexed为其它部分的代码提供了它们所需要的增加信息和搜索信息的接口,这样在从网络中获取到相关的搜索或者发布请求,并且CKademliaUDPListener完成消息的解释后,就可以交给CIndexed来进行处理了。

emule中的Kademlia搜索任务管理

CSearch和CSearchManager是完成具体搜索任务的。CSearch对应的是一个具体的搜索任务,它包括了一个搜索任务从发起到结束的全部过程,要注意的是搜索任务并不只是指搜索文件源或者关键词的任务,一次发布任务它也需要创建一个CSearch对象,并且让它开始执行。CSearchManager则掌握所有的搜索任务,它包含了一个包含所有CSearch指针对象的CMap,使用CMap的原因是因为所有的CSearch都一定对应一个ID,那个ID就是该CSearch所对应的目标,不管是要查找节点,还是要搜索或者发布信息,一定都要找到和目标ID相近的联系人。因此CSearchManager可以使用CMap来表示所有的搜索任务。

我们注意到CSearch在创建的时候就把自己加入到CSearchManager当中。另外CSearch在创建的时候需要说明它的类型,例如是只是为了搜索节点还是要搜索关键词信息或者文件源信息,当然也有可能是发布文件源信息或者关键词信息。我们介绍一下CSearch的几个方法的作用就可以大概了解CSearch的工作过程。Go是它的启动过程,它会开始第一次从本地的联系人列表中寻找候选的联系人,然后开始发动搜索。SendFindValue的功能就是向某个联系人发送一个搜索某ID的联系人信息这样一个请求。JumpStart则是在搜索进行到一定地步的时候,如得到了一些中间结果,开始进行下一步的行动,下一步的行动仍然可能是SendFindValue,也有可能认为搜索到的联系人离目标已经足够近了,于是就可以开始实质性的请求。StorePacket就是这样一个实质性的请求,例如在一个以发布文件源为任务的CSearch中,StorePacket会向目标联系人发送KADEMLIA2_PUBLISH_SOURCE_REQ(如果不支持Kademlia2,那么是KADEMLIA_PUBLISH_REQ)。最后,CSearch能够处理各种搜索结果,然后向调用它的代码返回处理好的结果。

CSearchManager直接和Kademlia网的其它部分代码接触,例如,如果CKademliaUDPListener搜索到了一些结果,它会把这些结果交给CSearchManager,然后CSearchManager再去寻找这个结果是属于那个搜索任务的,并且进行转交。另外CSearchManager对外提供创建各种新的搜索任务的接口,作用类似于设计模式中的Factory,其它部分的代码只需要说明需要开始一个什么样的搜索任务即可,CSearchManager来完成相应的创建CSearch的任务。

你可能感兴趣的:(数据结构,算法,应用服务器,网络应用,网络协议)