P2P设计详解

此文章是我在多年前做P2P时写的,其中有好多不妥之处,后因没有时间所以文章内容一直没有改动纠正,但是这篇文章对于一个开发设计者还是有一定启发的。所以就亮出来让大家看看,让大家见笑了

 

P2P详细设计

作者:王海升

2007.05.06

 

1.       P2P适用范围

本系统是应用因特网的视频播放系统。支持点播和直播。对于不同NAT下的节点交换数据我们将使用与以往不同的UDP穿透技术,从而实现了真正的P2P,使的服务器在负荷量较小的情况下能支持更多的客户端播放。本版中没有考虑SEEK和多服务器的实现,下一版中将对此进行添加和实现

2.       系统实现概述

系统主要由PP服务器和PP客户端构成,无论是服务器还是客户端在网络上都是由TCPUDP两个部分,两个部分各有分工和功能。服务器的只是完成最本的一些连接登录,和协助工作,再就是数据源的提供,其它大部分工作都由客户自己去完成。下我面我们分别说一下服务器和客户端的功能。

2.1     服务器的主要功能

扫描二维码关注公众号,回复: 11815460 查看本文章

l          负责客户端最初的登录连接(TCP

l          负责协助穿透(TCP)。收到来自客户UDP穿透命令后,使用TCP回发通知。

l          负责对超级种子提供媒体数据(TCP)。

l          负责给客户端提供节点列表(TCP)。

节点列表的提供仅是向客户端提供了一块内存区域。在提供时就是把这块区域进行压缩后发给客户端就算完成作务了,在此我们来看一下这个内存是什么样子的。如下:

 

上面是一块连续的内存,在服务器启动时建立,每一块是一个小的内存块,这一个小的内存就存一个结构,结构里就是节点的具体信息。每建立一个节点在开始下载之后就把这个节点信息放进来。直到存满时,就会再向其中增加一块内存来使用,或是重新分配一块更大的内存。

这个大内存块的管理,可以仍然使用MAPMAP中只有已在使用的小内存块,用SESSION指针来作为first的值,这样更容易定位,对于没有使用的块在另一个地方进行管理存储

当有用户请求时,即对这个内存就进行压缩,直接发送。从而避免了进行信息组合带来的计算资源浪费。

l          负责接收来自客户端的数据情况上报,和保持连接。(TCP

l          负责完成穿透的过程,记录客户端的UDP地址。(UDP

l          发送媒体数据时要对数据进行打包。

我们每个数据包大小是固定的,对于文件来说,所以每次从文件中取数据,只要给一个包号就可以根据包号计算文件位置,取出想要的数据。而直接是服务器接收直播数据时就已经打包好了,所以只要有包号就能得想要的数据包了。

l          控制同时服务的超级数。同时大量用户上线可能会一起变为超级种子,虽然最终可能会有部分节点变为普通节点,但是这样仍然会影响服务的性能。为了能减少种情的影响,服务应能控制同时连接到服务器上的数目。(是不是不应准确的把服务节点数定为超级种子数。是不是可以把服务数定为S  = 超级种子规定数 * 规定上传节点数

l          除此之外就是对节目的管理了。

在服务器上对于每个节目都会有一个节点列表,用来个管理这个节目的所有节点。这些节目列表是用MAP来管理的。

 

2.2     客户端的主要功能

客户端TCP的作用就是服务器TCP的交互,完全与服务相同。而UDP的作用显的比较重要,下面一一列出除TCP作用外的功能;

l          负责向服务器通报本志UDP地址。(UDP

l          负责发送接收媒体数据(UDP

l          负责对客户端的探测,包括节点基本信息的获取,节点带宽的探测。(UDP

这个探测是非常重要的,就是因为有了这个探测才让后面的选种成为了可能。具体参考后面的3.2.3

l          负责补包。(UDP

补包可以参考后面的补包处理。

l          负责种子的选取。

下面讲一下选种的过程:

 

客户端有三个列表,如上图,即“节点列表”、“备用种子列表”、“种子列表”;这三个列是从服务拿下来的节点最终成为种子的所必须经历的三个过程:

第一个过程:从服务下来的节点列表经过解压后把所有节点取出放入节点列表,放入的同时把自身和不可用节点过滤掉,第一个过程结束。

第二个过程:是备用种子维护过程,当维护过程发现当前备用列表中节点数没有达到系统规定的最大数,那么就会从节点列表                 中取出足够的节点放入备用列表;如果发现取时或取完时节点列表已经为空 ,那么就向服务器发送节点获取命                                                                      

令,以便补充节点。

第三个过程:当有种子因某种原因被删除时,那么就会到备用节点列表中取相应的种子来补充,如果取不到就会通知服务把自己置为超级种子。

l          负责任务分配。

种子放种子列表时是没有分发任务的,仅是把任务信息放入了节点中,任务分发由一个专过的过程来处理,定时去处理一下,处理的过程就是把节点里面的任务信息取出来发给对方,发送与否的决定在节点里有一个标志,当对方收到任务后会发回一个通知,收到后就把标志重置。就完了任务的分发。 有时对方在收到任务后回发可能不成功,这样为了保证发送方不会重复发送任务,那么当任务发送方收到与务相同的数据包后也会重置这个标置。

l          负责发送码流的调整,即带宽控制。

码流的控制是发送完成的,发送方依靠定时发送探测包,得到对方的接收情况(接收码流),然后根对方的回馈的丢包情况及对方的数据接情来调节自己向对方的发送码流。

l          本地数据的存放。

文件的存放是一个自定义文件加上一个媒体文件,作为两个文件来存在。下面我们来看一下自定义文件的格式,

如上图就自定义文件的格式,自定义文件由文件头和数据区组成,文件头中各个数据就如图所示,下面进行详细的说明,

DWORD   m_dwHeaderSize;  // 文件头大小,用来标识整个头的大小

DWORD    m_dwIndexPackets;       // 索引包数,是指每一个索引号指定了多少个包。

DWORD   m_dwIndexSize;      // 索引区的大小,是指索引的总大小。

DWORD   m_dwMaxBps;       // 最大发送码流,这个通常是指这个文件在发送时以多大的码流发送,现在还没有用,保留备用

DWORD  m_dwTag;     // 数据标志;    这个标志是用来标识文件中的数据是否可用,在文件中每个包的位置也有一个标志与之对应,如果相等那就说明数据在当前是可用的,如果不等那这块数据就是无效的。

  DWORD   m_dwPacketSize;          // 每个数据包的大小,这个值是来说明每个位置存的数据包的大小,在定位与取包时要用

  DWORD   m_dwPacketCount;     // 数据包的个数,这个数值是来说数据包的总数,但现在还没有用,作为备用

BYTE   m_URLBuff[256];       // 文件URL,这个值用来说明当前文件中的可用数据是那一个节目,

BYTE   m_BVersion[21];       // 文件版本,说明这个文件是哪一个版本,不的版本可能里的数据格会不一样

BYTE        m_BIndex[1275];         // 大索引区,是索引大块存放位置。

BYTE    m_BPacketIndex[m_dwIndexPackets*m_dwIndexSize ];              // 数据包索引区,是索引存放位置。

有了这些参数当进行访问时就变的简单多了,只要有一个包号就可以定位到想要的位置,取到想要的数据,如你想取255号包,那就可先定位到 255 * m_dwPacketSize 位置,取出m_dwPacketSize大小的数据,取出包中的数据标志与m_dwTag参数作比较,如果相等就是想要的数据了,如果不等那说明没有想要的数据。

通常来判断是否另一个节点想要的数据时会看一个大索引区索引号是否为1,这样保证了在取数据时不会取完一个包就没有数据而又要重新选种,如某节想看看该节点有没有dwPacketNo这个点处的数据,就会判断索引 n = dwPacketNo / m_dwIndexPackets 处是否为1,如果为1就是有,否则就是没有。

一但开始给一个点发送据那么就是以数据包索引来判断当前要发送的包是否存在。还有在播放时也可以判断在媒体文件中是否拥有想要播放的数据。这个索引区是一个索引号对应一个数据包。

3.       客户端的具体实现

 

3.1     P2P客户端启动过程

 

客户端的启动过程(图1

当客户端向服务器通报地址时,由于使用的是UDP,为了保证通报成功,客户端会启动一个检测过程,每隔2秒检测一次回复,如果没有回复再进行一次通报,直到成功后才关闭这个重发机制。

在根据URL打开文件时,会选检测当前有没打开,如果有人正在下载则不用再次打开,直接取来使用就行了。

3.2     客户端种子管理和选取种过程

客户端种子处理过程分为五个部分:

3.2.1 种子超时和删除处理;

超时处理就是一个过程在不断的轮循检查种子是否超时,如果超时就把这个种子从种子列表转移到删除列表。另一种处理就是收到节点对方发送过来的停止命令,这时也会将此节点从种子列表转移到删除列表。在这里不会再对删除列进行操,其它操作留给补种过程去处理。

3.2.2 补种的过程

这个过程主要完成给种子列表补种,同时要据节点的情况作出反应是否让自己变成超级种子或是普通的节点。

如果当前种子列表中种子数没有达到允许的最大值,就去备用节点取出种子,如果备用列表没有完成排序过程,那也就是取种子不成功,则会直接返回,如果备用列表已经完成了排序就会有如下的情况:

l          取到种子和删除节点中节点数相同,那么就会把删除节点列表中的节点任务一一对应的重新分配给新取到的种子,然后把删除列中节点都删除。

l          取到种子数大于零,但不等于删除节点数。看一下取到的数与原有种子数之和是否够用:

够用补入种子列表,重新分配任务,此时如果是超级种子就上报服务器成为普通节点;不够用,如果此时不是超级种就上报服务器成为超级种子,并清除节点列表。此后都要清空一下删除节点列点列表。

l          取到种子为空,但删除节点不为零。看一下剩余种子是否够用:

够用重新分配任务,清空删除列表。不够用清空种子列表和删除节点列表,并通知原有种子不要再发送数据。

l          取到的种子和删除列表全为空。如果此时不是超级种子就通知服务器成为超级种子。

这样只要有足够的种子,客户端就会通知服务器成为普通节点,不再从服务器取数据。

在此选种后并没有发送任务分配命令,这个发送任务的处理不在这个过程中,这是一个单独的过程。

3.2.3 任务的发送处理

在补节点过程中选出来的种子直接放入到了种子列表,但是并没有进行发送任务分配命令,那在这个过程中主要是处理任务分配命令的发送,在补种的过程中放入种子时,会设置任务信息,同时也设了一个标志,这个标志就是为了告诉这个过程这个节点需要发送任务分配的命令,所以这个过程会每隔几秒就检测一次这个标志,如果这里为TRUE,那就发送任务分配命令,当收这个任务分配命令的回复时就会重置这个标志,如果没有收到就会一直在发送。有时对方在收到任务后回发可能不成功,这样为了保证发送方不会重复发送任务,那么当任务发送方收到与务相同的数据包后也会重置这个标置。当然如果对方一直没有反应那这个节点也就会超时了,那就会有新的种子补进来。

3.2.4 备用种子维护

这个维护过程主要是为了保证备用节点列表中有足够可用的节点。所以就有四个功能,即获得对方的具体信息(包括数据情况、连接数),利用得到的信息和回馈的时间进行排序,保证备用列表中有足够可能的节点,还有一个就是如何从超级种变为非超种的过程。

         首先检测备用列表中是否达到规定数,如果不够节点不够就从节点列中取出相应数补到备用节点列表。然后对每个备用节点发送节点探测包,一次发送5个包。在发送的信息探测包中带有当前比发送最快节点的包号,这是对方用来判断是否有此节点所需数据的,同时对方回馈一个包,包中带有对方当前是否包含此数据的信息,还有对方当前的连接数。当接收到这些包的回馈时如果对方连接数已经达到最大,或是没有数据,那么就会将这个节点直接从备用节点中删去,否则就把这些信息记录在备用节点中,也记录回馈信息的个数。

估计发送3秒后回馈已经接收完毕就对备用节点进行排序。排序先按每个节点的回馈数进行由大到小的排序,然后再对回馈数相同的节点以回馈时间进行由小到大进行排序。由此完成一个维护过程。

如果给备用节点时节点列表中没有足够的节点,就要从服务器发送请求,请求服务器发送节点列表。

为了让补种过程在取备用种子时不会取到未经探测和排序的节点,我们在每个操作完成后才进入下一个过程。过程如下:给备用节点补入节点时置为状态NODE_NEW, ==>> 当然状态为NODE_NEW时同时发送了探测包后再置状态为NODE_SEND_QOS,==>> 下一步当状态为NODE_SEND_QOS且经排序后再将状态置为NODE_MAY_SORT。 ==>> 这样补种过程可以在NODE_MAY_SORT状态时,对节点进行排序,并进行取种。以保证了取种的正确性。

同时还有一个超时维护过程,如果有长时间没有回馈的节点就视为不可能节点,这样的节点就会记录下来,作为在给备用列表补节点时过滤之用,如果有两倍节点超时时也没有收到一个回馈,那么就把这个客户端视为无法穿透的节点。就是直接向服务器发送命让自己成为一个特殊的超级节点,同时停止自己的一切探测过程,直接从服务器取数据。

3.2.5 任务调整

当有多个节点提供数据时往往因为各个节点的速度不一样造成最快和最慢节点相差太大,这样就会使得有大量的数据滞留置在内存中不能排序写入文件,因此当最慢节点与最快节点之间的距离过大时就会把最慢节点的任务发送点向前提,提至与当前最快节点的发送点一致,这样前面的数据就会通过补包的方式快速补全。避免造成播放时数据不够的现象。这个过程的实现就是重新分配任务的过程,依然任务发送过程去完成最终的发送。

3.3     发送节点

发送节点的任务主要是根据任务提供数据,但由于我们用的是UDP,所以它并不知道自己发送码流是多少会是合适,因此就有一个探测的过程来得知当前的丢包情况以调整发送码流。这个过程可参考第7部分。

发送节点的创建是收到任务分配命令时来创建的。创建后放入发送列表进行管理,。当对方在删除种子时会通过UDP发送一个删除节点的命令过来,如果收到就删除该发送点,若没有收到就依靠超时去删掉节点。为了不让发送点超时,在接收方由一个时钟定时向发送方保持连接的命令包,以此保持连接。

 

3.4     补包的处理

这里仅是使用已选节点去补包的过程。

对于补包是每三秒补一次,每次在从当前没有丢包的位置开始到接收最慢的一个节点接收的包号之间取N*80个包,N是可以用来补包的节点数,它的取得满足能者多劳的原则,是在当前种子中发送较快的节点。是与发送最快的节点相差不大于300的节点。

在下载过程中通常节点可能快慢相差很大,所以随时的加长,可能有些节点的接收包号会与最快大的节点的包号相差过大,以致于响影到了补包和播放,因此可加入任务调整,当最慢点与最快点相差超过系统允许差值时(1000)就将这个节点的任务发送点向前提至系统规定值(500),这样中间缺少的包由补包来完成,也就是说由发送较快的点来帮助完成了这部分任务。

 

3.5     不可用节点

不可用节点有两种,一种是某一个节点只对某个节点是无法穿透的,那这两个节点之间是不能互为种子的,而另一种就是某一个节点无法接收来自任一节点的数据,也就是说此点对于它人来说是无法穿透的。

单一不能穿透的节点的判断方法: 如果一个备用种子在该节点超时也没有收到任何来远程的UDP数据那么就视为不可用节点。记入不可用节点列表。(原本不想记录,只想通过探测来去掉,如果记在本地,那么如何管理,会不会列表越来大。只记录ID,也大不哪里去,大就让他大去吧)

全都无法穿透节点的判断方法:   如果能收到来自己任一个节点的远程UDP数据则认为是可用的节点,便关闭这一检测机制。如果客户端在使用两倍系统规定超级种子数进行探测后,在超时时都没有收到任一个来自远程的UDP数据,那么就视为是无法穿透的节点。便上报服务器使自己成为一个特殊节点,直接从服务器取数据。同时也关闭本地的一切探测机制。

 

3.6     带宽探测与码流自动调整

如果当前带宽已经使用完毕,可以是UDP数据依然狂发,这时丢包率就会大增,如果补包,补包命令很可就就会中途丢失,所以必须要对发送码流进行控制。因为要加入一种带宽探测的机制。根丢包率进行发送量的递减和递增。如下图:

 

码流控制(图3

由发送方每隔5毫秒连续发5个探测包,接收方在回馈时把此时从发送点的接码流一起进行回馈,发送方根据收到回复的情况,调节发送码流,以保持良好的带宽。每20秒进行一次这样的探测,这样从而保证了小的丢包率和补包的成功率。在图上最后得出的预加值与真正SleepTime相加,所得值就是在发送时该节点的真正休息时间,注如果相加时间小于了35,那么就等于35,因为不允许发送太快,所以最小就是35了。

 

3.7     任务的分配方法

对于任务分配,我们使用的是给一个起始点按规律一直向下发。这样能避免不断频繁分配任而造成的效率低下。

首先我们设起始包号为S

设当前种子数为N

设从0到(N - 1 )之间分别的每一个值为X,包括0 N – 1

每个节点的任务起始点包号为T

则有  T = S + X

则每个节点任务就是在发送时只发送从T开始,每次增加一个N值后的每个包

 

3.8     节点下线

 

客户端退出(图4

如上图在这里我们可以看到,只要客户端退出一方面就会利用UDP通知该客户端的种子,告诉对方把自己从发送节点删除,节省带宽,如果没有收到那就通过超时去删除了,这样可能会慢一些。另一方面也要通知以此为种子的节点,即每一个发送点,让其把自己删掉重新启动一个补种的过程,这个补种的过程,其实和种子超时后补种过程是一样的。

4.       服务器和超种的部署

为了保证超级种子的可用性,和避开UDP上传时对TCP下载的影响,我们将把超种子放于公网之上。这样超种在下发时的UDP数据将不会影响到TCP的数据下载,同时我们还可把文件直接考入到超种的指定位置,这样也可不用再去从服务器上下载节目。

公网超级种子的最小数为每个节点的最小下载种子数,公网超级种可以可以连接多个节目。并且公网超种会比普通节点的发送服务节点数要大,发送量也要大,普通节点的发送量可以是自适应的,但发送节点数不能过大,这也是为了保证在服务时对于每一个节点的发送码不至于过小。

5.       性能指标估计

4.1 从超级种子的角度估计服务器承载量

我们选种的条件是两个节点之间至少要大于一个索引,因此在理想的情况下每个节点与其最近的一个种子相差的包数为一个索引所包含的包的个数I500,可以根据实际情况进调整),由于我们节点信息在选种时得到的有些滞后,所以在此我们以每相邻两个节点之间相差数为 1.2 *  500 , 每个包的大小为P(1.4KB),由此我们可以得到每两个相邻节点之间相差的数据量为:

B = I * 1.2  *  P  =  500 *1.2 * 1.4KB  = 840KB

如果一个大小为S(对于直播来说就是缓存大小,300M)的节目。在不考虑无法穿透的情况那么可能会有节点层数:

L = S * 1024 / B  =  300 * 1024 /  840KB  = 366

为保证带宽不会被堵,且能播放的更加流畅,所以设计会尽量使其下载与上传的流量相等,也就是说,一个节点数据的提供最多只能满足一个节点的需求。设我们定超级种子的数目为N 为 4,那么此时能带动节点的总数T就会有如下计算结果:

T = N * L =  4 * 366 = 1463

这是保守估计,是下载码流和流放码流相同的情况,如果播放码小,那么文件下载完毕就会提前就会有更多的种子为新来的节点提供数据,那就能支持更多的人。

如果我们以6个超级种子为开始,每个节点可以从D6)个节点开始取数据,为U9)个节点提供数据,也就是说上传大于下载是下载的U/D (1.5)倍,那么其它参数依为上面所示   那么所能带动的节点数为级数 T   =    D = 6 * ;此时我们可以看的出来提高节点的上传量就能带动更多的节点,如果提高超级种子数(N > DT = N/D * D = N ;

当超级种子数减少时其它不变(N< DT = D - ( D - N ;好像对总数影响不大,但一开始节点可能就因为得到的数据量不足,所以到后面他也没有足够的数据提供,那么播放就得不到足够的数据。因此超级种子的数目应该和下载节点的数目尽量一致,不能低于下载节点数。

 

4.2 从带宽角度估服务器的承载量

估计条件:

在此考虑的是服务器配置本身能够承受的条件下,;

通常服务器每个节目下载在400K左右,在此设为430K的码流;

设命令占用带宽4K

一个节目通常有4个种子。不能穿透的机率大概占总节点数的确20%

设一个节目会有400人在线。

计算结果数据:

那么服务器单个节目的负载为  430K * 4 + 400 *20% =  35.27M

命令点用带宽为 4 * 400 = 1.6M

如果服务器带宽为1000M,利用率为80%,那么此时能承载此条件下的的节目数为 1000 * 80%  / ( 35.27 + 1.6 )  = 21.7

此时在线用户数为 22 * 400 = 8679 

 

如果考虑每个节目的在线人数不同会有如下结果

不变条件:

通常服务器每个节目下载在400K左右,在此设为430K的码流;

一个节目通常有4个种子。不能穿透的机率大概点总节点数的确20%

                   如果是在1000M带宽下,利用率为80%,不用P2P用户可达1888

在线人数/节目

能承载节目数

用户数

500

17.5

8770

400

21.7

8679

300

28.5

8556

200

41.5

8307

100

76.4

7640

50

131.7

6589

10

312.5

3125

                如果是在500M带宽下,利用率为80%,不用P2P用户可达944

在线人数/节目

能承载节目数

用户数

500

8.8

4385

400

10.8

4340

300

14.3

4278

200

20.7

4018

100

38

3820

50

66

3295

10

156

1563

                   如果是在100M带宽下,利用率为80%,不用P2P用户可达189

在线人数/节目

能承载节目数

用户数

500

1.8

877

400

2.2

868

300

2.9

856

200

4.2

831

100

7.6

764

50

13.2

659

10

31.3

313

 

猜你喜欢

转载自blog.csdn.net/wanghaisheng/article/details/5819529
P2P