ESFramework 开发手册(04) -- 可靠的P2P

      本文介绍ESFramework 开发手册(00) -- 概述一文中提到的四大武器中的最后一个:P2P通道。

      ESPlus 2.0版本相对于1.x而言,新增的最主要特性就是对P2P的支持。

      ESPlus 2.0提供了基于TCP和UDP的P2P通道(不仅支持局域网,还支持广域网的P2P通信),而无论我们是使用基于TCP的P2P通道,还是使用基于UDP的P2P通道,ESPlus保证所有的P2P通信都是可靠的。这是因为ESPlus在原始UDP的基础上模拟TCP的机制进行了再次封装,以使UDP像TCP一样可靠。在客户端之间需要高频通信的分布式系统中(如IM系统等),可靠的P2P通信将为您节省巨大的带宽和服务器成本。

1.P2P打洞

  了解P2P的朋友都知道,P2P Channel的建立需要通过“打洞”来完成,而运行于两个NAT设备后面的PC上的客户端实例之间的P2P打洞能否成功,或者说,P2P通道能否成功建立,取决于NAT设备的类型。

UDP打洞

  就目前我们常用的路由器、防火墙等NAT设备来说,大多都是Cone型(Full Cone、Restricted Cone、Port Restricted Cone 之一)的,所以UDP打洞的成功率还是非常大的(如80%以上)。
  基于UDP的P2P打洞成功率与NAT设备的类型的关系一览表如下所示:
     
  从列表可以看出,最麻烦的是Symmetric类型,如果需要P2P通信的双方有一方是Symmetric,那么打洞就需要用到端口预测技术,而且其打洞成功希望非常渺茫。
  大家可以通过从网上下载NAT类型的检测程序(比如STUN)来检测自己路由器等NAT设备的类型,以此来确定通信的双方基于UDP的P2P通道是否可以创建成功。 

TCP打洞

      TCP打洞的原理几乎与UDP是一样的,但不幸的是,目前支持TCP打洞的NAT设备非常少,以至于位于两个NAT后面的客户端实例之间能成功建立基于TCP的P2P连接的机会就很小了。我们希望在不久的将来,支持TCP打洞的NAT设备会逐渐多起来,这需要时间。关于基于TCP的P2P的更多介绍可以参见这里
  即使如此,ESFramework支持基于TCP的P2P还是非常必要的,因为在以下两种情况下,基于TCP的P2P通道肯定是可以成功创建的。

  • 通信的双方位于同一个局网内。
  • 通信的双方中至少有一方运行于具有公网IP的机器上。

      在这两种情况下,都不需要TCP打洞,也不需要NAT设备的额外支持,基于TCP的P2P通道就可以成功建立。

2.P2P通道的可靠性

   由于我们的P2P通道可能是基于TCP的、也可能是基于UDP的,所以P2P通道就继承了协议的特性:基于TCP的P2P通道是可靠的、而基于UDP的P2P通道是不可靠的。

      值得庆幸的是,ESFramework/ESPlus内部使用了增强的UDP -- 在UDP的基础上模拟TCP机制,以保证通信的可靠性。所以,ESPlus提供的P2P通道都是可靠的。

      同时,ESPlus也提供了选择,可以使用原始的UDP,只要设置IRapidPassiveEngine的ReliableUdpEnabled属性为false即可。如此,ESPlus基于UDP创建的P2P通道就是不可靠的。

3.通道选择

      在介绍CustomizeInfo空间时,我们提到可以使用ICustomizeOutter发送一个P2P消息给另外一个在线的用户,该接口的如下几个方法发送的都是P2P消息:

   public interface ICustomizeOutter :IOutter
    {            
        void Send(string targetUserID, int informationType, byte[] info);

     void SendCertainly(string targetUserID, int informationType, byte[] info);     

        
byte[] Query(string targetUserID, int informationType, byte[] info);

        
/// <summary>
        
/// 通过P2P通道(即使是不可靠的)向在线用户targetUserID发送信息。
        
/// </summary>
        
/// <param name="targetUserID">接收消息的目标用户ID</param>
        
/// <param name="informationType">自定义信息类型</param>
        
/// <param name="info">信息</param>  
        
/// <param name="actionType">当P2P通道又不存在时,采取的操作</param>
        void SendByP2PChannel(string targetUserID, int informationType, byte[] info ,ActionTypeOnNoP2PChannel actionType);       
    }

      除SendByP2PChannel方法外,其它的方法都将使用可靠的通道来发送P2P消息 -- 如果存在可靠的P2P通道(比如,基于TCP的P2P通道或基于增强UDP的P2P通道),则使用P2P通道发送,否则仍然通过服务器中转。  

      SendByP2PChannel方法是当与目标用户之间有P2P通道存在时,即使其是不可靠的(比如普通的基于UDP的P2P通道),也一定采用P2P通道发送消息。所以,调用SendByP2PChannel方法发送目标消息,意味着,目标消息是可以被丢弃的。如果与目标用户之间的P2P通道不存在,那么将采取的操作取决于该方法的第4个参数ActionTypeOnNoP2PChannel的值,可以是通过服务器中转、也可以是丢弃消息。
  ActionTypeOnNoP2PChannel定义如下:

   public enum ActionTypeOnNoP2PChannel        
    {
        /// <summary>
        
/// 通过服务器中转
        
/// </summary>
        TransferByServer = 0,

        
/// <summary>
        
/// 丢弃消息
        
/// </summary>
        Discard
    }
      两个客户端之间有可能同时存在两个P2P通道:一个是TCP的,一个是UDP的。在这种情况下,ESFramework将优先使用基于TCP的P2P通道。

      还记得前面我们介绍的ICustomizeOutter接口的TransferByServer方法,它的意思是,即使有可靠的P2P通道存在,信息也一定要通过服务器中转。

  顺便说一下,当我们采用前面介绍的IFileOutter来发送文件给其他在线用户时,ESFramework底层采用的一定是可靠的通道,以避免文件包丢失或其顺序发生错乱。

4.P2P通道控制器 IP2PController 

  ESPlus为客户端提供了ESPlus.Application.P2PSession.Passive. IP2PController接口以控制和管理P2P通道。

  通过RapidPassiveEngine暴露了P2PController属性,我们可以获取IP2PController的引用。

  IP2PController接口的定义如下:

    public interface IP2PController

{
///<summary>
/// 当尝试建立P2P连接失败时,触发此事件。参数为对方的UserID。
///</summary>
event CbGeneric<string> P2PConnectFailed;


///<summary>
/// 当某个P2P Channel创建成功时,触发此事件。
///</summary>
event CbGeneric<P2PChannelState> P2PChannelOpened;


///<summary>
/// 当某个P2P Channel关闭时,触发此事件。参数为对方的UserID。
///</summary>
event CbGeneric<P2PChannelState> P2PChannelClosed;


///<summary>
/// 当使用可靠UDP的P2P通道时,是否开启PMTU自动发现。默认状态为关闭。
///</summary>
bool PMTUDiscoveryEnabled { get; set; }


///<summary>
/// 采用的P2P通道的类型。默认为TcpAndUdp,表示TCP打洞和UDP打洞都进行尝试。
///</summary>
P2PChannelMode P2PChannelMode { get; set; }


///<summary>
/// 尝试与目标用户建立P2P Channel。(异步方式。)
///</summary>
///<param name="destUserID">目标用户的UserID</param>
void P2PConnectAsyn(string destUserID);


///<summary>
/// 与目标用户之间是否存在P2P通道。
///</summary>
bool IsP2PChannelExist(string destUserID);


///<summary>
/// P2P通道是否繁忙。如果返回null,表示没有P2P通道,或者不了解P2P通道的繁忙状态(当tcp通道接入时或使用未增强的UDP通道)。
///</summary>
bool? P2PChannelIsBusy(string destUserID);


///<summary>
/// 获取所有P2P通道的状态。
///</summary>
Dictionary<string, P2PChannelState> GetP2PChannelState();


///<summary>
/// 获取目标用户的P2P通道的状态。
///</summary>
P2PChannelState GetP2PChannelState(string destUserID);


}

///<summary>
/// P2P通道模型。
///</summary>
public enum P2PChannelMode

{
TcpAndUdp = 0,
Tcp,
Udp
}

      首先,我们可以通过设置P2PChannelMode属性,来要求ESPlus在尝试创建P2P通道时是使用UDP还是TCP,或者都进行尝试。

      当我们要与某个其他在线用户P2P会话之前,可以先调用IP2PController的P2PConnectAsyn方法,该方法将会在后台线程中尝试与目标用户建立P2P连接(即进行UDP打洞和TCP打洞)。当P2P连接建立成功时,会触发P2PChannelOpened事件,而接下来后续的P2P消息就可以通过P2P通道发送。如果P2P连接建立失败,则将触发P2PConnectFailed事件。

  每一个P2P通道在内存中都对应着一个P2PChannelState实例,该实例记录着P2P通道的相关信息和实时状态,比如:P2P会话对方的UserID和地址信息,P2P通道的协议类型、通道的创建时间、通过该通道发送的消息个数、以及发送的最后一个消息的时间、当前通道是否可靠等。P2PChannelState的类图如下:

  
  当已经建立的P2P通道关闭时(可能是因为对方下线、或者P2P连接中断、或者UDP的P2P心跳超时),IP2PController将触发P2PChannelClosed的事件。

  任何时候,我们都可以通过IP2PController的IsP2PChannelExist方法查询与目标用户之间是否存在P2P通道。我们还可以通过其GetP2PChannelState方法来获取所有的或某个特定的P2P通道的实时状态。 

5.四大武器小结 

      到现在为止,四大武器就全部介绍完毕了,现在我们以表格的形式,将前面介绍的四大武器涉及的API和回调接口整理一下。 

      
      API是框架已经为我们实现好了的,像所有以Outter和Controller结尾的组件,我们可以直接拿来使用。而所有以Handler结尾的接口,是我们要在程序中根据项目的具体需求来实现的,将这些实现的类注入到框架中,供框架回调(通过客户端和服务端Rapid引擎的Initialize方法的对应参数传入) - - 如此,框架就可以带动整个业务流程运转起来了。

      最后,大家可以查看P2P的demo的源码(ESFramework Demo -- P2P通信Demo(附源码)),并运行demo,来尝试一下P2P。谢谢。

 

阅读 更多ESFramework开发手册系列文章

----------------------------------------------------------------------------------------------------------------------------------------------- 

 下载免费版本的ESFramework 以及 demo源码 

 

关于ESFramework的任何问题,欢迎联系我们:

电话:027-87638960

Q Q:372841921

 

posted on 2011-10-10 08:45  傲瑞中国  阅读(2065)  评论(0编辑  收藏  举报

导航