(from:http://hi.baidu.com/elseif/blog/item/d94998f9d977e258252df2c1.html)
1,分块机制——正确传输资源的保证
继续解析CKnowFile类
为了加快内容分发的速度,分块处理是一种简单有效的方法。emule中对每个文件都进行了分块处理。另外分块还有一个好处就是如果保留了每一分块的hash值,就能在只下载到文件的一部分时判断出下载内容的有效性。
emule在获取每个共享文件的信息时,就对它进行了分块处理,因此如果要知道emule中的分块处理和恢复机制,看CKnownFile::CreateFromFile函数的实现就行了。这个函数中牵涉到的和分块处理以及hash计算相关的类都在SHAHashSet.cpp和SHAHashSet.h中。
(注意CKnowFile里的两个成员变量:CAICHHashSet *m_pAICHHashSet 和 CArray<uchar*, uchar*> 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的要求。