65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 WCF 进行点对点文件共享

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (199投票s)

2013年8月5日

CPOL

28分钟阅读

viewsIcon

413925

downloadIcon

5959

在本文中,您将了解 P2P 网络和 Windows Communication Foundation,以便通过 P2P 网络在对等节点之间共享您的文件。

目录

引言

系统资源的缺乏导致了我们可以利用全球海量(且大部分可用)计算机的这一想法。例如,我们可以利用 PC 的计算资源,这些 PC 大部分时间都处于空闲状态。我们还可以利用存储在计算机上的海量数据。此外,与使用服务器/客户端架构不同,可以直接通过点对点连接连接计算机。这很有帮助,因为当我们想处理集中式架构时,会出现一些问题,而且随着时间的推移,这些问题会变得越来越棘手。这些日益增长的问题迫使我们转向更强大的硬件。因此,我们需要使用更大带宽和更强大的服务器(可能是超级计算机),这些都非常昂贵。为此,出现了一种名为“分布式系统”的新型计算系统。分布式系统是一种软件系统,其中位于网络计算机上的组件通过传递消息进行通信和协调操作。[1]

点对点 (P2P) 计算或网络是一种分布式应用程序架构,它将任务或工作负载分配给对等节点。对等节点在应用程序中享有同等的特权和能力。它们被认为是构成节点点对点网络。网络上的每个节点都是一台计算机,它与其他对等节点进行交互和通信,以直接提供其部分资源(如处理能力、磁盘存储或网络带宽)给其他网络参与者,而无需服务器或稳定主机的集中协调。[1]

对等节点既是资源的提供者也是资源的消费者,这与传统的客户端-服务器模型相反,在客户端-服务器模型中,资源的消耗和供应总是分开的。新兴的协作 P2P 系统正在超越对等节点执行相似任务并共享资源的时代,而是寻找能够为虚拟社区带来独特资源和能力的异构对等节点,从而使社区能够承担比单个对等节点能完成的更艰巨的任务,同时这些任务对所有对等节点都有益。[2]

您可以在 Torrents 和一些著名的文件共享系统(如 eMule 项目)中看到点对点实现。下面我将解释 P2P 文件共享系统。

背景

一年多以来,我一直在思考 Torrents 是如何工作的?eMule 是如何工作的?好吧,直到我参加硕士期间的分布式系统课程,我才找到了研究这个问题的好机会。我们的老师要求我们开发一个点对点文件共享系统作为实践作业。我当时想:“哦天哪,这个问题在我脑海里盘旋了很久,现在终于有了答案。现在我有了学习它的机会。”于是我开始研究,结果令人失望。研究得越多,我越感到困惑。因此,我决定找一个示例代码来获取一些初步信息,但结果也很令人失望,因为我找不到任何可以让我与两个对等节点共享文件的源代码。我只找到了一些关于点对点聊天的代码示例,尽管这些代码并没有帮助我理解点对点文件共享是如何工作的。于是我开始在不同的论坛上提问,但这也没有太大帮助。您可以在这里这里阅读我的问题。最终,我意识到我应该从头开始自己完成这一切。然而,在本文中,我将让所有热情的程序员更容易地深入理解点对点系统以及我们如何使用 C# 在对等节点之间共享文件。

好了,回到正题。准备好了吗?那么,我们开始吧。

为什么选择 WCF?

WCF 简史

Microsoft 开发了 COM (组件对象模型),以使应用程序组件能够在本地计算机上相互交互和通信,但 COM 不提供组件通过远程调用在分布式系统中通信的方式。因此,Microsoft 开发了 DCOM (分布式组件对象模型) 来弥补 COM 的不足。DCOM 提供了将应用程序组件分布到不同位置的机会。此外,DCOM 提供了一个基础结构来保证安全问题、可靠性和位置独立性。.NET Remoting 的引入是为了创建一种在 .NET 中构建分布式应用程序的方式。“它取代了 DCOM,成为构建分布式应用程序的首选技术。它解决了困扰分布式应用程序多年的问题(例如,互操作性支持、可扩展性支持、高效的生命周期管理、自定义主机和简单的配置过程)。.NET Remoting 通过提供一个简单、可扩展的编程模型,实现了轻松分布式计算的承诺,而不会影响灵活性、可扩展性和健壮性。它附带了组件的默认实现,包括通道和协议,但所有这些都是可插拔的,并且可以在不进行大量代码修改的情况下替换为更好的选项”[Pro WCF4,Practical Microsoft SOA Implementation]。COM+ 是 COM 和 DCOM 的组合。它提供了一个基础结构,应用程序可以使用它来访问开发团队构建的这些服务和功能之外的服务和功能。COM+ 最初是为了提供 COM 组件的基础结构而引入的,但 .NET 也可以利用其服务。

那么,为什么 Web 服务会成为一种新的通信方式呢?

想象一下,如果您开发了一个基于 COM+ 的应用程序,那么用 JAVA 编写的另一个应用程序如何使用它呢?我们如何与其他在其他平台、操作系统等上运行的应用程序通信?然后,我们得出结论,互操作性是一个重要问题,有时公司会花费高昂的成本来通过使用桥接应用程序作为中间件来实现它。Web 服务使这种互操作性更加容易和便宜。“Web 服务不仅仅是创建分布式应用程序的另一种方式。与其他分布式技术相比,Web 服务的独特之处在于,它不依赖于专有标准或协议,而是依赖于开放的 Web 标准(如 SOAP、HTTP 和 XML)。这些开放标准在行业内得到广泛认可和接受。Web 服务改变了分布式应用程序的创建方式。互联网对松耦合且可互操作的分布式技术产生了需求。特别是,在 Web 服务出现之前,大多数分布式技术都依赖于面向对象的范例,但 Web 催生了对自治且平台无关的分布式组件的需求。”[Pro WCF4,Practical Microsoft SOA Implementation]。

WCF 包含了分布式技术所有最佳部分。WCF 将 ASMX 的效率、.NET Remoting 的能力、可扩展性和灵活性,以及 MSMQ 在构建排队应用程序方面的优势,还有 WSE 的互操作性结合在一起。下图提供了关于 WCF 及其用法的相当真实的视图。(有关更多信息,请参阅 Pro WCF 4 书籍。)

图表来自“Pro WCF 4”书籍。

WCF 基础

由于使用 WCF 需要了解其主要概念,因此在本节中将描述一些 WCF 基础知识。一些开发人员可能认为他们对 WCF 了如指掌,但我会说 WCF 比您想象的要深刻得多,除非他们详细研究 WCF,否则他们不会理解这一点。

终结点 (EndPoint):终结点是服务向外部世界公开自己的方式。终结点包含有关确定服务可用方式的路径的信息。终结点是地址、绑定和契约的组合。

  • 地址 (Address):指定消息可以发送到何处,或者服务在哪里处于活动状态或可访问。
  • 绑定 (Binding):描述如何发送消息。
  • 契约 (Contract):决定消息应包含什么内容。

对于某些场景,我们使用一组通用的地址、绑定和契约。因此,我们可以有一些默认或标准地址、绑定和契约。

在下面的几行中,您可能会看到一组标准终结点:

Mex 终结点

地址对于使用 WCF 服务以及能够将消息发送到服务至关重要。WCF 中的地址由几个部分组成,包括:端口、计算机名称、传输方案、路径。端口是可选字段,传输方案是消息传输的协议。因此,服务地址的格式如下:
scheme://<machinename>[:port]/path1/path2

您可以在以下几行中看到地址的示例:

<endpoint
address="https://:8080/QuickReturns/Exchange"
bindingsSectionName="BasicHttpBinding"
contract="IExchange" /> 

基地址

当有多个终结点与 WCF 服务关联时,我们可以定义一个主地址,然后通过相对地址寻址这些终结点。主地址称为基地址。基地址定义如下:

<host>
<baseAddresses>
<add baseAddress="https://:8080/QuickReturns"/>
<add baseAddress="net.pipe:///QuickReturns"/>
</baseAddresses>
</host>

而终结点的相对地址将是这样的:

<endpoint
name="BasicHttpBinding"
address="Exchange"
bindingsSectionName="BasicHttpBinding"
contract="IExchange" />
<endpoint
name="NetNamedPipeBinding"
address="Exchange"
bindingsSectionName="NetNamedPipeBinding"
contract="IExchange" /> 

绑定:绑定定义了您与服务通信的方式。基于此定义,有几种预定义绑定如下:

契约:契约用于实现不同平台之间的真正协作,另一方面,契约包含了一系列暴露服务给外部世界的约定。契约有多种风格。ServiceContract 是服务的公开行为,DataContract 引入了持久数据(服务的字段和属性),MessageContract 考虑到了安全性等方面,可以根据服务的时效性需求和期望来自定义 SOAP 消息。OperationContractMessageContract 的一个方面,它提供了访问和使用服务行为的方法。

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) { … };
}  

通道

通道负责根据所选择的消息传递模式在客户端和服务器之间传输消息。通道由工厂创建,以便它们可以跨指定的通道或一组通道与服务方通信。在下面的图中,您可以看到消息传递堆栈的结构以及通道在其中的位置。

现在我们可以说绑定是控制通道堆栈的处理程序。当我们选择预定义的绑定之一时,我们实际上是在选择一个特定的通道。

ServiceHost 和 ChannelFactory

ServiceHost 是一个允许您以编程方式创建服务主机的类,您可以对其进行任何操作,例如设置行为、地址、绑定和契约。要使用此 ServiceHostChannelFactory,您应该使用 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
        ("https://:8080/QuickReturns/Exchange");
     ServiceHost host = new ServiceHost(typeof(TradeService);
     host.Open();
     Console.WriteLine("Service started: Press Return to exit");
     Console.ReadLine();
  }
}
}

ChannelFactory 是一个使客户端能够访问服务器端设置的服务类的类。客户端仅知道服务的公开契约,因此它将接口作为其泛型参数。然后,当您使用通道时,您将能够访问服务并调用其方法。这是 Visual Studio 在您的项目中创建并添加 Web 服务作为 WebReference 时的实际后台过程。您可以在下面的代码示例中看到调用我们创建并已通过 ServerHost 类启动的服务的方法。

internal class ExchangeServiceSimpleClient
{
    private static void Main(string[] args)
    {
        EndpointAddress address =
            new EndpointAddress
                ("https://: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);
    }
}

客户端的 App.config 文件

<configuration>
<system.serviceModel>
<client>
<endpoint address="https://:8080/QuickReturns/Exchange"
binding="basicHttpBinding"
contract="QuickReturns.StockTrading.ExchangeServiceClient. ITradeService">
</endpoint>
</client>
</system.serviceModel>
</configuration>

这两个类非常重要,因为它们是计算机网络中所有节点之间连接的基础。我们将大量使用这些类来构建文件共享系统。因此,您对这两个类的理解越深入,就越能理解如何在网络中共享文件。您可以阅读上述书籍的第三章来深入了解它们。

WCF 中有许多细节需要您了解。因此,我强烈建议您详细研究它,否则阅读代码将只是徒劳的追逐。这里我将尝试解释 WCF 的基础知识,但您也应该至少开发一个项目,从客户端到服务器进行远程调用,以便更好地理解正在发生的事情。至于其余的,让我们谈谈点对点架构及其概念。

点对点概念

由于此介绍对于创建文件共享系统很重要,因此在本节中,我将总结 P2P 架构的主要概念。

首先,我将讨论点对点网络的各种类型。点对点网络有两种类型:纯 P2P 网络混合 P2P 网络。尽管这些名称似乎很直观,但我们应该了解这两种类型的主要区别。纯 P2P 网络是一个仅通过对等节点工作的对等节点网络,没有独立的客户端或服务器概念,而是每个节点都相互连接并一起工作,并根据需要同时扮演客户端和服务器的角色。混合 P2P 网络是一个包含客户端和服务器概念的网络。此服务器仅负责响应对等节点发送的获取信息的请求。服务器除了存储一些汇总信息外,从不存储任何数据。例如,在 P2P 文件共享系统中,对等节点请求有关其他对等节点和共享文件的当前位置的信息,然后服务器通过提供有关共享文件的信息(例如文件大小、对等节点名称、文件扩展名和可用对等节点)来响应其请求。当我们谈论 P2P 网络时,我们会遇到一些对于理解点对点网络来说太重要的术语和概念。在下面的几行中,我将说明一些最重要的概念并进行总结,以便在本篇文章中易于理解。

  • Mesh (网格):点对点应用程序中的网络称为 Mesh 或 Mesh Networks。
    • Mesh Types (网格类型):网格网络有两种类型。
      • Peer Channels (对等通道):对等通道基本上是 WCF 中可用的一种基于消息的服务。
      • Groupings (分组):在这种类型的对等网络中,对等节点通过复制包含Data.Grouping(可在 Windows XP SP2 中获得)的记录来交换消息。
 
  • Peer (对等节点)网络中相互交互的每台计算机都称为对等节点。
  • Cloud (云):云是一个具有指定地址范围的网格网络,该范围与 IPV6 范围相关。云中的对等节点是可以跨此范围进行通信的节点。以下是两种预定义的云类型:
    • Global_ (全局):云:如果计算机连接到 Internet,那么它将加入全局云。
    • LinkLocal_ (本地链接):云:一组通过局域网连接的节点,它们在一个本地链接云上工作。
  • PNRP (*非常重要*)

当我们在一个云中时,每个节点都应该由一个唯一的 ID 来标识。正如您所知,当我们使用 Internet 时,我们通过 DNS 标识每个服务器。但网格网络由于对等节点的动态本质而无法使用 DNS。对等节点的性质是极其动态的,我们无法为其使用静态 IP 地址。然后我们使用另一种称为对等名称解析协议 (PNRP) 的协议来解析对等节点的 ID,而不是 DNS。

  • PeerName (在 .NET 中是 PeerHostName)
每个对等节点除了 ID 外还有一个名称,称为PeerNamePeerName 可以注册为安全名称或不安全名称。建议在私有网络中使用安全名称,在全局网络(Internet)中建议使用安全名称。不安全网络在对等节点名称的开头可能有一个0.,例如“0.Peer1”。另一方面,安全名称由数字签名进行签名。您可以在 .NET 中按名称解析对等节点。
  • Peer Graph (对等图):图是可以通过邻居连接与其他节点通信的对等节点集合,因此,它可以将消息发布给图中的所有节点。
  • Registering Peer (注册对等节点):首先,对等节点需要在一个云中注册。您可以通过编程方式注册一个云,或者使用 **netsh** 命令在一个云中注册对等节点,如下图所示:
  • Resolving Peer (解析对等节点):我们可以使用 .NET 和 **netsh** 命令来解析对等节点。当我们解析一个对等节点时,实际上,我们就可以访问对等节点的信息,例如对等节点名称、对等节点端口,并能够与之协同工作。
有关 **NetShell** 及其对点对点网络命令的更多信息,请参阅此处,这篇文章也很有信息量。
命名空间:要利用 .NET 类来处理对等节点,我们需要同时使用 System.ServiceModelSystem.Net.PeerToPeer 命名空间。

代码解析 (一切如何运作)

此项目的代码分为以下 5 个主要子项目:

  • FreeFile.DownloadManager
  • FreeFiles.TransferEngine.WCFPNRP
  • FreeFiles.UI.WinForm
  • FreeFilesServerConsole

那么,让我们谈谈这些部分如何协同工作,然后我将详细介绍每个部分。

首先,我应该提到这个项目是一个混合 P2P 网络。正如我在开头提到的,这意味着我们有一些节点和一个服务器(我们称之为超级对等节点),它通过存储和准备有关文件路径和节点 ID 的信息来为节点服务,以促进节点的协作。所以,我们有一个服务器来处理一些任务,比如搜索文件。因此,我们需要启动一个提供 WCF 服务的服务器,以便其他节点能够连接到它并获取它们所需的信息。此服务器可以是 Windows 服务或控制台服务器。然而,在此项目中,它是一个控制台项目,我认为它应该在 Windows 服务上运行。另一个 WCF 服务在对等节点需要相互连接以下载所需文件时运行。因此,我们必须同时运行两个 WCF 服务。

当我与同事决定将此项目开发为开源应用程序时,我们计划使其具有可靠且灵活的编码风格,因此,我们将项目划分为多个层,并将每个任务分配给一个独立的层。我将讨论 DownloadManagerTransferEngineServerConsole 层中的代码。

核心传输引擎层

此层负责对等节点的所有操作以及在对等节点之间传输请求的文件。然后,在此项目中,我们期望看到一些关于对等节点的代码。这部分是系统的核心。当一个对等节点开始工作时,首先需要注册自己为一个对等节点,然后需要同时扮演服务器和客户端的角色。之后,如果任何对等节点请求文件,它首先需要搜索文件,当它收到文件信息(例如目标对等节点主机名)后,它应该使用该信息连接到对等节点,然后下载文件。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() 方法注销指定云中的对等节点。为了访问对等节点的信息,需要解析该对等节点。当对等节点被解析后,就可以访问诸如对等节点主机名、分类器、端口等信息。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 属性的接口。因此,每个对等节点都向外部世界发布一个服务,以使其方法在服务中可访问。(在这种情况下,方法是 TransferFileTransferFileByHash。)

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);
} 

如果客户端(对等节点)想要访问另一个对等节点的方法,它应该使用通道来实现此功能。这部分代码位于 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);
}  

下载管理器层

此层致力于管理下载任务背后的所有活动,例如管理下载过程和异常、下载文件、将请求的文件分割成多个部分,然后异步下载每个部分、根据其哈希 ID 或名称搜索文件,最后,创建一个共享文件夹并将下载的文件存储到其中。

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 方法。此方法调用由 factory 类创建的 TransferEngine 类的 GetFile 方法。

那么,让我们看一眼 factory 类。此类使用 TransferEngine 层的 DLL 并制造所需引擎(例如:搜索引擎或传输引擎)的新实例。它接受一个 DLL 文件路径并加载它,然后使用它的方法。我们之所以以这种方式访问类库,是因为两个类不能同时引用对方。正如您所见,它使用通道访问服务器的方法来下载文件。

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();
    }.................................................................. 

在此类中,我使用了一个 string 作为 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();            
    }
} 

如您所见,此类使用单元工作模式将所有事务汇总在一个事务中。

它确实带来了两个重要的好处:内存更新将各种事务统一在一个事务中。有关更多详细信息,我建议您阅读这篇文章,因为我发现它很有用。

另一个值得考虑的类是 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 服务启动状态。之后,您可以搜索或共享您想要的文件。

代码使用 (如何运行和调试此应用程序)

使用此代码并不像您想象的那么简单,因为您需要跨至少 2 台通过同一网络连接的计算机运行它,但如果您仔细阅读下面的内容,您就可以运行和测试它。

要运行此应用程序,首先,您的计算机应连接到网络,并且防火墙应已禁用。然后按照以下步骤运行代码:

  • 安装附带的数据库,或在服务器上使用 Visual Studio 中的“从 Edmx 文件生成数据库”选项,并在 FreeFileServerConsole 应用程序上设置正确的服务器配置。
  • 运行 FreeFileServerConsole 项目,然后等待直到它显示服务运行成功消息。
  • 在 2 台不同的联网计算机上打开项目,并运行Windows 窗体应用程序项目
  • 共享一个文件,然后等待直到它显示“完成”消息。
  • 搜索文件,在找到后,单击 GridView 中的文件名以开始下载。目前还没有显示下载进度的进度条,但此功能将在不久的将来和下一个版本中添加。
  • 注意点:您可以使用一台计算机来完成所有这些步骤,但您需要打开两个 Visual Studio 实例,并在运行其中一个实例后,在第二个 Visual Studio 实例中更改端口号(已通过代码设置),然后手动再次设置,否则您在下载文件时会遇到错误。在第二个 Visual Studio 实例中设置新的“端口号”后,也运行它。现在一切都准备就绪!

我认为关于成功运行此应用程序没有其他重要细节可以讨论了,除非我忘记了一些细枝末节。如有任何问题,您可以在此页面的评论部分提问。

当前应用程序的外观如何?

初版在外观和功能上都相当简单。当您设置好应用程序的所有部分后,在共享文件中,您可以搜索它并在一个简单的 GridView 中看到结果。

您也可以通过单击“共享文件”按钮来共享所需的文件。当前的共享方式非常简单。在下一版本中,最重要的新增功能之一是将文件复制到可供其他人访问的共享文件夹,或者能够共享一个文件夹而不仅仅是一个文件。尽管如此,当前的外观就是这样:

此外,当前的外观应该改变得更好,对最终用户来说更方便、更易于使用。

当前缺点的摘要

**重要更新: NetPeerTcpBinding 在 Net 4.5 中已弃用

有关更多信息,请访问此链接: http://bit.ly/1pyFOSb

 

如前所述,存在许多薄弱点和泄漏点。作为为下一版本开发而优选的功能列表,应侧重于以下几点:

  • 用户友好的外观。
  • 一个端口管理器类,可以查找当前计算机上的开放端口。
  • 高级文件共享过程,如共享文件夹或将文件复制到指定文件夹(类似于 Dropbox)。
  • 高级搜索,包括按文件哈希码、相似名称、相关文件等进行搜索。
  • 对等节点的在线状态,并将其显示给想要下载文件的用户。
  • 从共享该文件的不同对等节点下载不同的文件部分。此功能已编码,但在当前版本中未使用。
  • 错误处理:这部分非常重要,我们没有花足够的时间来正确开发它。现在,您会发现许多空的 catch{} 语句。这部分至关重要,我们必须尽快改进。
  • 消息处理。
  • 应该有一个进度条显示下载状态。
  • 数据库设计应更改为更复杂的特性。
  • 正如我警告过的,在下载管理器层的工厂类中,我使用了一个 string 作为 DLL 路径,但这是错误的,并且会带来很多问题(例如,对于每个用户,我们都必须重新设置文件路径。这不好,对吧?)。因此,正确的方法是使用一个返回应用程序文件夹路径的方法。

在后续版本中,还有大量可能的功能需要开发。因此,如果您相信开源开发,请参与进来。

我如何为这个项目做出贡献?

伙计们,我们要再次开发这个项目,并使其成为一项了不起的工作。所以,加入我们吧!

本项目是一个开源项目,因此您可以为其开发做出贡献。您可以提出您的想法和倡议,以获得更好、更高级的应用程序。基于当前项目的开源本质,您可以通过GitHub参与开发过程。正如您可能知道的,GitHub 是一个用于开源开发活动的基于 Web 的应用程序。您可以与其他开发人员讨论,提出您的想法,跟踪最新的更改、当前错误并与他人讨论。有关 GitHub 的更多信息,请参阅其帮助中心。当前应用程序在 GitHub 上的链接是:

您可以下载它并轻松地协作开发。正如我所提到的,有很多可以开发的想法。那么,我们开始吧!

更多学习的额外资源

我试图为点对点网络、WCF 等提供一个合适的视角,但有一些惊人且信息丰富的资源可以了解点对点网络。我上传了一些关于 P2P 网络及其基本概念的有益视频,如下所示。您可以点击图片下载它们。

PNRP 入门

PNRP 和 WCF 直接连接

对等通道的精彩介绍:

关注点

在这样一个令人惊叹的项目作为强制学习的过程中,我得出了一个结论,我对 WCF 及其权威性了解不足。我相信,这个项目最精彩的一点之一是认识到我在 WCF 方面的不足,而这个问题迫使我更深入地学习它。因此,我建议深入学习它,并忘掉您对 WCF 有限的知识。我相信,除非您阅读了大量的书籍并在使用它几年(并适应了新版本)后,否则不可能完全熟悉 WCF。另一个让我感兴趣的是,竟然没有关于使用 C# 进行点对点文件共享的开源项目,这是一个奇怪而有趣的问题。因此,我希望这个开源项目和上面的描述能有所帮助,并且是关于点对点网络使用 C# 进行开源项目的一个合适步骤。

结语

我认为最后的话应该献给那些在这个项目中发挥了重要作用的人。首先,我应该感谢我的好朋友,“Pooya Shahbazian”,他在这项目的架构和编码方面进行了合作。第二个是“Yousof Mehrdad”,他鼓励我继续努力解决这个问题并找到开发这个项目的方法。最后,我感谢“Priscilla Fontes 女士”,她对我的能力给了极大的启发,让我尽我最大的努力完成这个项目,她是我生活中所有方面的伟大伴侣。感谢所有阅读本文的人,也感谢那些为开发过程做出贡献的人。

历史

2013年4月6日

  • 第一个版本 1.0.0
© . All rights reserved.