ESFramework 开发手册(16) -- 可靠的P2P
本文介绍ESFramework 开发手册(00) -- 概述一文中提到的四大武器中的最后一个:P2P通道。
ESPlus 提供了基于TCP和UDP的P2P通道(不仅支持局域网,还支持广域网的P2P通信),而无论我们是使用基于TCP的P2P通道,还是使用基于UDP的P2P通道,ESPlus保证所有的P2P通信都是可靠的。这是因为ESPlus在原始UDP的基础上模拟TCP的机制进行了再次封装,以使UDP像TCP一样可靠。在客户端之间需要高频通信的分布式系统中(如IM系统等),可靠的P2P通信将为您节省巨大的带宽和服务器成本。
一. P2P 打洞
了解P2P的朋友都知道,P2P Channel的建立需要通过“打洞”来完成,而运行于两个NAT设备后面的PC上的客户端实例之间的P2P打洞能否成功,或者说,P2P通道能否成功建立,取决于NAT设备的类型。
1. UDP打洞
就目前我们常用的路由器、防火墙等NAT设备来说,大多都是Cone型(Full Cone、Restricted Cone、Port Restricted Cone 之一)的,所以UDP打洞的成功率还是非常大的(70%以上)。
基于UDP的P2P打洞成功率与NAT设备的类型的关系一览表如下所示:
从列表可以看出,最麻烦的是Symmetric类型,如果需要P2P通信的双方有一方是Symmetric,那么打洞就需要用到端口预测技术,而且其打洞成功的希望非常渺茫。
大家可以通过从网上下载NAT类型的检测程序(比如STUN)来检测自己路由器等NAT设备的类型,以此来确定通信的双方基于UDP的P2P通道是否可以创建成功。
2. TCP打洞
TCP打洞的原理几乎与UDP是一样的,但不幸的是,目前支持TCP打洞的NAT设备非常少,以至于位于两个NAT后面的客户端实例之间能成功建立基于TCP的P2P连接的机会就很小了。希望在不久的将来,支持TCP打洞的NAT设备会逐渐多起来,这需要时间。关于基于TCP的P2P的更多介绍可以参见这里。
即使如此,ESFramework支持基于TCP的P2P还是非常必要的,因为在以下两种情况下,基于TCP的P2P通道肯定是可以成功创建的。
(1)通信的双方位于同一个局网内。
(2)通信的双方中至少有一方运行于具有公网IP的机器上。
在这两种情况下,都不需要TCP打洞,也不需要NAT设备的额外支持,基于TCP的P2P通道就可以成功建立。
二. P2P 通道的可靠性
由于我们的P2P通道可能是基于TCP的、也可能是基于UDP的,所以P2P通道就继承了协议的特性:基于TCP的P2P通道是可靠的、而基于UDP的P2P通道是不可靠的。
值得庆幸的是,ESFramework/ESPlus内部使用了增强的UDP -- 在UDP的基础上模拟TCP机制,以保证通信的可靠性。所以,ESPlus提供的P2P通道都是可靠的。
三. 通道选择
在介绍CustomizeInfo空间时,我们提到可以使用ICustomizeOutter基于P2P通道发送消息给另外一个在线的用户,该接口的如下几个方法都可能采用P2P通道发送消息:
public interface ICustomizeOutter { 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通道没有创建成功,则消息将通过服务器中转。
SendByP2PChannel方法是当与目标用户之间的P2P通道存在时,一定采用P2P通道发送消息。如果与目标用户之间的P2P通道不存在,那么将采取的操作取决于该方法的第4个参数ActionTypeOnNoP2PChannel的值,可以是通过服务器中转、也可以是丢弃消息。所以,调用SendByP2PChannel方法发送目标消息,意味着,目标消息是可以被丢弃的。
ActionTypeOnNoP2PChannel定义如下:
public enum ActionTypeOnNoP2PChannel { /// <summary> /// 通过服务器中转 /// </summary> TransferByServer = 0, /// <summary> /// 丢弃消息 /// </summary> Discard }
两个客户端之间有可能同时存在两个P2P通道:一个是TCP的,一个是UDP的。在这种情况下,ESFramework将优先使用基于TCP的P2P通道。
还记得前面我们介绍的ICustomizeOutter接口的TransferByServer方法,它的意思是,即使有可靠的P2P通道存在,信息也一定要通过服务器中转。
顺便说一下,当我们采用前面介绍的IFileOutter来发送文件给其他在线用户时,如果存在P2P通道,则ESFramework将通过P2P通道来发送文件数据块。
四. P2P通道控制器 IP2PController
ESPlus为客户端提供了ESPlus.Application.P2PSession.Passive. IP2PController接口以控制和管理P2P通道。
通过IRapidPassiveEngine暴露了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通道的实时状态。
您可以查看P2P的demo的源码(ESFramework Demo -- P2P通信Demo(附源码),并运行demo,来尝试一下P2P。
下一篇:ESFramework 开发手册(05) -- 好友与组
上一篇:ESFramework 开发手册(03) -- 文件(夹)传送
-----------------------------------------------------------------------------------------------------------------------------------------------
Q Q:168757008