转自中国源码网
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
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的工程的路径的相对位置,因此需要把那些解压过的库的目录移到一些合适的地方,有时还需要给这些目录改个名称。
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中很多其它很有用的基础设施。
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保存和读取它的列表中的所有文件的信息。