1, eMule源代码学习心得(1):eMule代码的总体风格和其它相关工程
eMule的官方首页上写着:2002年05月13日一个叫做Merkur的人,他不满意原始eDonkey2000客户端并且坚信他能够做的更好,所以他开始制作。他聚集了其它开发人员在他的周围,并且eMule工程就此诞生。
eMule是一个典型的MFC程序,它的图形界面等,已经和MFC紧紧融合到了一起。因此通常情况下它只能在windows平台下运行。有一些其它的工程,如aMule等,把它进行了移植,因此跨平台的功能要强些。
其实还有另外一个叫做xMule的工程,不过现在已经人气快不行了。在aMule的主页上可以看到eMule移植到linux平台下的一些历史,最早是有个叫做lMule的工程,他使用wxwidgets来进行eMule的跨平台的移植,这个工程2003年就不再更新了,后来转变成为xMule工程,它一度是linux平台下eMule的事实上的替代品。但是他们的程序员之间由于理念不同,发生了内讧,导致aMule 分裂出来,他们后来矛盾严重的时候曾经一度从理念问题上升到互相对对方进行人身攻击,并且曾经对对方的网站发动过DDos。后来aMule和xMule就是两个完全不同的工程,xMule现在只有HopeSeekr一个人在维护,基本上也没有什么更新了。这一点不仅让人感慨。今年寒假的时候我曾经和 HopeSeekr进行过一些交流,感觉他非常自信,经常拿着aMule的一部分代码来给我看,说你看看他们的代码这么这么写,这简直就是一陀xx嘛,这种代码在某些情况下肯定会Crash掉嘛,相反,你看看我们xMule的代码,这里是这样这样,肯定就不会有这种问题了。
eMule从0.42版开始支持Kad技术,这是一个非常重要的里程碑。Kad是一种DHT的协议,它可以使节点之间互相保留一些其它节点的联系信息,并且利用这样一个“关系网”寻找到整个网络中的任何一个节点以及上面的资源,整个过程不需要任何中心服务器。因此向当年搞napster那样直接端掉中心服务器就搞跨napster网络一样来对付eMule的Kad网就毫无作用了。0.42版是2004年2月27日放出 的,比eDonkey2000的OverNet晚了将近一年,但是它的Kad网络的规模却在迅速扩大。Overnet和eMule中的Kad使用的都是Kademlia结构,但是具体的消息报文的格式有区别,因此两个DHT网络并不能互相兼容。OverNet直到现在,用户也仍然维持在十万左右,这是比较近的一篇文章写的。但是eMule的Kad网的规模有多大,目前却没有一个很准确的说法。由此也可以看出开源软件的力量。目前aMule的Kad网和eMule的Kad网是兼容的,xMule中还没有Kad的支持。Kad协议的原始paper可以在我们实验室的机器上下到:
http://bigpc.net.pku.edu.cn:8080/paper/new/by%20conference/IPTPS/IPTPS02/Kademlia%20A%20Peer-to-Peer%20Information%20System%20Based%20on%20the%20XOR%20Metric%28IPTPS02%29.pdf
2. eMule源代码学习心得(2):从emule.cpp开始,顺便谈如何编译emule
先说一声抱歉,因为前两天刚回到家中,休息了一下,所以这两天没有更新。今天继续昨天的话题。
eMule的代码结构非常合理。虽然代码量比较大,但是各个功能模块之间的划分都很合理。从它的工程文件里面就可以看出这一点。eMule把表示功能的代码文件和表示界面的代码文件分开了,Source Files和Header Files是实现功能的代码的源文件和头文件,而Interface Source和Interface Header是实现图形界面的源文件和头文件。由于eMule的代码量太大,本系列将跳过图形界面的实现,着重分析eMule的功能实现部分的代码,而且功能实现方面也主要挑主要的部分分析。本节将从emule.cpp开始分析,引出eMule中使用到的几个主要的功能实现的类,并大体描述它们的作用。最后介绍一下如何在VS2003下编译eMule。emule中还有不少模块实现的功能挺有用,我说的是在其它的程序里很有用,可以考虑在其它程序中进行复用。
emule.cpp为类CemuleApp的实现。因此在运行时,首先会运行InitInstance进行一些初始化的工作。从这个函数里面我们也可以第一次看出那些即将在整个程序中发挥作用的类了。
最开始的时候是计算出程序常用的一些目录,如配置文件,日志文件等。接下来是ProcessCommandline,它的作用有两方面,第一是确认该eMule的运行方式,即命令行后面有没有参数,第二是确认目前eMule是不是只有一个实例在运行。在一般的情况下,双击eMule可执行文件是不会带参数的。但是通过点击链接或者打开关联文件的方式打开eMule则相当于带参数运行eMule。通过在注册表里添加一些项目可以让一个程序和某种链接或者某个后缀的文件产生关联。具体办法可以参见OtherFunctions.cpp中的Ask4RegFix,BackupReg,RevertReg三个函数的功能。ProcessCommandline中通过创建带有名称的互斥信号量来确认是否有其它的eMule实例在运行。对于一个确定的名称,CreateMutex只能创建一个互斥信号量。因此通过该信号量是否创建成功就可以知道是否有其它eMule实例运行。如果有的话,而且又是带参数的那种模式,那么直接把这个参数使用Windows的消息机制发给那个窗口即可,接下来的代码无非就是如何找到另外一个叫"eMule"的家伙以及给它发个什么消息。pstrPendingLink是一个全局变量,表示将要被处理的命令行参数。它将会在初始化完成后一段时间后被处理。
下面两个比较重要的类是CPreferences和CStatistics。前者掌握着程序的大部分配置数据,后者则进行各种统计。它们的特点都是有很多的成员变量,而且还是静态的,这种方式可以保证它们的唯一性,而且把这些变量统一到一个类管理。但是实际上并不需要了解每个变量的含义。thePrefs和theStats是这两个类的唯一的实例。
在处理完其它一些事情,包括创建图形界面对象CemuleDlg后,接下来可以看到一排一排的创建新对象的语句。这些类将会实现eMule程序运行的主要功能,后面的系列将详细分析。
最后描述一下如何在VS2003里编译eMule,由于eMule中用到了一些其它的库,因此官方在提供eMule的源代码下载的同时如果也提供这些库的下载会使源码包变得很大。因此eMule选择了让开发者去那些库的官方网站下载它们的方式。一般来说,编译这种工程文件,很重要的地方是要保持各个库和主程序编译参数的一致性。这些编译参数中,最主要的参数有三个,字符集(多字节/Unicode),调试/发行,单线程/多线程,这样排列组合一下就有八个版本了。因此编译的时候如果不注意,就会出现和程序设计无关的错误。
eMule0.47a解压后自带id3lib库,还需要下载以下的库:
zlib:http://www.gzip.org/zlib/
ResizableLib:http://sourceforge.net/projects/resizablelib/
Crypto++:http://www.eskimo.com/~weidai/cryptlib.html
pnglib:http://www.libpng.org/pub/png/libpng.html
下载它们,解压它们,编译它们。编译的时候注意要和eMule的工程的参数一致:字符集为Unicode,支持多线程安全,调试版或者是发行版可以根据需要选择,但是也要和eMule的工程参数一致。这些库的解压包里通常都能找到VC的工程文件,但是版本低一些,直接转化就可以了。另外建议编译这些库的时候都选择生成静态库,不要生成动态的库,这样最后生成的可执行文件就可以自己运行了。编译完这些库,包括从其它地方下载的和eMule自带的id3lib库和CxImage库后,就可以开始编译emule了。而编译emule也无非就是注意让它能够在编译的时候找到所有的头文件,以及在链接的时候能够找到所有的库。在链接的时候能够找到所有的库可以通过修改工程文件里面的属性->配置属性->链接器->输入->附加依赖项来完成。但是能够找到所有的头文件反而需要一些技巧了。由于emule的代码中对于这些库的头文件的包含,在一定程度上限定了那些库的路径和emule的工程的路径的相对位置,因此需要把那些解压过的库的目录移到一些合适的地方,有时还需要给这些目录改个名称。
3. eMule源代码学习心得(3):emule中最重要的几个基础设施
eMule中要读取的配置文件数量较多,每种配置文件都是自己定义的格式,为了方便读取和存储这些文件,eMule中有一个很重要的基础设施类来复制这些文件操作,它能够很方便得处理一些常用数据类型的读写,并且带有一定的安全保护机制。这项基础设施在SafeFile.cpp和SafeFile.h中实现。在kademlia\io目录下有这项功能的另外一项实现。它们实现的功能基本上相似,但是kademlia\io目录下的版本实现的时候多了一个以Tag作为单位进行读写的功能。这些实现中和另外一项基础设施,那就是字符串转化密切相关。StringConversion.cpp和StringConversion.h是 eMule中专门复制各类字符串转化的基础设施,什么Unicode啊,多字节流啊,或者是UTF-8之类的,在这里转化全部都不是问题。关于字符串转化,个人推荐尽量使用Unicode的宽字符,这样可以最大程度得避免乱码。
SafeFile.cpp或者kademlia\io目录下的实现都有这样的特点,那就是把数据操作的行为和数据操 作的对象分割开来。它们都定义了一个抽象的数据操作的基类(在SafeFile.cpp中是CFileDataIO,在kademlia目录下是 DataIO.cpp实现的Kademlia::CDataIO),这个类中只负责实现在逻辑上操作一项数据的行为,例如,要读取出一个32位的整型,那么就是读出四个字节到一个整型数值的地址中,要读取或者写入其它类型的数据用的是类似的方法。但是这个类把物理上进行数据操作的方法全部都声明为纯虚函数,即读出多少个字节,写入多少个字节这样的。有了这样一个基类,就可以非常方便得在它上面进行重载,把这些纯虚函数定义为向某块内存中进行读写的操作,就能很方便得将较为复杂的数据序列化到一块连续的内存,而如果这些纯虚函数是向文件读写的操作,那么自然就可以很方便得用来读写各种格式比较奇怪的自己定义的配置文件了。
这些类要读取的数据对象通常有这些,各种整型,字符串,以及Tag类型。整型读写起来比较简单,从1个字节的,2个字节的到4个,8个或者16个字节类型的数据读写方法都比较类似。这里要稍微提一下16个字节的那种,16个字节是128位,是eMule中的Kad网的随机生成的ID的长度,也是eMule中常用的MD4的hash算法生成的结果的长度。通常用来直接存取整个的这样的一个ID。在kademlia\utils目录下的UInt128.cpp实现了一个表示128位的整数的类,功能十分完善,可以进行一些算术操作,并且可以进行比较,这样为它以后作为 key出现在hash表中打下了基础。仔细学习UInt128.cpp中的代码实现可以学到很多在编写这种自定义的数据对象类型时应该注意的问题。
数据操作中另外一项很重要的操作是字符串,总的原则是先写一个长度,再写内容。但是到具体的操作的时候就需要注意这些细节了,如长度是写4个字节还是两个字节,字符串的内容要不要用UTF-8进行编码。这些操作就需要和StringConversion.cpp紧密合作了。其实后者的字符串转化函数很多也是调用ATL的相关函数,只是在外面再包上一层MFC的CString。
在kademlia\io\DataIO.cpp中实现的CDataIO中,还另外实现了按照Tag进行读写的功能。这在网络上交换共享文件的元信息非常重要,通常一个文件的元信息就可以分解成很多的Tag,如"文件名=xxx","文件长度=xxx"等等。也就是说,一个Tag就是表示某项属性等于某个值这样一个事实。在Opcodes.h这个文件中定义了很多的代码,其中就有很多常见的Tag的属性名称。CDataIO类中存储Tag的属性名都是先存一个字节的类型,再存名称,最后按照类型存值。
eMule中的这几项基础设施都是编写得比较好的,可以很方便得拿出来复用。像字符串编码的处理和具有一定数据结构的文件IO操作在很多地方都会很有用。eMule中这些类的实现基本上复制到其它的工程文件中只要稍微修改一下很快就能使用。以后我们还将看到eMule中很多其它很有用的基础设施。
4. eMule源代码学习心得(4):对自己的资源要了如指掌,CKnownFileList类的作用
emule作为一个文件共享方面的程序,首先要对自己共享的所有的文件的信息都十分清楚,类CKnownFileList的作用就是这样的,它在emule.cpp中随着cmuleapp类创建的时候被创建。
CKnownFileList类使用了MFC的CMap类来维护内部的hash表,这也可以看出emule和MFC的关系确实非常紧密。这里如果用STL的map其实也是可以的。它内部维护了一个已知的文件的列表和取消了的文件列表。这些hash表的关键字都是文件的hash值。这样能够判断出文件名不同而内容相同的文件,而一般要让不同内容的文件有相同的hash值是非常困难的,这也是hash函数它设计的初衷。因此除非是碰上王小云教授这样的牛人,我们基本上可以认为,两个文件hash值相同就代表了它们内容相同。再来看CKnownFileList.cpp,这个文件其实并不长,因为管理一个列表确实不需要太多种类的操作,如果对于每个具体的文件有一个很强大的类来处理它的话。而这里确实有,它就是CKnownFile。有了这么一个类,我们就可以看到,CKnownFileList类所需要做的工作就是能够根据一些信息查找到对应的CKnownFile类,能够复制其它的列表中的信息,能够把所有的这些信息存成文件,然后下次emule运行的时候能够把这些信息快速恢复出来,最重要的是能够在完成以上工作的情况下不造成内存泄漏。
CKnownFile类就是一个专门关注某个特定文件的信息的类,它仍然有其基类CAbstractFile。但是 它和CAbstractFile类的主要区别就是CAbstractFile类只有基本的信息存取的功能,而CKnownFile能够主动的生成这些信息,例如,给一个文件的路径给CKnownFile,它能够主动地去获取和这个文件有关的一切信息,并且把它保存在自己的成员变量里(CreateFromFile)。CKnownFile.cpp文件看上去比较长,是因为它做的工作比较多,现在版本的emule中,除了对某个文件进行全文hash以外,还采用了BT的方式,进行分块hash,这样在传输文件的时候,即使发生出错的情况,也可以不必重传整个文件,而只是重传有错误的那块,这种机制叫做高级智能损坏处理(AICH,Advanced Intelligent Corruption Handling),这个机制以后再继续分析。
CKnownFile把读到的文件信息都保存成一个一个的Tag。它在运行中会尽量得获取更多的文件信息,例如,对于媒体类型的文件,它能够调用id3lib库来获取诸如作者,唱片发行年代,风格等tag信息。如果是视频媒体文件,它还会去抓图(功能实现:CFrameGrabThread)。
CKnownFile还能够随时掌握目前该文件的下载情况(内部有个CUpDownClient的列表),当然,还会根据要求序列化和反序列化自己,LoadFromFile和WriteToFile都以CFileDataIO为参数,这样方便 CKnownFileList保存和读取它的列表中的所有文件的信息。
5. eMule源代码学习心得(5):分块机制--正确传输资源的保证
为了加快内容分发的速度,分块处理是一种简单有效的方法。emule中对每个文件都进行了分块处理。另外分块还有一个好处就是如果保留了每一分块的hash值,就能在只下载到文件的一部分时判断出下载内容的有效性。emule在获取每个共享文件的信息时,就对它进行了分块处理,因此如果要知道emule中的分块处理和恢复机制,看 CKnownFile::CreateFromFile函数的实现就行了。
这个函数中牵涉到的和分块处理以及hash计算相关的类都在SHAHashSet.cpp和SHAHashSet.h中。下面介绍其中几个主要的类:
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值。这样对于一个文件就有了两套分块验证的机制,能够适应不同场合。
6. eMule源代码学习心得(6):网络基础设施--网络基础设施的基础设施
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的子类,平时单独运行一个线程。下一次会详细描述它是如何控制全局的上传速度的。
7.eMule源代码学习心得(7):网络基础设施--全局限速器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)只使用一次就离开队列。而标准队列中的套接字不会这样。在一轮循环结束后,如果还有没有用完的发送数据的配额,则会有部分配额保存到下一轮。
8. eMule源代码学习心得(8):网络基础设施--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的要求。
9. eMule源代码学习心得(9):搜索信息集-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只负责和搜索有关的信息的储存和读取,本身并不进行搜索。
10. eMule源代码学习心得(10):服务器信息集-CServerList
嗯,明天要回北京了,今天打算好好休息一下,故挑了个内容少的来说,呵呵。
尽管目前有了Kad网络,但是使用服务器来获取各个emule用户的共享文件列表仍然是emule中主要的资源获取方式。CServerList就是emule中负责管理服务器列表的类。和前面若干列表类结构类似,CServerList需要对外提供列表的增加,删除,查找,修改等接口。在CServerList中,每个服务器的信息是一个CServer类。和搜索信息不一样,但是和已知文件列表一样,服务器的信息列表是需要长期保留的,因此CServerList和CKnownFileList类一样提供了把它所包含的所有信息保存到一个文件中,以及从这个文件中读回其信息的功能。
CServer中的结构比较简单,只需要保留服务器的各种信息即可。它可以通过IP地址和端口来创建,也可以通过一个简单的结构ServerMet_Struct来创建,其中后者是用来直接从文件中读取的。该结构仅仅包含IP地址和端口以及属性的个数,CServer 中其它的属性在保存到文件中时,均采用Tag方式保存。
CServerList除了提供通常的CServer信息外,还提供一些统计信息诸如所有的服务器的用户数,共享的文件数等。这些统计信息也是基于每个单独的CServer的相关信息计算出来的。
11. eMule源代码学习心得(11):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了,这里进行协议代码变化是为了使接受方能够正确识别并且进行相 应的解压操作。
12. eMule源代码学习心得(12):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来完成。
其它类型包没有必要全部都列出来,以后可以通过在分析其它部分时,因为牵涉到往服务器发送或者接受数据的时候再进行相应的分析。
13. eMule源代码学习心得(13):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类代码很长,以后再说。
14. eMule源代码学习心得(14):emule中的信誉机制
信誉机制在P2P系统中有非常重要的作用。为了使用户更加愿意共享自己的资源,需要有一些机制能够让对整个P2P系统贡献更大的用户有更多的激励。在emule中,激励机制的设计方案是 tit-for-tat这种最直观的方案。这种方案的意义就是最简单的如果别人对你好,那么你也对别人好。
下面看实际的实现。CClientCreditsList和CClientCredits类负责emule中的信誉机制。我们再次见到这种列表和元素之间的关系,不必再重复那些语言。和信誉相关的信息是需要永久保存的,这样才有意义,因此CClientCreditsList提供了LoadList和SaveList方法。我们另外注意到,CClientCredits类可以使用 CreditStruct结构来创建,而CreditStruct结构只包含静态信息。主要是上传量和下载量等。
信誉机制的信息需要有一定的可靠性,在emule中采用了数字签名的方式来做到这一点。Crypto++库为emule全程提供和数字签名验证相关的功能。CClientCreditsList在创建时,会装载自己的公钥私钥,如果没有的话,会创建一对。CClientCreditsList中包含的有效的信息都是经过其它人数字签名的,所以更加有信服力。在实际使用中,这些信息和自己的私钥要注意保存。重装emule后应该把配置文件目录先备份,这样能够保留自己辛辛苦苦积攒的信誉。
15. eMule源代码学习心得(15):下载任务即部分文件的表示
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方法,这个方法要做的事情就是在一些和日常运行有关的事情,例如检查为了下载该文件而链接到自己的各个客户端的状态,向它们发送下载请求等。
eMule源代码学习心得 (2)
2008年06月11日下午 01:23
16. eMule源代码学习心得(16):下载任务队列
CDownloadQueue是下载队列类。这个队列中的项目是CPartFile指针。因此和emule中出现的很多其它的列表类一样,它需要能够提供对这个列表中的元素进行增加,查询,删除的功能。例如查询的时候能够根据该文件的hashID或者索引来进行查询。CDownloadQueue同时还要完成一些统计工作。
和其它的列表类不一样的是,它的所有元素的信息并不是集中存放于一个文件,而是对应于每一个下载任务,单独得存放在一个元信息文件(.part.met)中,因此当该类进行初始化的时候,它需要寻找所有可能的下载路径,从那些路径中找到所有的.part.met文件,并且试图用这些文件来生成CPartFile类,并且将这些通过.part.met文件正确生成的CPartFile类添加到自己的列表中,同样,在退出时,所有的下载任务的元信息也是自行保存,不会合成为一个文件。
CDownloadQueue中的Process方法的主要任务就是把它的列表中的CPartFile类中的 Process方法都调一遍,另外主要的一些关于下载情况的统计信息也是在每一轮的Process后进行更新的。从这里我们也可以看出Process方法在emule中的意义,就是一个需要经常执行的方法,通过经常执行它们来完成日常工作,而且所有的这些Process方法肯定是顺序执行,因此可以减少很多多线程的同步之类的问题。emule中已经尽量减少了多线程的使用,但是在很多地方如果多线程是不可避免的话,也不会排斥。
17. eMule源代码学习心得(17):上传任务队列
CUploadQueue是上传队列类。这个列表类中只有以CUpDownClient为元素的列表,它和其它列表类还有一个很大的不同就是它所保存的信息都不需要持久化,即不需要在当前的 emule退出后还记住自己正在给谁上传文件,然后下次上线的时候再继续给他们传,这在大部分情况下是没有意义的。
上传队列类列表中有两个列表,上传列表和排队列表。当一个收到一个新的下载请求后,它会把对应的客户端先添加到排队列表中,以后再根据情况,把它们不断添加到上传列表中。在这里,信誉机制将会对此产生影响。
CUploadQueue的Process方法就相对简单了,那就是向上传队列中的所有客户端依次发送数据,而排队的客户端是不会得到这个机会的。另外它还需要完成关于上传方面的一些统计信息。
另外我们还需要注意在CUploadQueue的构造函数里面,创建了一个以100毫秒为间隔的定时器,这个定时器成为以上所有的Process所需要的基础。我们看它的UploadTimer就可以看出这一点。这里面充斥了各个类的Process方法的执行,其中包括以前我们提到的一些类,但是没有提到它们的Process方法,因为其过于简单,基本上就只是更新了一下要保存的信息。
18. eMule源代码学习心得(18):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协议进行包装,以便使它能够尽可能地穿越更多的网络的防火墙。
19. eMule源代码学习心得(19):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中相当重要的一部分,因此特意把这一部分单独拿出来,把它放在这个小结之后进行描述。
20. eMule源代码学习心得(20):emule中的Kademlia代码总体描述
首先要说一下抱歉,这两个星期的事情比较多,所以现在才来写。不过总算是把这些东西写完了。而且最后用的代码是0.47c。
当emule中开始使用Kademlia网络后,便不再会有中心服务器失效这样的问题了,因为在这个网络中,没有中心服务器,或者说,所有的用户都是服务器,所有的用户也是客户端,从而完完全全得实现了P2P。接下来讲针对emule中的Kademlia网络进行分析,会有一节进行原理方面的分析。另外的几节将会根据emule中实现Kademlia所使用的不同的类分别进行讲述。其中:
CKademlia是整个Kademlia网络的主控类,可以直接开始或者停止Kademlia网,并且含有Process方法来处理日常事务。
CPrefs负责处理自身的Kademlia相关信息,如自身的ID等。
CRoutingZone,CRoutingBin和CContact三个类组成了每个节点所了解的联系信息以及由这些联系信息所组成的数据结构。
CKademliaUDPListener负责处理网络信息。
CIndexed负责处理本地存储的索引信息。
CSearch,CSearchManager负责处理和搜索有关的操作,其中前者表示的是一个单一的搜索任务,后者负责对所有搜索任务进行处理。
CUInt128负责处理一个128位的长整数,并且内置其各种运算。前面已经提到过
21. eMule源代码学习心得(21):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值进行查找。非结构化的网络的查找本身就是随意转发的,每个收到的查询请求的节点都对本地的资源掌握的很清楚,因此自然可以支持复杂查询,但是显然非结构化的网络支持的复杂查询不太可能动员所有的节点都来做这一动作。目前还没有方法能够把两种覆盖网络的优点结合起来,我也非常想知道这样的一种方法。
22.eMule源代码学习心得(22):emule中的Kademlia的基础设施类
Kademlia的主控类是CKademlia,它负责启动和关闭整个Kademlia网的相关代码。在它的Process函数中,会处理和Kademlia网相关的事务,例如隔一段时间检查某个区间的节点数是否过少,如果是则寻找一些新的节点。另外经常对自己的邻居进行检查等,这些都是属于需要进行日常安排的工作。所有搜索任务的日常处理也需要它来调度。它还作为Kademlia网的代表,向emule其它部分的代码返回Kademlia网的一些统计信息。
另一个基础设施类是CPrefs,它和emule普通代码中的CPreferences作用类似,但是CPrefs只保留和Kademlia网相关的,需要长期保存的本地信息。具体到这个版本来说,主要就是本地的ID。
还有一个很重要的基础设施就是CUInt128,实现对128位的ID的各种处理,前面的部分已经提到。
23. eMule源代码学习心得(23):emule中的Kademlia的联系人列表管理
CRoutingZone,CRoutingBin和CContact三个类组成了联系人列表数据结构。它要达到我们搜索的要求,即搜索到目标的时间要能够接受,而且所占用的空间也要能够接受。
首先CContact类包含的是一个联系人的信息,主要包括对方的IP地址,ID,TCP端口,UDP端口,kad 版本号和其健康程度(m_byType)。其中健康程度有0-4五个等级。刚刚加入的联系人,也就是健康状况未知的,这个数值设置为3。系统会经常通过与各个联系人进行联系的方式对其进行健康状况检查,经常能够联系上的联系人,这个数值会慢慢减少到0。而很就没有联系的,这个数值会慢慢增加,如果增加到4 后再过一段时间未能成功联系上的,则将会被从联系人列表中删除。
CRoutingBin类包含一个CContact的列表。这里要注意的是要访问联系人的信息必须通过某个CRoutingBin,CRoutingZone内部是不直接包含联系人信息的。可以把新的联系人信息往一个特定的CRoutingBin中加,当然也可以进行联系人查找。它也提供方法能够寻找出离某个ID距离最近的联系人,并给出这样的一个列表。这是相当重要的。最后,一个CRoutingBin类中能够包含的CContact的数量也是有限制的。
CRoutingZone类处于联系人数据结构的最上层,直接为Kademlia网提供操作接口。该类的结构为一个二叉树,内含两个CRoutingZone指向它的左子树和右子树,另外也包含一个CRoutingBin类型的指针。但是只有在当前的 CRoutingZone类为整个二叉树的叶节点时,这个指向CRoutingBin类型的指针才有意义。这个二叉树的特点是,每个节点以下的所有联系人的ID都包含一个共同前缀,节点的层数越深,这个共同前缀越长。例如,根节点的左子树的所有的节点的ID一定有一个前缀"0",而右子树的所有节点一定有 前缀"1"。同样,根节点的左子树的右子树下的所有节点的ID一定有前缀"01",等等,依此类推。我们设想一下节点不断得往这个二叉树添加的过程。刚开始只有一个根节点,它也就是叶节点,这时它内部的CRoutingBin是有意义的,当联系人信息不断得被添加进去以后,这个CRoutingBin的容量满了,这时要进行的就是一个分裂的操作。这时,会添加两个左子节点和右子节点,然后把自身的CRoutingBin中的联系人信息按照它们的前缀特点分别复制往左节点和右节点,最后把自身的CRoutingBin废除掉,这样这个分裂过程就完了。当分裂完成后,就会再次试图添加该联系人信息,此时会试图按照它的ID,把它添加到对应的子树中。但是并不是所有的这种情况节点都会发生分裂,因为如果允许任意分裂的话,本地所需存储的节点信息数量就会急剧上升。这里,自身ID的作用就体现了。只有当自身ID和当前准备分裂的节点有共同前缀时,这个节点才会分裂,而如果判断到一个节点不能分裂,而它的 CRoutingBin又满掉了,那么就会拒绝添加联系人信息。
我们可以看出,在以上政策的进行下,离自身ID逻辑距离越近(也就是共同前缀越长)的联系人信息越有可能被加入,因为它所对应的节点越有可能因为分裂而获得更多的子节点,也就对应了更多的容量。这样,在Kademlia网中,每一个参与者知道的其它参与者信息中,离自己逻辑距离越近的参与者比例越高。由于在搜索的时候也只需要不断得寻找更近的ID,而且每一步都一定会有进展,所以寻找到目标ID所需要的时间上的代价是 O(logn),从这个二叉树的结构来看,我们也可以看到,由于只有部分节点会分裂,所以实质上存储所需要的空间代价也是O(logn)。
实际上CRoutingZone在实现时和理论上的Kademlia有一些区别,如从根节点开始,有一个最低分裂层数,也就是说,如果层数过低的话,是永远允许分裂的,这样它知道的其它地区的联系人信息就能够稍微多一些。
24. eMule源代码学习心得(24):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里面增加的这个特性,就会对那种还没有发出请求就收到回应的情况直接无视,从而避免被愚弄。
25. eMule源代码学习心得(25):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来进行处理了。
26. eMule源代码学习心得(26):emule中的Kademlia搜索任务管理
CSearch 和CSearchManager是完成具体搜索任务的。CSearch对应的是一个具体的搜索任务,它包括了一个搜索任务从发起到结束的全部过程,要注意的是搜索任务并不只是指搜索文件源或者关键词的任务,一次发布任务它也需要创建一个CSearch对象,并且让
eMule的官方首页上写着:2002年05月13日一个叫做Merkur的人,他不满意原始eDonkey2000客户端并且坚信他能够做的更好,所以他开始制作。他聚集了其它开发人员在他的周围,并且eMule工程就此诞生。
eMule是一个典型的MFC程序,它的图形界面等,已经和MFC紧紧融合到了一起。因此通常情况下它只能在windows平台下运行。有一些其它的工程,如aMule等,把它进行了移植,因此跨平台的功能要强些。
其实还有另外一个叫做xMule的工程,不过现在已经人气快不行了。在aMule的主页上可以看到eMule移植到linux平台下的一些历史,最早是有个叫做lMule的工程,他使用wxwidgets来进行eMule的跨平台的移植,这个工程2003年就不再更新了,后来转变成为xMule工程,它一度是linux平台下eMule的事实上的替代品。但是他们的程序员之间由于理念不同,发生了内讧,导致aMule 分裂出来,他们后来矛盾严重的时候曾经一度从理念问题上升到互相对对方进行人身攻击,并且曾经对对方的网站发动过DDos。后来aMule和xMule就是两个完全不同的工程,xMule现在只有HopeSeekr一个人在维护,基本上也没有什么更新了。这一点不仅让人感慨。今年寒假的时候我曾经和 HopeSeekr进行过一些交流,感觉他非常自信,经常拿着aMule的一部分代码来给我看,说你看看他们的代码这么这么写,这简直就是一陀xx嘛,这种代码在某些情况下肯定会Crash掉嘛,相反,你看看我们xMule的代码,这里是这样这样,肯定就不会有这种问题了。
eMule从0.42版开始支持Kad技术,这是一个非常重要的里程碑。Kad是一种DHT的协议,它可以使节点之间互相保留一些其它节点的联系信息,并且利用这样一个“关系网”寻找到整个网络中的任何一个节点以及上面的资源,整个过程不需要任何中心服务器。因此向当年搞napster那样直接端掉中心服务器就搞跨napster网络一样来对付eMule的Kad网就毫无作用了。0.42版是2004年2月27日放出 的,比eDonkey2000的OverNet晚了将近一年,但是它的Kad网络的规模却在迅速扩大。Overnet和eMule中的Kad使用的都是Kademlia结构,但是具体的消息报文的格式有区别,因此两个DHT网络并不能互相兼容。OverNet直到现在,用户也仍然维持在十万左右,这是比较近的一篇文章写的。但是eMule的Kad网的规模有多大,目前却没有一个很准确的说法。由此也可以看出开源软件的力量。目前aMule的Kad网和eMule的Kad网是兼容的,xMule中还没有Kad的支持。Kad协议的原始paper可以在我们实验室的机器上下到:
http://bigpc.net.pku.edu.cn:8080/paper/new/by%20conference/IPTPS/IPTPS02/Kademlia%20A%20Peer-to-Peer%20Information%20System%20Based%20on%20the%20XOR%20Metric%28IPTPS02%29.pdf
2. eMule源代码学习心得(2):从emule.cpp开始,顺便谈如何编译emule
先说一声抱歉,因为前两天刚回到家中,休息了一下,所以这两天没有更新。今天继续昨天的话题。
eMule的代码结构非常合理。虽然代码量比较大,但是各个功能模块之间的划分都很合理。从它的工程文件里面就可以看出这一点。eMule把表示功能的代码文件和表示界面的代码文件分开了,Source Files和Header Files是实现功能的代码的源文件和头文件,而Interface Source和Interface Header是实现图形界面的源文件和头文件。由于eMule的代码量太大,本系列将跳过图形界面的实现,着重分析eMule的功能实现部分的代码,而且功能实现方面也主要挑主要的部分分析。本节将从emule.cpp开始分析,引出eMule中使用到的几个主要的功能实现的类,并大体描述它们的作用。最后介绍一下如何在VS2003下编译eMule。emule中还有不少模块实现的功能挺有用,我说的是在其它的程序里很有用,可以考虑在其它程序中进行复用。
emule.cpp为类CemuleApp的实现。因此在运行时,首先会运行InitInstance进行一些初始化的工作。从这个函数里面我们也可以第一次看出那些即将在整个程序中发挥作用的类了。
最开始的时候是计算出程序常用的一些目录,如配置文件,日志文件等。接下来是ProcessCommandline,它的作用有两方面,第一是确认该eMule的运行方式,即命令行后面有没有参数,第二是确认目前eMule是不是只有一个实例在运行。在一般的情况下,双击eMule可执行文件是不会带参数的。但是通过点击链接或者打开关联文件的方式打开eMule则相当于带参数运行eMule。通过在注册表里添加一些项目可以让一个程序和某种链接或者某个后缀的文件产生关联。具体办法可以参见OtherFunctions.cpp中的Ask4RegFix,BackupReg,RevertReg三个函数的功能。ProcessCommandline中通过创建带有名称的互斥信号量来确认是否有其它的eMule实例在运行。对于一个确定的名称,CreateMutex只能创建一个互斥信号量。因此通过该信号量是否创建成功就可以知道是否有其它eMule实例运行。如果有的话,而且又是带参数的那种模式,那么直接把这个参数使用Windows的消息机制发给那个窗口即可,接下来的代码无非就是如何找到另外一个叫"eMule"的家伙以及给它发个什么消息。pstrPendingLink是一个全局变量,表示将要被处理的命令行参数。它将会在初始化完成后一段时间后被处理。
下面两个比较重要的类是CPreferences和CStatistics。前者掌握着程序的大部分配置数据,后者则进行各种统计。它们的特点都是有很多的成员变量,而且还是静态的,这种方式可以保证它们的唯一性,而且把这些变量统一到一个类管理。但是实际上并不需要了解每个变量的含义。thePrefs和theStats是这两个类的唯一的实例。
在处理完其它一些事情,包括创建图形界面对象CemuleDlg后,接下来可以看到一排一排的创建新对象的语句。这些类将会实现eMule程序运行的主要功能,后面的系列将详细分析。
最后描述一下如何在VS2003里编译eMule,由于eMule中用到了一些其它的库,因此官方在提供eMule的源代码下载的同时如果也提供这些库的下载会使源码包变得很大。因此eMule选择了让开发者去那些库的官方网站下载它们的方式。一般来说,编译这种工程文件,很重要的地方是要保持各个库和主程序编译参数的一致性。这些编译参数中,最主要的参数有三个,字符集(多字节/Unicode),调试/发行,单线程/多线程,这样排列组合一下就有八个版本了。因此编译的时候如果不注意,就会出现和程序设计无关的错误。
eMule0.47a解压后自带id3lib库,还需要下载以下的库:
zlib:http://www.gzip.org/zlib/
ResizableLib:http://sourceforge.net/projects/resizablelib/
Crypto++:http://www.eskimo.com/~weidai/cryptlib.html
pnglib:http://www.libpng.org/pub/png/libpng.html
下载它们,解压它们,编译它们。编译的时候注意要和eMule的工程的参数一致:字符集为Unicode,支持多线程安全,调试版或者是发行版可以根据需要选择,但是也要和eMule的工程参数一致。这些库的解压包里通常都能找到VC的工程文件,但是版本低一些,直接转化就可以了。另外建议编译这些库的时候都选择生成静态库,不要生成动态的库,这样最后生成的可执行文件就可以自己运行了。编译完这些库,包括从其它地方下载的和eMule自带的id3lib库和CxImage库后,就可以开始编译emule了。而编译emule也无非就是注意让它能够在编译的时候找到所有的头文件,以及在链接的时候能够找到所有的库。在链接的时候能够找到所有的库可以通过修改工程文件里面的属性->配置属性->链接器->输入->附加依赖项来完成。但是能够找到所有的头文件反而需要一些技巧了。由于emule的代码中对于这些库的头文件的包含,在一定程度上限定了那些库的路径和emule的工程的路径的相对位置,因此需要把那些解压过的库的目录移到一些合适的地方,有时还需要给这些目录改个名称。
3. eMule源代码学习心得(3):emule中最重要的几个基础设施
eMule中要读取的配置文件数量较多,每种配置文件都是自己定义的格式,为了方便读取和存储这些文件,eMule中有一个很重要的基础设施类来复制这些文件操作,它能够很方便得处理一些常用数据类型的读写,并且带有一定的安全保护机制。这项基础设施在SafeFile.cpp和SafeFile.h中实现。在kademlia\io目录下有这项功能的另外一项实现。它们实现的功能基本上相似,但是kademlia\io目录下的版本实现的时候多了一个以Tag作为单位进行读写的功能。这些实现中和另外一项基础设施,那就是字符串转化密切相关。StringConversion.cpp和StringConversion.h是 eMule中专门复制各类字符串转化的基础设施,什么Unicode啊,多字节流啊,或者是UTF-8之类的,在这里转化全部都不是问题。关于字符串转化,个人推荐尽量使用Unicode的宽字符,这样可以最大程度得避免乱码。
SafeFile.cpp或者kademlia\io目录下的实现都有这样的特点,那就是把数据操作的行为和数据操 作的对象分割开来。它们都定义了一个抽象的数据操作的基类(在SafeFile.cpp中是CFileDataIO,在kademlia目录下是 DataIO.cpp实现的Kademlia::CDataIO),这个类中只负责实现在逻辑上操作一项数据的行为,例如,要读取出一个32位的整型,那么就是读出四个字节到一个整型数值的地址中,要读取或者写入其它类型的数据用的是类似的方法。但是这个类把物理上进行数据操作的方法全部都声明为纯虚函数,即读出多少个字节,写入多少个字节这样的。有了这样一个基类,就可以非常方便得在它上面进行重载,把这些纯虚函数定义为向某块内存中进行读写的操作,就能很方便得将较为复杂的数据序列化到一块连续的内存,而如果这些纯虚函数是向文件读写的操作,那么自然就可以很方便得用来读写各种格式比较奇怪的自己定义的配置文件了。
这些类要读取的数据对象通常有这些,各种整型,字符串,以及Tag类型。整型读写起来比较简单,从1个字节的,2个字节的到4个,8个或者16个字节类型的数据读写方法都比较类似。这里要稍微提一下16个字节的那种,16个字节是128位,是eMule中的Kad网的随机生成的ID的长度,也是eMule中常用的MD4的hash算法生成的结果的长度。通常用来直接存取整个的这样的一个ID。在kademlia\utils目录下的UInt128.cpp实现了一个表示128位的整数的类,功能十分完善,可以进行一些算术操作,并且可以进行比较,这样为它以后作为 key出现在hash表中打下了基础。仔细学习UInt128.cpp中的代码实现可以学到很多在编写这种自定义的数据对象类型时应该注意的问题。
数据操作中另外一项很重要的操作是字符串,总的原则是先写一个长度,再写内容。但是到具体的操作的时候就需要注意这些细节了,如长度是写4个字节还是两个字节,字符串的内容要不要用UTF-8进行编码。这些操作就需要和StringConversion.cpp紧密合作了。其实后者的字符串转化函数很多也是调用ATL的相关函数,只是在外面再包上一层MFC的CString。
在kademlia\io\DataIO.cpp中实现的CDataIO中,还另外实现了按照Tag进行读写的功能。这在网络上交换共享文件的元信息非常重要,通常一个文件的元信息就可以分解成很多的Tag,如"文件名=xxx","文件长度=xxx"等等。也就是说,一个Tag就是表示某项属性等于某个值这样一个事实。在Opcodes.h这个文件中定义了很多的代码,其中就有很多常见的Tag的属性名称。CDataIO类中存储Tag的属性名都是先存一个字节的类型,再存名称,最后按照类型存值。
eMule中的这几项基础设施都是编写得比较好的,可以很方便得拿出来复用。像字符串编码的处理和具有一定数据结构的文件IO操作在很多地方都会很有用。eMule中这些类的实现基本上复制到其它的工程文件中只要稍微修改一下很快就能使用。以后我们还将看到eMule中很多其它很有用的基础设施。
4. eMule源代码学习心得(4):对自己的资源要了如指掌,CKnownFileList类的作用
emule作为一个文件共享方面的程序,首先要对自己共享的所有的文件的信息都十分清楚,类CKnownFileList的作用就是这样的,它在emule.cpp中随着cmuleapp类创建的时候被创建。
CKnownFileList类使用了MFC的CMap类来维护内部的hash表,这也可以看出emule和MFC的关系确实非常紧密。这里如果用STL的map其实也是可以的。它内部维护了一个已知的文件的列表和取消了的文件列表。这些hash表的关键字都是文件的hash值。这样能够判断出文件名不同而内容相同的文件,而一般要让不同内容的文件有相同的hash值是非常困难的,这也是hash函数它设计的初衷。因此除非是碰上王小云教授这样的牛人,我们基本上可以认为,两个文件hash值相同就代表了它们内容相同。再来看CKnownFileList.cpp,这个文件其实并不长,因为管理一个列表确实不需要太多种类的操作,如果对于每个具体的文件有一个很强大的类来处理它的话。而这里确实有,它就是CKnownFile。有了这么一个类,我们就可以看到,CKnownFileList类所需要做的工作就是能够根据一些信息查找到对应的CKnownFile类,能够复制其它的列表中的信息,能够把所有的这些信息存成文件,然后下次emule运行的时候能够把这些信息快速恢复出来,最重要的是能够在完成以上工作的情况下不造成内存泄漏。
CKnownFile类就是一个专门关注某个特定文件的信息的类,它仍然有其基类CAbstractFile。但是 它和CAbstractFile类的主要区别就是CAbstractFile类只有基本的信息存取的功能,而CKnownFile能够主动的生成这些信息,例如,给一个文件的路径给CKnownFile,它能够主动地去获取和这个文件有关的一切信息,并且把它保存在自己的成员变量里(CreateFromFile)。CKnownFile.cpp文件看上去比较长,是因为它做的工作比较多,现在版本的emule中,除了对某个文件进行全文hash以外,还采用了BT的方式,进行分块hash,这样在传输文件的时候,即使发生出错的情况,也可以不必重传整个文件,而只是重传有错误的那块,这种机制叫做高级智能损坏处理(AICH,Advanced Intelligent Corruption Handling),这个机制以后再继续分析。
CKnownFile把读到的文件信息都保存成一个一个的Tag。它在运行中会尽量得获取更多的文件信息,例如,对于媒体类型的文件,它能够调用id3lib库来获取诸如作者,唱片发行年代,风格等tag信息。如果是视频媒体文件,它还会去抓图(功能实现:CFrameGrabThread)。
CKnownFile还能够随时掌握目前该文件的下载情况(内部有个CUpDownClient的列表),当然,还会根据要求序列化和反序列化自己,LoadFromFile和WriteToFile都以CFileDataIO为参数,这样方便 CKnownFileList保存和读取它的列表中的所有文件的信息。
5. eMule源代码学习心得(5):分块机制--正确传输资源的保证
为了加快内容分发的速度,分块处理是一种简单有效的方法。emule中对每个文件都进行了分块处理。另外分块还有一个好处就是如果保留了每一分块的hash值,就能在只下载到文件的一部分时判断出下载内容的有效性。emule在获取每个共享文件的信息时,就对它进行了分块处理,因此如果要知道emule中的分块处理和恢复机制,看 CKnownFile::CreateFromFile函数的实现就行了。
这个函数中牵涉到的和分块处理以及hash计算相关的类都在SHAHashSet.cpp和SHAHashSet.h中。下面介绍其中几个主要的类:
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值。这样对于一个文件就有了两套分块验证的机制,能够适应不同场合。
6. eMule源代码学习心得(6):网络基础设施--网络基础设施的基础设施
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的子类,平时单独运行一个线程。下一次会详细描述它是如何控制全局的上传速度的。
7.eMule源代码学习心得(7):网络基础设施--全局限速器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)只使用一次就离开队列。而标准队列中的套接字不会这样。在一轮循环结束后,如果还有没有用完的发送数据的配额,则会有部分配额保存到下一轮。
8. eMule源代码学习心得(8):网络基础设施--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的要求。
9. eMule源代码学习心得(9):搜索信息集-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只负责和搜索有关的信息的储存和读取,本身并不进行搜索。
10. eMule源代码学习心得(10):服务器信息集-CServerList
嗯,明天要回北京了,今天打算好好休息一下,故挑了个内容少的来说,呵呵。
尽管目前有了Kad网络,但是使用服务器来获取各个emule用户的共享文件列表仍然是emule中主要的资源获取方式。CServerList就是emule中负责管理服务器列表的类。和前面若干列表类结构类似,CServerList需要对外提供列表的增加,删除,查找,修改等接口。在CServerList中,每个服务器的信息是一个CServer类。和搜索信息不一样,但是和已知文件列表一样,服务器的信息列表是需要长期保留的,因此CServerList和CKnownFileList类一样提供了把它所包含的所有信息保存到一个文件中,以及从这个文件中读回其信息的功能。
CServer中的结构比较简单,只需要保留服务器的各种信息即可。它可以通过IP地址和端口来创建,也可以通过一个简单的结构ServerMet_Struct来创建,其中后者是用来直接从文件中读取的。该结构仅仅包含IP地址和端口以及属性的个数,CServer 中其它的属性在保存到文件中时,均采用Tag方式保存。
CServerList除了提供通常的CServer信息外,还提供一些统计信息诸如所有的服务器的用户数,共享的文件数等。这些统计信息也是基于每个单独的CServer的相关信息计算出来的。
11. eMule源代码学习心得(11):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了,这里进行协议代码变化是为了使接受方能够正确识别并且进行相 应的解压操作。
12. eMule源代码学习心得(12):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来完成。
其它类型包没有必要全部都列出来,以后可以通过在分析其它部分时,因为牵涉到往服务器发送或者接受数据的时候再进行相应的分析。
13. eMule源代码学习心得(13):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类代码很长,以后再说。
14. eMule源代码学习心得(14):emule中的信誉机制
信誉机制在P2P系统中有非常重要的作用。为了使用户更加愿意共享自己的资源,需要有一些机制能够让对整个P2P系统贡献更大的用户有更多的激励。在emule中,激励机制的设计方案是 tit-for-tat这种最直观的方案。这种方案的意义就是最简单的如果别人对你好,那么你也对别人好。
下面看实际的实现。CClientCreditsList和CClientCredits类负责emule中的信誉机制。我们再次见到这种列表和元素之间的关系,不必再重复那些语言。和信誉相关的信息是需要永久保存的,这样才有意义,因此CClientCreditsList提供了LoadList和SaveList方法。我们另外注意到,CClientCredits类可以使用 CreditStruct结构来创建,而CreditStruct结构只包含静态信息。主要是上传量和下载量等。
信誉机制的信息需要有一定的可靠性,在emule中采用了数字签名的方式来做到这一点。Crypto++库为emule全程提供和数字签名验证相关的功能。CClientCreditsList在创建时,会装载自己的公钥私钥,如果没有的话,会创建一对。CClientCreditsList中包含的有效的信息都是经过其它人数字签名的,所以更加有信服力。在实际使用中,这些信息和自己的私钥要注意保存。重装emule后应该把配置文件目录先备份,这样能够保留自己辛辛苦苦积攒的信誉。
15. eMule源代码学习心得(15):下载任务即部分文件的表示
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方法,这个方法要做的事情就是在一些和日常运行有关的事情,例如检查为了下载该文件而链接到自己的各个客户端的状态,向它们发送下载请求等。
eMule源代码学习心得 (2)
2008年06月11日下午 01:23
16. eMule源代码学习心得(16):下载任务队列
CDownloadQueue是下载队列类。这个队列中的项目是CPartFile指针。因此和emule中出现的很多其它的列表类一样,它需要能够提供对这个列表中的元素进行增加,查询,删除的功能。例如查询的时候能够根据该文件的hashID或者索引来进行查询。CDownloadQueue同时还要完成一些统计工作。
和其它的列表类不一样的是,它的所有元素的信息并不是集中存放于一个文件,而是对应于每一个下载任务,单独得存放在一个元信息文件(.part.met)中,因此当该类进行初始化的时候,它需要寻找所有可能的下载路径,从那些路径中找到所有的.part.met文件,并且试图用这些文件来生成CPartFile类,并且将这些通过.part.met文件正确生成的CPartFile类添加到自己的列表中,同样,在退出时,所有的下载任务的元信息也是自行保存,不会合成为一个文件。
CDownloadQueue中的Process方法的主要任务就是把它的列表中的CPartFile类中的 Process方法都调一遍,另外主要的一些关于下载情况的统计信息也是在每一轮的Process后进行更新的。从这里我们也可以看出Process方法在emule中的意义,就是一个需要经常执行的方法,通过经常执行它们来完成日常工作,而且所有的这些Process方法肯定是顺序执行,因此可以减少很多多线程的同步之类的问题。emule中已经尽量减少了多线程的使用,但是在很多地方如果多线程是不可避免的话,也不会排斥。
17. eMule源代码学习心得(17):上传任务队列
CUploadQueue是上传队列类。这个列表类中只有以CUpDownClient为元素的列表,它和其它列表类还有一个很大的不同就是它所保存的信息都不需要持久化,即不需要在当前的 emule退出后还记住自己正在给谁上传文件,然后下次上线的时候再继续给他们传,这在大部分情况下是没有意义的。
上传队列类列表中有两个列表,上传列表和排队列表。当一个收到一个新的下载请求后,它会把对应的客户端先添加到排队列表中,以后再根据情况,把它们不断添加到上传列表中。在这里,信誉机制将会对此产生影响。
CUploadQueue的Process方法就相对简单了,那就是向上传队列中的所有客户端依次发送数据,而排队的客户端是不会得到这个机会的。另外它还需要完成关于上传方面的一些统计信息。
另外我们还需要注意在CUploadQueue的构造函数里面,创建了一个以100毫秒为间隔的定时器,这个定时器成为以上所有的Process所需要的基础。我们看它的UploadTimer就可以看出这一点。这里面充斥了各个类的Process方法的执行,其中包括以前我们提到的一些类,但是没有提到它们的Process方法,因为其过于简单,基本上就只是更新了一下要保存的信息。
18. eMule源代码学习心得(18):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协议进行包装,以便使它能够尽可能地穿越更多的网络的防火墙。
19. eMule源代码学习心得(19):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中相当重要的一部分,因此特意把这一部分单独拿出来,把它放在这个小结之后进行描述。
20. eMule源代码学习心得(20):emule中的Kademlia代码总体描述
首先要说一下抱歉,这两个星期的事情比较多,所以现在才来写。不过总算是把这些东西写完了。而且最后用的代码是0.47c。
当emule中开始使用Kademlia网络后,便不再会有中心服务器失效这样的问题了,因为在这个网络中,没有中心服务器,或者说,所有的用户都是服务器,所有的用户也是客户端,从而完完全全得实现了P2P。接下来讲针对emule中的Kademlia网络进行分析,会有一节进行原理方面的分析。另外的几节将会根据emule中实现Kademlia所使用的不同的类分别进行讲述。其中:
CKademlia是整个Kademlia网络的主控类,可以直接开始或者停止Kademlia网,并且含有Process方法来处理日常事务。
CPrefs负责处理自身的Kademlia相关信息,如自身的ID等。
CRoutingZone,CRoutingBin和CContact三个类组成了每个节点所了解的联系信息以及由这些联系信息所组成的数据结构。
CKademliaUDPListener负责处理网络信息。
CIndexed负责处理本地存储的索引信息。
CSearch,CSearchManager负责处理和搜索有关的操作,其中前者表示的是一个单一的搜索任务,后者负责对所有搜索任务进行处理。
CUInt128负责处理一个128位的长整数,并且内置其各种运算。前面已经提到过
21. eMule源代码学习心得(21):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值进行查找。非结构化的网络的查找本身就是随意转发的,每个收到的查询请求的节点都对本地的资源掌握的很清楚,因此自然可以支持复杂查询,但是显然非结构化的网络支持的复杂查询不太可能动员所有的节点都来做这一动作。目前还没有方法能够把两种覆盖网络的优点结合起来,我也非常想知道这样的一种方法。
22.eMule源代码学习心得(22):emule中的Kademlia的基础设施类
Kademlia的主控类是CKademlia,它负责启动和关闭整个Kademlia网的相关代码。在它的Process函数中,会处理和Kademlia网相关的事务,例如隔一段时间检查某个区间的节点数是否过少,如果是则寻找一些新的节点。另外经常对自己的邻居进行检查等,这些都是属于需要进行日常安排的工作。所有搜索任务的日常处理也需要它来调度。它还作为Kademlia网的代表,向emule其它部分的代码返回Kademlia网的一些统计信息。
另一个基础设施类是CPrefs,它和emule普通代码中的CPreferences作用类似,但是CPrefs只保留和Kademlia网相关的,需要长期保存的本地信息。具体到这个版本来说,主要就是本地的ID。
还有一个很重要的基础设施就是CUInt128,实现对128位的ID的各种处理,前面的部分已经提到。
23. eMule源代码学习心得(23):emule中的Kademlia的联系人列表管理
CRoutingZone,CRoutingBin和CContact三个类组成了联系人列表数据结构。它要达到我们搜索的要求,即搜索到目标的时间要能够接受,而且所占用的空间也要能够接受。
首先CContact类包含的是一个联系人的信息,主要包括对方的IP地址,ID,TCP端口,UDP端口,kad 版本号和其健康程度(m_byType)。其中健康程度有0-4五个等级。刚刚加入的联系人,也就是健康状况未知的,这个数值设置为3。系统会经常通过与各个联系人进行联系的方式对其进行健康状况检查,经常能够联系上的联系人,这个数值会慢慢减少到0。而很就没有联系的,这个数值会慢慢增加,如果增加到4 后再过一段时间未能成功联系上的,则将会被从联系人列表中删除。
CRoutingBin类包含一个CContact的列表。这里要注意的是要访问联系人的信息必须通过某个CRoutingBin,CRoutingZone内部是不直接包含联系人信息的。可以把新的联系人信息往一个特定的CRoutingBin中加,当然也可以进行联系人查找。它也提供方法能够寻找出离某个ID距离最近的联系人,并给出这样的一个列表。这是相当重要的。最后,一个CRoutingBin类中能够包含的CContact的数量也是有限制的。
CRoutingZone类处于联系人数据结构的最上层,直接为Kademlia网提供操作接口。该类的结构为一个二叉树,内含两个CRoutingZone指向它的左子树和右子树,另外也包含一个CRoutingBin类型的指针。但是只有在当前的 CRoutingZone类为整个二叉树的叶节点时,这个指向CRoutingBin类型的指针才有意义。这个二叉树的特点是,每个节点以下的所有联系人的ID都包含一个共同前缀,节点的层数越深,这个共同前缀越长。例如,根节点的左子树的所有的节点的ID一定有一个前缀"0",而右子树的所有节点一定有 前缀"1"。同样,根节点的左子树的右子树下的所有节点的ID一定有前缀"01",等等,依此类推。我们设想一下节点不断得往这个二叉树添加的过程。刚开始只有一个根节点,它也就是叶节点,这时它内部的CRoutingBin是有意义的,当联系人信息不断得被添加进去以后,这个CRoutingBin的容量满了,这时要进行的就是一个分裂的操作。这时,会添加两个左子节点和右子节点,然后把自身的CRoutingBin中的联系人信息按照它们的前缀特点分别复制往左节点和右节点,最后把自身的CRoutingBin废除掉,这样这个分裂过程就完了。当分裂完成后,就会再次试图添加该联系人信息,此时会试图按照它的ID,把它添加到对应的子树中。但是并不是所有的这种情况节点都会发生分裂,因为如果允许任意分裂的话,本地所需存储的节点信息数量就会急剧上升。这里,自身ID的作用就体现了。只有当自身ID和当前准备分裂的节点有共同前缀时,这个节点才会分裂,而如果判断到一个节点不能分裂,而它的 CRoutingBin又满掉了,那么就会拒绝添加联系人信息。
我们可以看出,在以上政策的进行下,离自身ID逻辑距离越近(也就是共同前缀越长)的联系人信息越有可能被加入,因为它所对应的节点越有可能因为分裂而获得更多的子节点,也就对应了更多的容量。这样,在Kademlia网中,每一个参与者知道的其它参与者信息中,离自己逻辑距离越近的参与者比例越高。由于在搜索的时候也只需要不断得寻找更近的ID,而且每一步都一定会有进展,所以寻找到目标ID所需要的时间上的代价是 O(logn),从这个二叉树的结构来看,我们也可以看到,由于只有部分节点会分裂,所以实质上存储所需要的空间代价也是O(logn)。
实际上CRoutingZone在实现时和理论上的Kademlia有一些区别,如从根节点开始,有一个最低分裂层数,也就是说,如果层数过低的话,是永远允许分裂的,这样它知道的其它地区的联系人信息就能够稍微多一些。
24. eMule源代码学习心得(24):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里面增加的这个特性,就会对那种还没有发出请求就收到回应的情况直接无视,从而避免被愚弄。
25. eMule源代码学习心得(25):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来进行处理了。
26. eMule源代码学习心得(26):emule中的Kademlia搜索任务管理
CSearch 和CSearchManager是完成具体搜索任务的。CSearch对应的是一个具体的搜索任务,它包括了一个搜索任务从发起到结束的全部过程,要注意的是搜索任务并不只是指搜索文件源或者关键词的任务,一次发布任务它也需要创建一个CSearch对象,并且让