通过 WCF 实现点对点文件共享 z
-
下载项目的数据库
目录
-
简介
-
背景
-
为什么是WCF?
-
WCF历史简述
-
WCF基础
-
点对点概念
-
代码分析(它是怎么工作的)
-
核心转化引擎层
-
下载管理层
-
服务层
-
代码的使用(如何运行这个应用)
-
当前的应用是什么样的?
-
洩漏总结
-
在这个项目的开发中如何合作?
-
深入学习的外部资料
-
兴趣点
-
结语
-
历史
简介
由于缺少计算、存储和数据资源使得我们产生了这个想法。我们可以使用大量的不确定的资源,这些资源已经存在全球用户的电脑中。例如,我们可以使用PC机的计算资源,这些资源大多数时间是闲置的,另一方面,我们可以使用已经存储在电脑中的大量的数据。更进一步,我们可以让这些资源直接连接起来,使用它们的数据,而不是连到一个中央服务器来满足我们的需要。在我们研究集中式架构时,出现了一些问题,随着时间的推移,这些问题越来越棘手,问题的增长迫使我们转化更强大的硬件,基础设施等。随后,我们就得使用更高的带宽和更强大的服务器,甚至于是超极计算机,这将花费我们大量的资金。为解决这些问题一种新的称之为“分布式系统”的计算系统产生了。分布式系统是一个软件系统,它的组件是位于网络中的计算机,它们通过传递消息来通信和协作。
点对点(P2P)计算或者网络是一种分布式的应用架构,它把任务或者作业负载分散到各个结点中。每个结点有相同的权限、均等的参与到应用中。据说点对点的网络是由结点构成的。每个结点都是网络中的一台计算机,它们与其它的结点通信,分配它们的部分资源:例如处理能力、硬盘存储或者网络带宽,对于网络中的其它参与者是直接可用的,不需要服务器或者稳定的主机的中央协调【1】。
各个结点既是资源的提供者也是资源的消费者,这不同于传统的客户端-服务器模式,在传统模式中,资源的消费和供给常常是分开的。新兴的协作式P2P系统早已超出了结点做相同的事情并共享资源的那个时代,新兴的P2P系统正在寻找各种结点,这些结点可以带来独特的资源和处理能力,从而形成一个虚拟的社区,由此授权它自身参与到更大的任务中,这此任务远远超过了单一结点所能完成的任务,这将使得所有的结点都受益【2】。
你可以在Torrents项目中看到P2P实现的,P2P实现的还有一些著名的文件共享系统例如Emule项目。下面我将解释P2P文件共享系统。
背景知识
我用了一年多去思考Torrents是如何工作的,Emule是如何工作的。直到在研究生的分布式系统的课上之前,我都没有抓住正确地机会去研究它;在这门课上,我的老师要求我们开发一个点对点地文件分享系统作为最后考试的一部分。那时候,我很激动,因为这个事情一直是我喜欢的领域,这个问题在我脑海里存在了很长时间。然后我开始研究它,结果很令人失望;同时越多的学习导致了更多的困惑。然后我决定找一段例子去得到一些基本的信息,然后扩展它们到相关的书籍里面。直到我能开始对BUT编码,我仍然很失落;因为我不能找到任何通过WCF来完成两个点之间文件共享的资料。我只能找到一些代码段,它们是针对点对点聊天的;这些根本不够。然后我开始在CodeProject论坛上面,询问我的问题围绕着“关于点对点系统的学习资料”,“一种可适用的代码例子”,“一个在代码项目上的好文章”和其他相关的关键句来搜寻。但是这一点帮助都没有,因为结果既不是有用的知道,也不是答案。你可以阅读我的问题在这里here 和这里here.
最终,我发现我需要通过自己来开始我的工作;没有一种横切的方法来达到我的目标。在这篇文章中,我将为所有有热情的程序员,采用直接的方式去告诉他们如何得到关于点对点系统和基于点的文件分享的方法的有用信息。我们回到真实地问题上来,准备好了吗?我们开始吧。
为什么是WCF?
简单介绍一下WCF的历史
微软开发了COM(元件对象模型)用于在本地机器上,应用元件彼此之间的交互、交流;但是COM并没有提供对在分布式系统中的远程调用提供元件之间沟通的方法。然后,微软开发了DCOM(分布式元件对象模型)去弥补COM的补足。DCOM提供一种机会,在不同位置分发应用元件。并且,DCOM提供了一种架构,去保证安全、可靠性和位置独立的问题。.NET远程在.NET中被引入去创建分布式应用。“它依赖于DCOM作为倾向的技术去创建分布式引用。它解决的问题是困扰了分布式应用很多年的问题(比如,互操作的支持,可扩展的支持,生命周期管理的高效性,定制主机和简单地配置进程)。在默认的分布式计算中,.NET远程发布提供了简单、可扩展的程序模型,没有牺牲灵活性、可扩展性和健壮性。这来自于默认的可执行元件,包括渠道和协议;但是所有的这些都是补充,可以被其他更好的选项替代而不需要很多的代码修改”【Pro WCF4,实用的微软SOA实现】。COM+是一种结合COM和DCOM的方法。它提供了一种架构,允许应用程序采用它去接入服务和功能,而不需要开发团队去创建这些服务和功能。COM+最初被引入是为COM提供一种架构,但是.NET同样可以使用它的服务。
那么,为什么web服务会作为一个新的沟通方式呢?
想象一下,如果你开发了一个基于com+的应用程序,另外一个应用程序如何通过使用Java来进行编码呢?我们如何与运行于其他平台,操作系统,等等的另一个应用程序进行沟通呢?我们得出结论,互操作性有时候是企业花费高昂的成本,通过使用一个“桥应用程序”作为中间件来实现它的一个重要问题。web服务使这种互操作性变得更方便,更便宜。web服务并不是 创建分布式应用程序的另一种方式。相当于其他分布式技术,web服务的显著特点不是依赖于专有标准与协议,而是依赖于开放的web标准(诸如:soap,http,以及XML)。这些开放的标准被广泛地认可和整个行业所接受。web服务已经改变了如何创建分布式应用程序。互联网已经创建了一个松散的耦合和互操作性的分布式技术。具体而言,之前的web服务,大部分的分布式技术依赖于面向对象的范例,但是web创造了一个自主和独立平台的分布式组件需要。[预WCF4,实用的微软SOA实现]
WCF囊括了分布式技术的众多最佳部分。它包括了ASMX的有效性,.NET远程处理的能力、可扩展性和灵活性,MSMQ创建队列应用的强大能力和WSE的互操作性。下面的图片很清晰的展示了WCF和它的应用(更多信息,请参阅Pro WCF4 手册)。
图片来源于"Pro WCF 4"手册。
WCF基础
因为使用WCF需要了解它的基本概念,这部分将讲述它的一些基础知识。一些开发人员用WCF项目取代了网络服务,他们以为自己知晓WCF,但我要说的是,不是这样的,因为在学习细节之前,WCF远比你们所想像的要深刻。现在我们开始:
终端结点:终端结点是服务向外部暴露自己的一种方式。每个终端结点都包含了路径的信息,这个路径决定了通过何种方式可以该问服务。终端结点是地址、绑定和契约的组合。
-
地址:指出了在哪可以发送消息,或者服务在哪是健在的和可用的。
-
绑定:描述了如何发送消息。
-
契约:描述了消息应当包含哪些消息。
在一些场景,我们要使用一些公共的地址、绑定和契约集。这时我们就得有默认的或者标准的地址、绑定和契约集。
下面列出了一系列标准终端结点(Standard Endpoints):
(译者注:此处英文原文应该是错行了,首先,英文原文在最后一点上不应该有Adresses,否则无法解释,其次,点击进入相应类的msdn说明,可以发现原网站上的英文原文错行了)
-
mexEndpoint用来表现服务元数据的标准终端结点类。
-
AnnouncementEndpoint: 服务用来发送声明消息的标准终端结点类。
-
DiscoveryEndpoint: 服务用来发送发现消息的标准终端结点类。
-
UdpDiscoveryEndpoint: 在UDP多播绑定上,预配置好的用以进行发现操作的标准终端结点类。
-
UdpAnnouncementEndpoint: 让服务通过UDP绑定发送声明消息的标准终端结点类。
-
DynamicEndpoint: 在运行时,使用WS-Discovery动态地寻找终端地址的标准终端结点类。
-
ServiceMetadataEndpoint: 用以元数据交换的标准终端结点类。
-
WebHttpEndpoint: 带有WebHttpBinding绑定并自动添加了WebHttpBehavior行为的标准终端结点类。
-
WebScriptEndpoint: 带有WebHttpBinding绑定并自动添加了WebScriptEnablingBehavior行为的标准终端结点类。
-
WebServiceEndpoint: 带有WebHttpBinding绑定的标准终端结点类。
-
WorkflowControlEndpoint: 允许对于workflow实例调用控制操作的标准终端结点类。
-
WorkflowHostingEndpoint: 支持工作流创建及书签式恢复的标准终端结点类。
-
地址(Addresses) :如果需要使用WCF服务并能够向其发送消息,那么地址是必要的。WCF中的地址由几部分组成,包括: 端口号(Port)、机器名(Machine Name)、传输模式(Transport Scheme)、路径(Path)。 端口号是可选域,传输模式是消息传输的协议。于是,服务地址的格式如下所示:
scheme://<machinename>[:port]/path1/path2
-
地址的简单范例如下:
<endpoint
address="http://localhost:8080/QuickReturns/Exchange"
bindingsSectionName="BasicHttpBinding"
contract="IExchange" />
基地址(Base Addresses)
当有多个终端结点与一个WCF服务相关联,你可以定义一个主地址(primary address),并且把那些终端结点定义在相对地址上。主地址也叫做基地址。你可以如下定义基地址:
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080/QuickReturns"/>
<add baseAddress="net.pipe://localhost/QuickReturns"/>
</baseAddresses>
</host>
而终端结点的相对地址就会像这样:
<endpoint
name="BasicHttpBinding"
address="Exchange"
bindingsSectionName="BasicHttpBinding"
contract="IExchange" />
<endpoint
name="NetNamedPipeBinding"
address="Exchange"
bindingsSectionName="NetNamedPipeBinding"
contract="IExchange" />
绑定(Binding): 绑定定义了你与一个服务联系的方法。根据这一定义,有如下一些预定义的绑定。
Contracts: Contracts可以用来获取不同平台上的协作,同时也是用来把服务表现在外界的一系列转换。 每个Contract具有不同的特点。ServiceContract类是服务的外在表现形式,DataContract用来引入持久性数据 (服务的域和属性),MessageContract使得我们可以在考虑时间需求、服务功能、安全性等等情况下,定制SOAP消息。OperationContract是MessageContract的组成部分,用来表现访问及使用服务的方法。
namespace GettingStartedLib
{
[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")]
public interface ICalculator
{
[OperationContract]
double Add(double n1, double n2);
[OperationContract]
double Subtract(double n1, double n2);
[OperationContract]
double Multiply(double n1, double n2);
[OperationContract]
double Divide(double n1, double n2);
}
}
[ServiceContract]
public class Calculator
{
[OperationContract]
public double Add(double a, double b) { … };
[OperationContract]
private double Subtract(double a, double b) { … };
}
Channels
Channel是用来在客户端和服务端之间,根据选择的消息模式传输数据的。Channel由工厂类创建,因而可以通过特定的或者一系列channels与服务端联系。在下图中,你可以看到消息栈的结构和其中channel的位置。
我们可以这样认为,Binding类是控制Channel栈的处理器。当你选择了一个预定的binding,意味着你选择了一个特定的Channel。
ServiceHost 和 ChannelFactory
ServiceHost是一个允许你以编程方式制做一个服务主机的类,你可以用它做很多事,如设置行为,地址,绑定和契约。要使用 ServiceHost 和 ChannelFactory,你需要使用 System.ServiceModel 命称空间。
using System;
using System.ServiceModel;
using QuickReturns.StockTrading.ExchangeService;
using QuickReturns.StockTrading.ExchangeService.Contracts;
namespace QuickReturns.StockTrading.ExchangeService.Hosts
{
class Program
{
static void Main(string[] args)
{
Uri address = new Uri
("http://localhost:8080/QuickReturns/Exchange");
ServiceHost host = new ServiceHost(typeof(TradeService);
host.Open();
Console.WriteLine("Service started: Press Return to exit");
Console.ReadLine();
}
}
}
ChannelFactory 是一个使客户端能够获得已设置在服务器端的服务的类。客户端只要知道服务公开的契约,就可以获取一个接口作为其类属参数。然后,当你使用一个通道时,你将能够获得对服务的访问和调用它的方法。这是当你用 Visual Studio 创建并添加一个 WebService 作为一个 WebReference 到你的项目中时真实的后台处理。你可以在下面的示例代码中看到,我们通过 ServerHost 调用我们先前创建并启动的服务中的一个方法:
internal class ExchangeServiceSimpleClient
{
private static void Main(string[] args)
{
EndpointAddress address =
new EndpointAddress
("http://localhost:8080/QuickReturns/Exchange");
BasicHttpBinding binding = new BasicHttpBinding();
IChannelFactory<ITradeService> channelFactory =
new ChannelFactory<ITradeService>(binding);
ITradeService proxy = channelFactory.CreateChannel(address);
Quote msftQuote = new Quote();
msftQuote.Ticker = "MSFT";
msftQuote.Bid = 30.25M;
msftQuote.Ask = 32.00M;
msftQuote.Publisher = "PracticalWCF";
Quote ibmQuote = new Quote();
ibmQuote.Ticker = "IBM";
ibmQuote.Bid = 80.50M;
ibmQuote.Ask = 81.00M;
ibmQuote.Publisher = "PracticalWCF";
proxy.PublishQuote(msftQuote);
proxy.PublishQuote(ibmQuote);
}
}
客户端应用配置文件:
<configuration>
<system.serviceModel>
<client>
<endpoint address="http://localhost:8080/QuickReturns/Exchange"
binding="basicHttpBinding"
contract="QuickReturns.StockTrading.ExchangeServiceClient. ITradeService">
</endpoint>
</client>
</system.serviceModel>
</configuration>
这两个类很重要,因为它们是一个计算机网络中所有节点之间的连接的基础。在我们建立一个文件共享系统的代码中我们将完全依仗这些类,所以花越多成本来理解这两个类如何工作的,我们就能越多理解如何能在网络节点之间共享文件。你可以阅读我提到的书的第3章来深度理解他们。
你需要知道关于WCF的丰富细节。所以我强烈建议详细学习WCF,否则阅读代码只会徒劳无功。我试着举出一些WCF的重要基础,但你需要至少开发一个从客户机向服务器(运行着一个服务)发出远程调用的项目。在深入研究WCF之前,我有一些严重的问题。最终,我得出的结论是,我需要从头学习WCF,和忘了以前我曾通过 Visual Studio 用它来创建过一些 Web 服务,因为那些印象没有好处。至于其他的,让我们先谈谈点对点对等结构和概念。
点对点的概念
在本节,我将总述P2P架构的主要概念,因为这些介绍对于开发共享文件系统相当重要。
首先,我将要讨论不同类型的P2P网络。有两种不同类型的点对点网络:纯点对点网络和混合型P2P网络。看起来它们的名字已经解释了这两类网络,但我们仍需要明确这两种类型的不同。纯P2P网络是网络中的各个结点通过其它的结点工作,不存在客户端和服务器的区分,每个节点互相关联并一起工作,同时根据需要发挥着客户端和服务器的作用。混合型P2P网络是虽然网络是由客户端和服务器组成的,但服务器仅用于响应由对等结点发出的获取信息的请求的。服务器不会存储除了总数之外的任何数据。例如,在P2P的共享文件系统中,一个节点请求关于其它结点和当前共享文件存储位置的信息,服务器会响应它们的请求,给出共享文件的一些信息如文件大小、结点的名称、文件的扩展名和可用的结点们。在我们讨论P2P网络时,我们遇到了一些表达和概念,它们对于理解P2P网络相当的重要。下面我将解释一些重要的概念,对它们进行总结以便在本文中理解它们。
-
网格:在点对点应用中的网络称为网格或者网格网络。
-
结点通道:结点通道是WCF中基于消息的可用的服务。
-
分组:在对等网中,结点通过复制包含Data.Grouping记录来交换消息。这些记录中的Data.Grouping在WindowsXP SP2系统中是可用的。
-
网络的类型:有两种类型的网格网络:
结点:网络中的每一台可以一起互相交互的计算机,称为结点。
-
云:云是一种网格网络,它有一个特定的地址范围,这个地址范围与IPV6范围相关。云中的结点可以通过这个范围进行通信。两种预定义的云,如下:
-
全球性的云:如果一台计算机接入了互联网,那么它就加入了全球性的云。
-
本地链接云:一些结点通过广域网互相关联,那么它们就是在一个本地链接云里。
-
PNRP(*相当重要):
“当我们在云里时,每个结点都应当通过唯一的编号识别。正如你所知道的,在使用互联网时,我们是通过DNS识别每一台服务器的。但是在网格网络中不能使用DNS,因为结点实际上是动态的。结点原本就是动态的,我们不能使用静态的IP地址。因此我们使用了另一种称作是对等结点命名解决方案的协议(PNRP)而不是DNS协议来解决结点编号的问题。”
-
结点名称(在.NET 网络中对等结点的主机名):
每个结点除了它的编号之外会有一个称为“结点名称”。 结点名称可以被注册,不论名称是安全的或者不安全的。在私有网中安全的名称是推荐的;也建议在全球网络中使用安全的名称。不安全的网络可以看到以0.开头的结点名称,例如0.Peer1。安全的名称也可以做为数字签名。在.NET网络中你可以通过它的名称确定(解析)一个结点。
-
结点图表:图表是结点的集合,这些结点可以通过与它们相邻结点的连接与其它的结点通信,因此,使得向图表中的所有结点发送消息成为可能。
-
注册结点:首先结点需要在云中注册。可以通过编程或者是使用netsh命令在云中注册结点。命令行注册如下所示:
-
解析结点:我们可以使用.NET和netsh命令来解析结点。当我们在解析结点时,事实上,我们在获取结点的信息例如:结点的名称、结点的IP地址和结点的端口,这样我们就可以利用这些结点工作。
关于NetShell和它关于对等网络的命令的信息,请看这里,这篇文章的信息相当的充分。
命名空间:要使用.NET的类来使用结点,我们需要使用System.ServiceModel和System.Net.PeerToPeer这两个命名空间。
代码概述 (系统是如何工作的)
本项目的代码被分割成了如下5个主要的子项目:
-
FreeFile.DownloadManager
-
FreeFiles.TransferEngine.WCFPNRP
-
FreeFiles.UI.WinForm
-
FreeFilesServerConsole
让我们讨论下这些部分是如何共同工作的,然后我会详述每个部分。
首先,我应该提到这个项目是基于混合式P2P网络。正如我在开始提到的那样,这意味着我们有一些结点和一个服务器(我们把服务器叫做超级结点)。服务器结点通过存储、准备一些诸如文件路径、结点ID这样的信息,促进结点协作,从而向其它结点提供服务。所以,我们需要一个服务器,执行一些比如(搜索文件)的任务。因此,我们需要搭建一个WCF服务器,让其它结点能够连接上并获得他们需要的信息。服务器可以是Windows服务或者控制台服务器。在本项目中,它是控制台项目,虽然我认为它最好是一个Windows服务。另一个WCF服务应该在结点之间进行连结并下载需要文件的时候运行。因此,我们应该需要在同时运行两个WCF服务。
当我和我的同事决定把这个项目作为开源应用程序时,我们计划让其具有相当的可靠性和灵活性,因此,我们把这个项目分成了几层,并分摊每个任务到单独的一层中。我会讨论DownloadManager类、TransferEngine类和ServerConsole类,这几层的代码。
核心传输引擎层(Core Transfer Engine Layer)
这一层承担了一个结点行为的所有任务,同时也能够在结点间传输需要的文件。在本项目中,我们希望看到一些关于结点的代码。这部分是系统的核心部分。当一个结点开始工作的时候,首先,它应该把自己注册为一个参与 结点,然后它应该同时扮演服务器和客户端的角色。然后,如果某个参与结点请求一个文件,首先要搜索这个文件,然后当它收到了这个文件的信息(比如目标结点的主机名),它应该用那些信息连接到对应的结点然后下载文件。PNRPManager类是用来注册和解析结点用的。
Register()方法在云端注册此结点,并接收一个列表形式的PeerInfo作为输入参数
public List<PeerInfo> Register()
{
List<PeerInfo> registerdPeer = new List<PeerInfo>();
foreach (var registration in registrations)
{
string timeStamp = string.Format("FreeFile Peer Created at : {0}",
DateTime.Now.ToShortTimeString());
registration.Comment = timeStamp;
try
{
registration.Start();
if (registerdPeer.FirstOrDefault(x => x.HostName ==
registration.PeerName.PeerHostName) == null)
{
PeerInfo peerInfo = new PeerInfo(registration.PeerName.PeerHostName,
registration.PeerName.Classifier, registration.Port);
peerInfo.Comment = registration.Comment;
registerdPeer.Add(peerInfo);
}
}
catch { }
}
this.CurrentPOeerRegistrationInfo = registerdPeer;
return registerdPeer;
}
Start()方法注册结点,而Stop()方法注销某个云中的结点。为了获取一个结点的信息,结点会被解析。当一个结点解析完成,一些信息诸如结点主机名(peer host name),标识符(Classifier),端口号(port)是可以访问的。ResolveByPeerHostName()方法可以通过主机名解析一个结点并返回一个列表形式的PeerInfo。
public List<PeerInfo> ResolveByPeerHostName(string peerHostName)
{
try
{
if (string.IsNullOrEmpty(peerHostName))
throw new ArgumentException("Cannot have a null or empty host peer name.");
PeerNameResolver resolver = new PeerNameResolver();
List<PeerInfo> foundPeers = new List<PeerInfo>();
var resolvedName = resolver.Resolve(new PeerName(peerHostName,
PeerNameType.Unsecured), Cloud.AllLinkLocal);
foreach (var foundItem in resolvedName)
{
foreach (var endPointInfo in foundItem.EndPointCollection)
{
PeerInfo peerInfo = new PeerInfo(foundItem.PeerName.PeerHostName,
foundItem.PeerName.Classifier,endPointInfo.Port);
peerInfo.Comment = foundItem.Comment;
foundPeers.Add(peerInfo);
}
}
return foundPeers;
}
catch (PeerToPeerException px)
{
throw new Exception(px.InnerException.Message);
}
}
}
当解析结束后,得到了一系列结点,以EndPointCollection类的对象的形式出现。如果我们使用foreach循环,我们可以访问其中所有的结点作为终端结点。
FileTransferServiceHost 类使每个点成为向另外的点提供所需文件的服务器主机。这个类使用 TCP 协议在各点间传输数据。DoHost() 方法得到一个基于节点主机名的地址,然后添加一个应用了 ServiceContract 属性的接口。因此每个点都向外部世界发布了一个服务以使其方法可通过服务被访问到。(在这种情况下,方法是 TransferFile 和 TransferFileByHash)
sealed class FileTransferServiceHost
{
public void DoHost(List<PeerInfo> peers)
{
Uri[] Uris = new Uri[peers.Count];
string Address = string.Empty;
for (int i = 0; i < peers.Count; i++)
{
Address = string.Format("net.tcp://{0}:{1}/TransferEngine",
peers[i].HostName, peers[i].Port);
Uris[i] = new Uri(Address);
}
FileTransferServiceClass currentPeerServiceProxy = new FileTransferServiceClass();
ServiceHost _serviceHost = new ServiceHost(currentPeerServiceProxy, Uris);
NetTcpBinding tcpBinding = new NetTcpBinding(SecurityMode.None);
_serviceHost.AddServiceEndpoint(typeof(IFileTransferService), tcpBinding, "");
_serviceHost.Open();
}
}
[ServiceContractAttribute]
interface IFileTransferService
{
[OperationContractAttribute(IsOneWay = false)]
byte[] TransferFileByHash(string fileName,string hash, long partNumber);
[OperationContractAttribute(IsOneWay = false)]
byte[] TransferFile(string fileName, long partNumber);
}
如果一个客户端(点)想要访问到另一个点的方法,应使用Channel(通道)得到这个能力。这一部分已被编码在FileTransferServiceClientClass 类中,如下:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single,
InstanceContextMode = InstanceContextMode.Single, UseSynchronizationContext = false)]
class FileTransferServiceClientClass : System.ServiceModel.ClientBase<IFileTransferService>
{
public FileTransferServiceClientClass() :base()
{
}
public FileTransferServiceClientClass(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public FileTransferServiceClientClass(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public FileTransferServiceClientClass(string endpointConfigurationName,
System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public FileTransferServiceClientClass(System.ServiceModel.Channels.Binding binding,
System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
public byte[] TransferFile(string fileName,string hash, long partNumber)
{
return base.Channel.TransferFileByHash(fileName, hash, partNumber);
}
public byte[] TransferFile(string fileName, long partNumber)
{
return base.Channel.TransferFile(fileName, partNumber);
}
}
这个应用程序的每个实例把自身登记作为一个节点,然后用 FileTransferServiceHost 开始建立一个 WCF 服务,如下:
void IFileProviderServer.SetupFileServer()
{
var peers = pnrpManager.Register();
if (peers == null || peers.Count == 0) throw new Exception("Host not registered!");
var fileTransferServiceHost = new FileTransferServiceHost();
fileTransferServiceHost.DoHost(peers);
}
下载管理层
这一层保证了管理下载任务后边的所有活动,例如管理下载处理和异常,下载文件,把请求的文件分片,然后异步的下载各分片,基于哈希编号或者名称检索文件,共享文件夹,并把下载的文件存储到共享文件夹中。
这个search()类提供了搜索的引擎来查找跨服务的目标文件,这些服务运行在服务层。如我前边所讲述的,服务器发布了服务,这些服务提供了应用结点的一些公共的信息(例如文件名、结点主机名,文件类型)。在下边的段落中我将详细的讲述服务层的代码。
public List<Entities.File> Search(string searchPattern)
{
FileServer.FilesServiceClient fileServiceClient = new FileServer.FilesServiceClient();
List<Entities.File> filesList = new List<File>();
foreach (var file in fileServiceClient.SearchAvaiableFiles(searchPattern))
{
Entities.File currentFile = new File();
currentFile.FileName = file.FileName;
currentFile.FileSize = file.FileSize;
currentFile.FileType = file.FileType;
currentFile.PeerID = file.PeerID;
currentFile.PeerHostName = file.PeerHostName;
filesList.Add(currentFile);
}
return filesList;
}
这层代码核心的部分是FileTransferManager类,它管理了文件的传送过程。它由下载全部或部分文件的全部所需的方法构成。UI层调用该类的Download()方法,这个方法启动任务,使用StartDownload方法开始它的动作。随后StartDownload方法被调用。通过这个方法,文件的请求是基于它的端口号,这个端口号是基于一个常量10240生成的。
public void Download(Entities.File fileSearchResult)
{
//var action =new Action<object>(searchForSameFileBaseOnHash);
//Task searchForSameFileBaseOnHashTask = new Task(action, fileSearchResult);
//searchForSameFileBaseOnHashTask.Start();
var downloadAction = new Action<object>(StartDownload);
Task downloadActionTask = new Task(downloadAction, fileSearchResult);
downloadActionTask.Start();
}
const long FilePartSizeInByte = 10240;
private void StartDownload(object state)
{
Entities.File fileSearchResult = state as Entities.File;
//We need to apply multiThreading to use multi host to download different part of
//file concurrently max number of thread could be 5 thread per host in
//all of the application;
long partcount = fileSearchResult.FileSize / FilePartSizeInByte;
long mod = fileSearchResult.FileSize % FilePartSizeInByte;
if (mod > 0) partcount++;
downloadFilePart(new DownloadParameter {FileSearchResult=fileSearchResult,
Host = fileSearchResult.PeerHostName, Part = partcount });
}
如你所见,StartDownload方法调用downloadFilePart方法,这个方法调用TransferEngine类的GetFile方法,TransferEngine类是由factory类创建的。
所以,让我们瞥一下这个工厂类。这个类使用 TransferEngine 层的DLL制造所需引擎(如:搜索引擎或传输引擎)的一个新实例。它需要得到一个DLL文件的路径并加载它,然后利用它的方法。由于“两个类不能同时互相引用”这种理性的原因,我们对一个类库使用这种访问。正如你看到的,它使用信道(channels)访问服务器的方法来下载文件。
public sealed class Factory
{
Factory()
{
Assembly transferEngineAssembly = Assembly.LoadFile(String.Format(
"E:\\FreeFiles\\FreeFiles.TransferEngine.WCFPNRP\\bin\\Debug\\FreeFiles.TransferEngine.WCFPNRP.dll"));
var tnaTypes = transferEngineAssembly.GetTypes();
foreach (var item in tnaTypes)
{
if (item.GetInterface("ITransferEngineFactory") != null)
{
ITransferEngineFactory ITransferEngineFactory = Activator.CreateInstance(item) as ITransferEngineFactory;
this.transferEngine = ITransferEngineFactory.CreateTransferEngine();
this.fileProviderServer = this.transferEngine as IFileProviderServer;
break;
}
}
/* Create
*searchEngine;
*/
this.searchEngine = new Searchengine();
}..................................................................
在这个类中,我用一个字串作为 DLL 的路径,但这是错的,会弄出许多问题(例如,对于每个用户,我们必须根据实情重新设置文件路径,这太笨了)。那么,正确的方式是使用一个返回应用程序文件夹路径的方法。
如之前曾提到的,这个层是此项目的一个非常重要的组成部分,有很多可以采用它的衍生物(在未来的版本)。
服务器层
欢迎进入到本文要解释的最后一层。解释服务器这一层,我想说它就是一个WCF服务,它提供了文件(点对点共享的文件)之间检索的一些方法。这一层使用Entity Framework作为统一操作数据库的对象关系映射(ORM)。打开edmx文件,您将看到如下的数据库架构:
基于以上的设计,每一个节点可以关联多个文件(一对多的关系),这也恰恰是我们所期望的。这个结构非常简单,但是当我们要开发更复杂的系统的时候,它会变得十分精巧(这是本项目后续版本要实现的主要目标)。不管怎样,就像我说的,这一层扮演着WCF服务的角色,这个重要的角色在FilesService类中实现,如下:
public class FilesService
{
private FreeFilesEntitiesContext _freeFilesObjectContext=new FreeFilesEntitiesContext();
[OperationContract]
public void AddFiles(List<Entities.File> FilesList,Entities.Peer peer)
{
FileRepository fileRepository = new FileRepository
(_freeFilesObjectContext as FreeFilesServerConsole.IUnitOfWork);
this.AddPeer(externalPeerToEFPeer(peer));
fileRepository.AddFiles(externalFileToEFFile(FilesList));
SaveFile();
}
[OperationContract]
public void AddPeer(FreeFilesServerConsole.EF.Peer Peer)
{
FileRepository fileRepository = new FileRepository
(_freeFilesObjectContext as FreeFilesServerConsole.IUnitOfWork);
fileRepository.AddPeer(Peer);
}
[OperationContract]
public List<Entities.File> SearchAvaiableFiles(string fileName)
{
FileRepository fileRepository = new FileRepository
(_freeFilesObjectContext as FreeFilesServerConsole.IUnitOfWork);
return internalFileToEntityFile(fileRepository.SearchAvaiableFiles(fileName));
}
public void SaveFile()
{
_freeFilesObjectContext.Save();
}
.
.
.
.
文件搜索(以及和文件操作相关的其他方法,如添加文件、添加文件所属的节点)的实现在FileRepository类中。
class FileRepository:IFilesRepository
{
private FreeFilesEntitiesContext _freeFilesObjectContext;
public FileRepository(IUnitOfWork unitOfWork)
{
_freeFilesObjectContext = unitOfWork as FreeFilesEntitiesContext;
}
public List<FreeFilesServerConsole.EF.File> SearchAvaiableFiles(string fileName)
{
var filesList = from files in _freeFilesObjectContext.Files
join peers in _freeFilesObjectContext.Peers on files.PeerID equals peers.PeerID
where files.FileName.Contains(fileName)
select new {files,peers };
List<FreeFilesServerConsole.EF.File> List = new List<File>();
foreach (var item in filesList)
{
File file = new File();
file.FileName = item.files.FileName;
file.FileSize = item.files.FileSize;
file.FileType = item.files.FileType;
file.PeerHostName = item.peers.PeerHostName;
List.Add(file);
}
return List;
}
public void AddFiles(List<FreeFilesServerConsole.EF.File> FilesList)
{
//_freeFilesObjectContext = new FreeFilesEntitiesContext();
try
{
foreach (FreeFilesServerConsole.EF.File file in FilesList)
{
_freeFilesObjectContext.Files.AddObject(file);
}
}
catch (Exception exp)
{
throw new Exception(exp.InnerException.Message);
}
}
public void AddPeer(FreeFilesServerConsole.EF.Peer Peer)
{
//_freeFilesObjectContext = new FreeFilesEntitiesContext();
try
{
_freeFilesObjectContext.Peers.AddObject(Peer);
}
catch (Exception exp)
{
throw new Exception(exp.InnerException.Message);
}
}
public void Save()
{
_freeFilesObjectContext.Save();
}
}
正如你所看到的,这个类使用工作单元模式(Unit Of Work)将所有的任务集合到一个任务中。
事实上它有两个重要的优点:在内存中的更新和一次性统一了各种事务处理。更多的细节,建议您阅读这篇文章,我发现它相当的便捷。
另一个应当注意的类是ServiceInitializer.它管理了服务,使得服务对于外部结点而言是通过配置文件的值可以访问的。
public class ServiceInitializer : IServiceInitializer
{
private string _endPointAddress = string.Empty;
public ServiceInitializer()
{
_endPointAddress =
ConfigurationSettings.AppSettings["FileServiceEndPointAddress"].ToString();
}
public void InitializeServiceHost()
{
Uri[] baseAddresses = new Uri[]{
new Uri(_endPointAddress),
};
ServiceHost Host = new ServiceHost(typeof(FilesService),baseAddresses);
Host.AddServiceEndpoint(typeof(FilesService),
new BasicHttpBinding(),"");
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
Host.Description.Behaviors.Add(smb);
Host.Open();
}
}
在你运行FreeFilesServerConsole项目时这个服务是可用的。你将会看到如下图所示的消息,它发布了WCF服务的开始状态,随后,你可以检索或者共享你所需的文件。
使用代码(如何运行和调试这个应用)
使用这些代码并不像你想像的那样简单,因为你应当在至少两台通过同一网络相互关联的电脑上运行它,如果你认真的阅读了下面的内容,你将可以运行和测试这些代码。
为了运行这个应用,首先,你应当连接到网络并确保防火墙未启用。接着检进你的网络和防火墙。总之,为了运行代码,请参照如下的步骤:
-
安装附带的数据库或使用VisualStudio服务器端的Generate Database from Edmx file选项,在应用的配置文件FreeFileServerConsole中设定正确的服务器配置。
-
运行FreeFileServerConsole项目,并等待直到它写完服务运行成功的消息
-
在网络的两台不同的电脑中打开服务并运行Windows Form application project.
-
共享文件文件直到它发出完成的Done消息
-
检索文件,在它找到后,在GridView网络视图中单击文件的名称开始下载。目 前还没有显示下载过程的进度条,这个特征将在未来的下一个版本中增加。
-
需要注意的事:你可以只用一台电脑完成所有这些步骤,但你需要打开两个VisualStudio的实例,然后运行实例中的一个,你需要通过代码修改端口的设置,然后再手动的重新设置,否则在下载文件时,你将会遇到错误。
我想为了成功的运行这个应用再没什么别的细节可以讲述了,除非我遗漏了一些很小的点。如果遇到任务问题,您可以在本文的评论中提出问题。
目前应用是什么样的呢?
首个版本的外观和特征都相当的简单。在你启动应用的各个部分后,就会有一个共享的文件,你可以搜索,并看到网格视图列出的结果。
你可以通过单击共享文件按钮把指定的文件共享。目前共享的样式非常的简洁。在下一个版本中,最大的重要特征之一就是在共享文件夹中批量复制文件,这个些在共享文件夹中的文件其它使用者是可以访问的;或者可以共享文件夹,而不是共享单一的文件。总之,目前这版的外观就是这样的:
此外,当前版本的外观应当更好一些,更加的便利和对终端用户更加的方便。
当前缺点总结
如我前边所说,存在大量的问题和缺失。在一下个版本中可能开发的新的功能点如列表所示,应当关注的是如下要点:
-
用户友好的界面
-
端口管理类可以找到当前计算机打开的端口号
-
高级文件共享处理,例如共享文件夹或者复制文件到一个指定的文件夹(类似Dropbox)
-
高级搜索功能它由检索文件的哈希代码、相似名称、相关文件等组成。
-
结点在线或者离线的状态可以供下载文件的用户查看
-
从共享文件的不同结点下载文件的不同部分。
-
错误处理:这部分相当的重要,我们将花费大量的时间来把它处理妥当。现在你可以找到大量的空的catch{}代码,这部分至关重要,我们必将尽快的提升这部分。
-
消息处理
-
下载状态显示的进度条
-
根据详细的特征变更数据库的设计
-
如我所警示的,下载管理层的工厂类,我已经使用了一个string类型做为DLL的路径但它是错误的,它制造了很多的问题,(例如,对每个使用者而言,我们得再次设置文件的路径,这太愚蠢了)。正确的方法是使用一个方法返回应用文件的路径。
在下个版本中有大量潜在的特征应当开发。如果你相信开源开发,参与进来吧!
如何协同开发这个项目
该项目是一个开源项目,因此你可以对它的发展做出贡献。可以说出你的想法和举措来做一个更好和更高级地应用程序,基于目前这个项目的开源本质,可以通过Github来参与开发过程。正如你所知道的,Github是一个基于网页应用程序的开源开发活动。可以与其他开发人员讨论,说出你的想法,跟踪最近的变化,目前bug,给他们说说,关于Github的更多信息,可以参考他们的帮助中心,目前在Github之上应用程序链接是:
•https://github.com/amirjalilifard/FreeFilesProject
你可以下载它,并很容易地对其发展合作。当我提及时,有许多可以开发的想法,好吧,让我们开始吧!
更多的外部研究资源
我试图给点对点的网络提供一个合适的角度,wcf等,但也有一些惊奇的信息资源,以此了解点对点的网络。我上传如下一些关于p2p网络及其基本概念的有益视频。你可以通过在图片上敲击而去下载他们。
一些兴趣点
在把这个好玩的项目做为令人着迷的学习的过程中,我得出这样的结论:对于WCF我还不够了解。我相信,这个项目的绝妙之处在于让我通过WCF理解了自己的弱点,这将迫使我更加深入的学习这个项目。所以我建议更深入的学习并且要忘记关于WCF有限的知识。我相信只有阅到大量的书籍并使用这个项目工作几年才可以完成的掌握WCF,(或者你可以参与到新版本的开发中)。另一个我所关注的点是:目 前还没有使用C#语言编写的点对点文件共享的开源项目。我期待这样的开源项目 ,前边的描述是有用的,更进一点,通过恰当的步骤可以使用C#语言编写一个开源的产于点对点网络的项目。
结束语
在本文最后,我应当感谢和追忆在这个项目过程中起到积极作用的人们。首先感谢我的好友“Pooya Shahbazian”,他参与了项目的架构和编码。接下来是“Yousof Mehrdad”,他鼓励我解决这一问题并找到开发项目的途径。最后我要感谢“Priscilla Fontes”女士,因为她极大的激励我尽力完成这个项目,同时感谢她在生活诸方面的陪伴。感谢所有阅读本文的读者,感谢参与开发过程的诸位朋友。