2010年7月7日星期三

轉載:emule源代码解析 2-5

 
1,分块机制——正确传输资源的保证
继续解析CKnowFile类
为了加快内容分发的速度,分块处理是一种简单有效的方法。emule中对每个文件都进行了分块处理。另外分块还有一个好处就是如果保留了每一分块的hash值,就能在只下载到文件的一部分时判断出下载内容的有效性。
emule在获取每个共享文件的信息时,就对它进行了分块处理,因此如果要知道emule中的分块处理和恢复机制,看CKnownFile::CreateFromFile函数的实现就行了这个函数中牵涉到的和分块处理以及hash计算相关的类都在SHAHashSet.cpp和SHAHashSet.h中。
(注意CKnowFile里的两个成员变量:CAICHHashSet *m_pAICHHashSet 和 CArray hashlist)
下面介绍其中几个主要的类:
CAICHHash类只负责一块hash值,提供两个CAICHHash类之间的直接赋值,比较等基本操作。CAICHHashAlgo是一个hash算法的通用的接口,其它hash算法只要实现这种接口都能使用,这样,可以很方便得使用不同的hash算法来计算hash值。CAICHHashTree则是一个树状的hash值组织方式,它有一个左子树和右子树成员变量,类型是指向CAICHHashTree的指针,这是一个典型的实现树状结构的方法。CAICHHashSet中包含了一个CAICHHashTree类型的变量,它直接向CKnownFile负责,代表的是一个文件的分块信息SHAHashSet.h文件的开始的注释部分向我们解释了它的分块的方式。这里要用到两个常量9728000和184320,它们分别是9500k和180k。这是emule中两种不同粒度的分块方式,即首先把一个很大的文件分割成若干个9500k的块,把这些块组织成一颗树状的结构,然后每一个这样的块又分解成若干个180k的块(52块,再加一个140k的块),仍然按照树状的结构组织起来。最后总的结构还是一颗树。 CKnownFile::CreateFromFile方法是在读取目标文件的内容时,逐步建立起这样一颗树的CAICHHashTree::FindHash能够根据读取到的目标文件的偏移量和下一块的大小,来找出对应的树枝节点(就是一个指向CAICHHashTree的指针)。如果有必要的话,还会自动创建这些树枝节点。因此在进行分块操作的时候,把文件从头到尾读一边,整个CAICHHashTree就建立起来了,对应的分块hash值也赋值好了。
最后我们还需要注意的就是CKnownFile类中的hashlist变量。就是说它还单独保留直接以9728000字节为单位的所有分块的MD4算法的hash值。这样对于一个文件就有了两套分块验证的机制,能够适应不同场合的网络基础设施——网络基础设施的基础设施 MFC中已经有一些网络基础设施类,如CAsyncSocket等。但是emule在设计中,为了能够更加高效得开发网络相关的代码,构建了另外的一些类作为基础设施,这些基础设施类的代码也有很高的复用价值(注意这里的可以复用的一些类)。
(关于网络编程的一些可以复用的类)
首 先是CAsyncSocketEx类。AsyncSocketEx.h中对这个类的特点已经给出了一定的说明。它完全兼容CAsyncSocket类,即 把应用程序中所有的CAsyncSocket换成CAsyncSocketEx,程序仍然能够和原来的功能相同,因此在使用上更加方便。但是在这个基础 上,它的效率更高,主要是在消息分发机制上,即它处理和SOCKET相关的消息的效率要比原始的MFC的CAsyncSocket类更高
另外,CAsyncSocketEx类支持通过实现CAsyncSocketExLayer类的方式,将一个SOCKET分成若干个层,从而可以很方便得实现许多网络功能,如设置代理,或者是使用SSL进行加密等。
另 外还有ThrottledSocket.h中定义的ThrottledControlSocket类和ThrottledFileSocket类,这两个 类只定义了两个接口。任何其它的网络套接字类如果想实现限速的功能,只需要在其默认的发送函数(如Send或Sendto)中不发送数据而是把数据缓存起 来,然后在实现ThrottledControlSocket或者ThrottledFileSocket接口中的 SendFileAndControlData或SendControlData方法时才真正把数据发送出去,这样就能实现上传限速,而这也是需要 UploadBandwidthThrottler类进行配合,UploadBandwidthThrottler是一个WinThread的子类,平时 单独运行一个线程。下一次会详细描述它是如何控制全局的上传速度的。
网络基础设施——全局限速器UploadBandwidthThrottler
UploadBandwidthThrottler 是emule中使用的全局的上传限速器。它继承了CWinThread类,且在该类被创建的时候,就新创建一个线程开始单独运行。在该类被析构时也会自动 停止相应的线程。这个线程的目标函数就是RunProc,然后为了避免在RunProc函数不能使用this指针的情况,它使用了RunInternal 来实际完成工作线程的工作。在emule中,还有另外一个类LastCommonRouteFinder有类似的结构。 UploadBandwidthThrottler中保存了若干的套接字(Socket)队列,这些队列的处理方式略有不同。在标准队列 (m_StandardOrder_list)里面排队的都是实现了ThrottledFileSocket接口的类,通常这些类能够传输文件内容也可以 传输控制信息。
而其它四个队列都是实现ThrottledControlSocket接口的类的队列,在这些队列中的类主要以传输控制信息为主。
这四个队列为临时高优先级,临时普通优先级,正式高优先级,正式普通优先级。和把套件字直接添加到普通队列(AddToStandardList)不同,
QueueForSendingControlPacket把要添加到队列的套接字全部添加到两个临时队列。根据它们的优先级添加到普通的临时队列。在RunInternal的大循环中,临时队列中的项目先被移到普通队列中,然后再进行处理。
UploadBandwidthThrottler使用了两个临界区,两个事件。 pauseEvent是用来暂停整个大循环的动作的。而threadEndedEvent是标志整个线程停止的事件。sendLocker是大循环中使用 的主要的临界区,而tempQueueLocker是为两个临时队列额外添加的锁,这样可以一边发送已有队列中的套界字要发送的数据,一边把新的套接字加 到队列中。
UploadBandwidthThrottler 的RunInternal中的大循环是该工作线程的日常操作。这个大循环中做了以下事情,计算本次配额,即本次循环中能够发送多少字节,好安排调度,计算 本次循环应该睡眠多少时间,然后进行相应的睡眠,从而进行限速。操作控制信息队列,发送该队列中的数据,注意,控制队列中的套接字 (m_ControlQueueFirst_list和m_ControlQueue_list)只使用一次就离开队列。而标准队列中的套接字不会这样。 在一轮循环结束后,如果还有没有用完的发送数据的配额,则会有部分配额保存到下一轮。
网络基础设施——emule套接字CEMSocket
CEMSocket 是CAsyncSocketEx和ThrottledFileSocket的子类,它把若干功能整合到了一起,因此可以作为emule使用起来比较方便的 套接字。例如它可以很方便得指定代理,把CAsyncSocketEx中的创建一个新的代理层并且添加到列表中的功能对外屏蔽了。另外它可以分出状态,如 当前是否在发送控制信息等。
CEMSocket 中我们需要仔细考察的是它的SendControlData和SendFileAndControlData方法。如前所述,这些方法是用来和 UploadBandwidthThrottler进行配合,以便完成全局的限速功能的。它的功能应该是按照 UploadBandwidthThrottler的要求,在本次轮到它发送数据时发送指定数量的字节数。因此,应用程序的其它部分在使用 CEMSocket时,如果要达到上传数据限速的目的,不应该直接调用标准的Send或者SendTo方法,而是调用SendPacket。这里就有了另 外一个结构Packet,它通常包含一个emule协议中完整的包,例如有协议的头部数据等,还内置了PackPacket和UnPackPacket方 法,可以自行进行压缩和解压的功能。SendPacket把要发送的Packet放到自己的队列中,这个队列也有两个,控制信息包队列,和标准信息包队 列。如果有必要,把自己加入到UploadBandwidthThrottler的队列中。 我们注意到CEMSocket的SendControlData和SendFileAndControlData方法其实都是调用自己的另一个重载的 Send方法。而且我们也已经知道这个方法是在UploadBandwidthThrottler的工作线程中的大循环中被调用的,而这个Send方法的 内容本身也是一个大循环,但是意义很明了,就是在不超过自己本次发送的配额的情况下,把自己的包队列中的包取出来,并且发出去。同样,这里也用到了一个临 界区,它是为了保证从包队列中取出包来发送和把包往队列中放的操作是互斥的。因此,如果把它和UploadBandwidthThrottler结合起 来,我们就看到了一个两层的队列,即所有的套接字组成了一个发送队列,在UploadBandwidthThrottler的控制下保证了对速度的限制, 而每个套接字即将发送的数据包又组成了一个队列,保证了每次进行数据发送的时候都会满足UploadBandwidthThrottler的要求


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只负责和搜索有关的信息的储存和读取,本身并不进行搜索。  

服务器信息集-CServerList
尽管目前有了Kad网络,但是使用服务器来获取各个emule用户的共享文件列表仍然是emule中主要的资源获取方式。CServerList就是emule中负责管理服务器列表的类。和前面若干列表类结构类似,CServerList需要对外提供列表的增加,删除,查找,修改等接口。在CServerList中,每个服务器的信息是一个CServer类。和搜索信息不一样,但是和已知文件列表一样,服务器的信息列表是需要长期保留的,因此CServerList和CKnownFileList类一样提供了把它所包含的所有信息保存到一个文件中,以及从这个文件中读回其信息的功能

CServer中的结构比较简单,只需要保留服务器的各种信息即可。它可以通过IP地址和端口来创建,也可以通过一个简单的结构 ServerMet_Struct来创建,其中后者是用来直接从文件中读取的。该结构仅仅包含IP地址和端口以及属性的个数,CServer中其它的属性 在保存到文件中时,均采用Tag方式保存。

CServerList除了提供通常的CServer信息外,还提供一些统计信息诸如所有的服务器的用户数,共享的文件数等。这些统计信息也是基于每个单独的CServer的相关信息计算出来的。

emule的通信协议-一些基本的约定
接下来将不可避免得要碰到emule的协议。emule的通信协议格式设计成一种便于扩充的格式。对于TCP连接来说,连接中的数据流都能够划分成为一个一个的Packet,CEMSocket类中就完成了把接收到的数据划分成Packet这一工作。但是具体的对于每个Packet进行处理的工作被转移到它的子类中进行。CEMSocket类的两个子类CServerSocket和CClientReqSocket所代表的TCP连接就分别是客户端和服务器之间的TCP连接以及客户端之间的TCP连接。在数据流中的第一个字节代表的是通信的协议簇代码,如0xE3为标准的edonkey协议,0xE4为kademlia协议等等。接下来的四个字节代表包内容的长度,所有的包都用这种方式发送到TCP流中,就可以区分出来了。另外每个包内容中的第一个字节为opcode,即在确定了某个具体协议后,这个opcode确定了这个包的具体含义。

对于走UDP协议的包,处理起来更加得简单,因为UDP本来就是以一个包一个包作为单位在网络上流传的,因此不需要在包的内容中再包含表示长度的字段。每 个UDP包的第一个字节是协议簇代码,其它内容就是包的内容。CClientUDPSocket类负责处理客户端和客户端之间的UDP包,而 CUDPSocket类负责处理客户端和服务器之间的UDP包。另外还有个Kademlia::CKademliaUDPListener类,专门处理和 Kademlia协议相关的UDP包。

最后说一下Packet类,这个类以前只是提到过。它是emule的通信协议的最小单位。我 们可以看出,它的构造函数有多个版本,这也是为了可以用不同的方式来创建Packet。例如只包含一个头部信息的缓冲区,或者只是指定协议簇代码等。而且 它内部实现了压缩和解压的方法,该方法直接调用zlib库中的压缩方法,可以减少数据的传输量。这里要注意一点的就是压缩的时候协议簇代码是不参与压缩 的,压缩完毕后会更换协议簇代码,例如代码为标准edonkey协议0xE3的包在压缩后,协议代码就变成0xD4了,这里进行协议代码变化是为了使接受 方能够正确识别并且进行相应的解压操作。

emule的通信协议-客户端和服务器之间的通信概述
客户端和服务器之间的所有通信由类CServerConnect掌握。CServerConnect本身不是套接字 的子类,但是它的成员变量CServerSocket类型的connectedsocket是。CServerConnect内部有一列表,可以保存若干 CServerSocket类型的指针。但是这并不说明它平时连接到很多服务器上。它只是可以同时试图连接到若干个服务器上,这只是因为连接到服务器上的 行为不一定能成功。

CServerSocket类是CEMSocket的子类,它比CEMSocket要多保存一些状态,比如当前的服务器连接状态。它同时还保留它当前所连 接的服务器的信息。通过分析CServerSocket::ProcessPacket就可以直接把emule客户端和服务器之间的通信协议理解清楚,这 里是服务器发回的包。TCP连接建立后的第一个包是在CServerConnect::ConnectionEstablished中发出的,即向服务器 发出登陆信息。如果登陆成功,则能够从服务器处获取自己的ID,这是一个32位的长整数。如果这个数小于16777216,那么我们称它为LowID。具 有LowID的客户端通常情况下其它客户端将不能直接连接它。得到LowID的原因比较多,例如当自己处于NAT的后端的时候。获取自己的ID后将会向服 务器发送自己的共享文件列表,这一动作由共享文件列表类CSharedFileList来完成。

其它类型包没有必要全部都列出来,以后可以通过在分析其它部分时,因为牵涉到往服务器发送或者接受数据的时候再进行相应的分析。

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类负责的是从逻辑上对网络另一边的一个 客户端进行表达。CUpDownClient类代码很长,以后再说。


emule中的信誉机制
信誉机制在P2P系统中有非常重要的作用。为了使用户更加愿意共享自己的资源,需要有一些机制能够让对整个P2P系统贡献更大的用户有更多的激励。 在emule中,激励机制的设计方案是tit-for-tat这种最直观的方案。这种方案的意义就是最简单的如果别人对你好,那么你也对别人好。

下面看实际的实现。CClientCreditsList和CClientCredits类负责emule中的信誉机制我们再次见到这种列表和元素之间的关系, 不必再重复那些语言。和信誉相关的信息是需要永久保存的,这样才有意义,因此CClientCreditsList提供了LoadList和 SaveList方法。我们另外注意到,CClientCredits类可以使用CreditStruct结构来创建,而CreditStruct结构只 包含静态信息。主要是上传量和下载量等。

信誉机制的信息需要有一定的可靠性,在emule中采用了数字签名的方式来做到这一点。Crypto++库为emule全程提供和数字签名验证相关的功 能。CClientCreditsList在创建时,会装载自己的公钥私钥,如果没有的话,会创建一对。CClientCreditsList中包含的有 效的信息都是经过其它人数字签名的,所以更加有信服力。在实际使用中,这些信息和自己的私钥要注意保存。重装emule后应该把配置文件目录先备份,这样能够保留自己辛辛苦苦积攒的信誉
下载任务即部分文件的表示
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

前面提到的AICH机制对于最大程度得保证下载文件的正确性以及尽量减少重复传输都有很大的帮助,在下载的过程中,该机制会经常对下载到的数据进行校验。

最后提一下它的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。

Kademlia网络是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 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的任务。

沒有留言:

發佈留言