如何看emule下载的源码

eMule的官方主页上写着:2002年5月13日,一个叫Merkur的人对最初的eDonkey2000客户端不满意,坚信自己可以做得更好,于是开始制作。他把其他开发者聚集在身边,eMule项目就这样诞生了。

EMule是一个典型的MFC程序,其图形界面已经与MFC紧密结合。所以一般情况下,只能在windows平台下运行。其他一些项目如aMule移植了它,所以跨平台功能更强。

其实还有一个项目叫xMule,不过现在人气已经不行了。在aMule的主页上,可以看到一些eMule移植到linux平台的历史。最早有个项目叫lMule,用wxwidgets进行eMule的跨平台移植。这个项目在2003年没有更新,后来转型为xMule项目,一度成为linux平台下eMule事实上的替代品。但由于理念不同,他们的程序员发生了冲突,导致了阿木勒的分裂。后来矛盾严重,一度从观念问题上升到对对方的人身攻击,一度在对方网站上发起DDos。后来aMule和xMule是两个完全不同的项目。现在只有HopeSeekr在维护xMule,基本没有更新。这不仅令人印象深刻。今年寒假,我和HopeSeekr有过一些交流,感觉他很自信。他经常给我看一些aMule的代码,说你看看他们的代码,这样写,就是一段xx而已。这种代码在某些情况下肯定会崩溃。相反,如果你看看我们的xMule代码,绝对不会有这样的问题。

EMule从0.42版本开始支持Kad技术,这是一个非常重要的里程碑。Kad是一种DHT协议,它使得节点之间能够相互保留其他节点的一些联系信息,并利用这样一个“关系网络”在没有任何中心服务器的情况下找到整个网络中的任何节点和资源。所以,就像当初用napster直接移除中心服务器,穿越napster网络那样处理eMule的Kad网络是没有用的。0.42版本于2004年2月27日发布,比eDonkey2000的内部网晚了近一年,但其Kad网络的规模正在迅速扩大。内网和eMule中的Kad都使用Kademlia结构,但是具体的消息格式不同,所以两个DHT网络不兼容。到现在,内网的用户数一直保持在10万左右,这是最近一篇文章里写的。但是关于电驴的Kad网络规模,目前还没有一个准确的说法。我们也可以看到开源软件的力量。目前aMule的Kad网络兼容eMule的Kad网络,xMule没有Kad支持。Kad协议的原文可以从我们实验室的机器上下载:

:8080/paper/new/by % 20 conference/IPTPS/IPTPS 02/Kademlia % 20A % 20对等% 20信息% 20系统% 20基于% 20 on % 20 then % 20 or % 20 metric % 28 IPTPS 02% 29 . pdf

EMule的代码结构非常合理。虽然代码量比较大,但是功能模块之间的划分还是比较合理的。这一点从它的工程文档就能看出来。EMule将代表功能的代码文件与代表接口的代码文件分开。源文件和头文件是实现功能的代码的源文件和头文件,而界面源文件和界面头文件是实现图形界面的源文件和头文件。由于eMule的代码太大,本系列将跳过图形界面的实现,重点分析eMule的功能实现部分的代码,功能实现的主要部分也选了出来。本节将从对eMule.cpp的分析入手,引出eMule中使用的几个主要功能类,并大致描述它们的功能。最后介绍了如何在VS2003中编译eMule。emule里还有很多模块非常有用。我说的是在其他程序中很有用,可以考虑在其他程序中重用。

Emule.cpp是CemuleApp的实现。因此,在运行时,将首先运行InitInstance来做一些初始化工作。从这个函数中,我们还可以第一次看到将在整个程序中发挥作用的类。

一开始是计算一些程序常用的目录,比如配置文件,日志文件。接下来是ProcessCommandline,它有两个函数。首先是确认eMule的运行模式,即命令行后面是否有参数,其次是确认目前是否只有一个eMule实例在运行。正常情况下,双击一个eMule可执行文件不需要参数。但是通过点击链接或者打开关联文件来打开eMule,相当于带参数运行eMule。通过在注册表中添加一些项目,一个程序可以与一个具有特定链接或后缀的文件相关联。具体请参考ProcessCommandline中OtherFunctions.cpp中Ask4RegFix、BackupReg、RevertReg的函数,可以通过创建带有名称的互斥信号量来确认是否有其他eMule实例在运行。对于给定的名称,CreateMutex只能创建一个互斥体。因此,我们可以通过信号量是否成功创建来了解是否有其他eMule实例正在运行。如果有,而且是带参数的模式,那么可以利用Windows的消息机制直接把这个参数发送到那个窗口。接下来的代码无非就是怎么找到另一个叫“电驴”的家伙,发什么消息。PstrPendingLink是一个全局变量,表示要处理的命令行参数。它将在初始化完成后进行处理。

下面两个更重要的类是CPreferences和CStatistics。前者保存程序的大部分配置数据,后者进行各种统计。它们的特点是成员变量多,而且是静态的,可以保证它们的唯一性,将这些变量统一到一个类管理中。但是你真的不需要知道每个变量的含义。Prefs和Stats是这两个类的仅有的实例。

在处理了一些其他的事情之后,包括创建一个图形界面对象CemuleDlg,你可以看到一行又一行的语句来创建新的对象。这些类将实现eMule程序运行的主要功能,后续系列将详细分析。

最后介绍了如何在VS2003中编译eMule。因为eMule中使用了一些其他的库,如果官方提供eMule的源代码下载,那么源代码包会变得非常大。所以eMule选择让开发者去那些库的官网下载。一般来说,在编译这类工程文件时,保持各个库和主程序编译参数的一致性是非常重要的。在这些编译参数中,主要有三个参数,分别是字符集(多字节/Unicode)、调试/发布、单线程/多线程,这样排列组合后就有八个版本。所以编译时如果不注意,就会出现与程序设计无关的错误。

EMule0.47a解压后自带id3lib库,需要下载以下库:

zlib:

/~戴维/cryptlib.html

pnglib:

http://www.libpng.org/pub/png/libpng.html

下载它们,解压它们,然后编译它们。编译时注意与eMule的工程参数保持一致:字符集为Unicode,支持多线程安全。可根据需要选择调试版或发布版,但也要与eMule的工程参数一致。VC工程文件通常可以在这些库的解压包中找到,只是版本较低,直接转换就可以了。另外,建议在编译这些库的时候,选择生成静态库而不是动态库,这样最终生成的可执行文件就可以自己运行了。编译完这些库,包括从其他地方下载的,emule提供的id3lib库和CxImage库,就可以开始编译eMule了。编译emule无非是注意让它在编译时找到所有头文件,在链接时找到所有库。链接时,修改项目文件中的属性即可找到所有库-->;配置属性-& gt;链接器->;输入->;附加要完成的依赖项。但是找到所有的头文件需要一些技巧。因为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位的整数,也就是读取四个字节到一个整数值的地址,类似于读取或写入其他类型的数据。但是这个类把所有数据操作的物理方法都声明为纯虚函数,也就是读写多少字节。有了这样一个基类,重载它就很方便了。通过将这些纯虚函数定义为对某个内存的读写操作,可以方便地将更复杂的数据序列化到一个连续的内存中。如果这些纯虚函数是对文件读写的操作,自然可以用来读写各种格式奇怪的自定义配置文件。

这些类要读取的数据对象通常包括各种整数、字符串和标记类型。整数读写比较简单,1字节,2字节到4,8字节或者16字节的数据读写方式都差不多。这里要提一下16字节,也就是128位,这是eMule中Kad网络随机生成的ID的长度,也是eMule中常用的MD4哈希算法生成的结果的长度。这样的ID通常用于直接访问整个。在kademlia\utils的目录下,UInt128.cpp实现了一个表示128位整数的类,该类具有完善的功能,可以进行一些算术运算,可以进行比较,为以后它作为一个键出现在哈希表中奠定了基础。仔细研究UInt128.cpp中的代码实现,可以学到很多编写这种自定义数据对象类型时应该注意的问题。

数据操作中另一个很重要的操作是字符串。总的原则是先写个长度,再写内容。但在具体操作时,需要注意这些细节,比如长度是4字节还是2字节,字符串的内容是否要用UTF-8编码。这些操作需要与StringConversion.cpp紧密配合,其实后者的很多字符串转换函数也调用ATL相关的函数,只是外面包了一层MFC CString。

在kademlia\io\DataIO.cpp中实现的CDataIO中,也实现了根据Tag读写的功能。在互联网上交换文件的元信息是非常重要的。通常一个文件的元信息可以分解成很多标签,比如“文件名=xxx”、“文件长度=xxx”等等。换句话说,标签代表一个属性等于某个值的事实。文件Opcodes.h中定义了很多代码,其中有很多常见的标签属性名。CDataIO类中存储的Tag的属性名是先存储一个字节,再存储名称,最后根据类型存储值的类型。

eMule中的这些基础设施写得很好,可以很容易地取出来重用。像字符串编码、文件IO操作等具有一定数据结构的处理,在很多地方都会非常有用。这些类在eMule中的实现基本都是复制到其他工程文件中,稍加修改就可以很快使用。在未来,我们还将在eMule中看到许多其他非常有用的基础设施。

Emule作为一个文件分享的程序,首先必须非常清楚它所享受的所有文件的信息。这是CKnownFileList类的作用,它是随着cmuleapp类的创建而在emule.cpp中创建的。

CKnownFileList类使用MFC的CMap类来维护内部哈希表,这也说明了emule和MFC的关系确实很密切。这里实际上可以使用STL贴图。它在内部维护一个已知文件列表和一个已取消文件列表。这些哈希表的关键字都是文件的哈希值。这样就可以判断出文件名不同但内容相同的文件,但是要让不同内容的文件有相同的哈希值是非常困难的,这也是哈希函数设计的初衷。因此,除非我们遇到像王小云教授这样了不起的人,否则我们基本上可以认为两个文件的哈希值相同意味着它们的内容相同。再看CKnownFileList.cpp,这个文件其实并不长,因为管理一个列表真的不需要很多种操作,如果每个具体文件都有一个强大的类来处理的话。这里有一个,它是CKnownFile。有了这样一个类,我们可以看到CKnownFileList类需要做的就是根据一些信息找到对应的CKnownFile类,将其他列表中的信息复制过来,将这些信息全部保存为文件,然后在下次emule运行时快速恢复这些信息。最重要的是能够在不造成内存泄露的情况下完成上述工作。

CKnownFile类是一个特别关注特定文件的信息的类,它还有它的基类CAbstractFile。但它与CAbstractFile类的主要区别在于,CAbstractFile类只有基本的信息访问功能,cknowFile可以主动生成这些信息,比如给cknowFile一个文件的路径,它可以主动获取与这个文件相关的所有信息并保存在自己的成员变量(CreateFromFile)中。CKnownFile.cpp文件看起来更长,因为它做了很多工作。在当前版本的emule中,除了对一个文件进行全文哈希之外,还使用BT对块进行哈希,这样在传输文件时,即使出现错误,也不需要重新传输整个文件,只需要传输有错误的块。这种机制被称为高级智能损坏处理(AICH,高级智能损坏处理),这种机制将在后面继续分析。

CKnownFile将读取文件的所有信息保存为标签。它将试图在其操作中获得尽可能多的文件信息。例如,对于媒体类型的文件,它可以调用id3lib库来获取标签信息,如作者、发布年份和记录风格。如果是视频媒体文件,也会抓取一张图片(函数实现:CFrameGrabThread)。

CKNowFile还可以随时了解文件当前的下载情况(里面有CUpDownClient的列表),当然也会根据需要对自己进行序列化和反序列化。LoadFromFile和WriteToFile都以CFileDataIO为参数,方便CKnownFileList保存和读取其列表中所有文件的信息。