In this section, I am going to summarize the main concepts of P2P architecture, since this introduction is pretty important for coding a file sharing system.
First, I am going to talk about the various types of Peer to Peer networks. There are two kinds of Peer to Peer Network. Pure P2P Network and Hybrid P2P Network. However these namings seem explanatory, but we should know the main distinction between these 2 types. A Pure P2P Network is a network of peers that just works through Peers and there is no separated concept of Client or Server, instead each of the nodes are connected and work together and play the role of both Client and Server as needed. A Hybrid P2P Network is a network which consists of the client and server concepts though. This server is just responsible to answer the requests which is sent by peers to get some information. The server never stores any data except some totalities. For instance, in a P2P file sharing system, peers request some information about the other peers and the current place of the shared file, then server responds to their request by giving some information about the shared files such as File size, Peer name, File extension and the available peers. When we are talking about a P2P network, we meet some expressions and notions which are too important to comprehend the Peer to Peer network. In the below lines, I am going to illustrate some of the most important concepts and summarize them in order to be understandable in this article.
Mesh: The Network in Peer to Peer applications called Meshes or Mesh Networks
Peer Channels: Peer Channels are basically a message based service which is available in WCF
Groupings: In this kind of peer network, Peers exchange message by replicating records containing the Data.Grouping is available in Windows XP SP2
Mesh Types: There are two types of Mesh Networks:
Peer:Each computer in a network of computers who are interacting together, called Peer
Cloud: A Cloud is a Mesh Network which has a specified address scope and this scope is related to an IPV6 scope. Peers in a cloud are those ones who can communicate across this scope. There are 2 types of predefined Cloud as below:
Global_: Cloud: If a computer connects to the internet, then, it is joined to a Global cloud
LinkLocal_: Cloud: A set of nodes who are connected to a via Lan, they are working on a LinkLocal cloud
PNRP (*Pretty momentous):
When we are in a cloud, each node should be identified by a unique ID. As you know, when we use internet, we identify each server by a DNS. But a Mesh network cannot use DNS because of the peers dynamic essence. Peers nature are dramatically dynamic and we cannot use a Static IP address for it. Then we use another Protocol called Peer Name Resolution Protocol (PNRP) to resolve the peers IDs instead of DNS.
PeerName(PeerHostName in .NET):
Each peer beside its ID gets a name is called PeerName. PeerName can be registered either as secured or unsecured names. Secured names are recommended in private networks and secured names are suggested in the Global network (Internet) . Unsecured networks can seem with a 0. at the first of Peer's Name, for instance " 0.Peer1". Secured names on the other hand are signed by a digital signature. You can resolve a peer by its name in .NET.
Peer Graph: A graph is a collection of peer nodes that can communicate with other nodes across their neighbor's connections, hence, it makes this possibility to publish a message for all nodes in a graph
Registering Peer : First of all, the peer needs to become registered in a cloud. You can register a cloud programatically or use netsh command to register peer in a cloud such as the below figure:
Resolving Peer : We can use both .NET and netsh command to resolve a Peer.When we resolve a peer, indeed, we get access to the peer's information such as peer's name, peer's port and peer's port and become capable to work with it.
For more information about NetShell and its commands for Peer To Peer Networking, see and this is pretty informative too.
Namespaces: For taking advantage of .NET classes to work with peer, we need to use both System.ServiceModel and System.Net.PeerToPeer namespaces.
The code of this project has been divided into 5 main subprojects as follows:
FreeFile.DownloadManager
FreeFiles.TransferEngine.WCFPNRP
FreeFiles.UI.WinForm
FreeFilesServerConsole
So, let's talk about how these parts work together and then I'll describe each one in detail.
At first, I should mention that this project is a Hybrid P2P Network. As I cited at the pristine lines, it means we have some nodes and a server (we name it Super Peer) who serves the nodes by storing and preparing some information concerning file's path and node's ID to facilitate the nodes collaboration. So, we have a server for taking on some tasks such as searching the files. Therefore, we need to start a Server that provides a WCF service in order for the other nodes be able to connect to it and get their desired information. This server can be a Windows Service or a Console server. However in this project it is a Console project, I believe it should run over a Windows Service. The another WCF service is run when peers should connect to each others for downloading the required files. So, we ought to have two WCF running services in a same time.
When me and my Colleague, decided to develop this project as an open source application, we planned for making a reliable and flexible coding style, hence, we divided this project into several layers and amortized each task to a separated layer. I am going to talk about the codes of DownloadManager, TransferEngine and the ServerConsole layers.
This layer undertakes all the tasks of Peer actions and also transfers the requested files between peers. Then, in this project, we expect to see some codes around Peers. This part is the heart of this system. When a peer starts to work, first it should register itself as a peer, then it should play the role of both server and client. Afterwards, if any peer asks a file, first it should search the file and when it receives the file's information (such as the destination peer host name), it should use that information to connect to the peers and then, download the file. The PNRPManager class is responsible to Register and Resolve peers.
The Register() method registers the peer in the cloud and accepts a list of PeerInfo type as its input argument.
public List Register(){ List registerdPeer = new List (); 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;}
The Start() method registers peers and the Stop() method unregisters the peer from a specified cloud. For getting access to a peer's information, the peer should be resolved. When the peer gets resolved, some information as though peer host name, Classifier, port can be accessible. The ResolveByPeerHostName method can resolve a peer by its host name and returns a list of PeerInfo type.
public List 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 foundPeers = new List (); 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); } } }
After resolving, there are a list of peers who appear as an EndPointCollection and if we use a foreach loop, we can access each peer as an endpoint.
public List 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 foundPeers = new List (); 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); } } }
FileTransferServiceHost class makes each peer as a server host to provide required files to another peers. This class uses TCP protocol for transferring the data between peers. The DoHost() method gets an address, based on the peer host name, then adds an interface who applied the ServiceContract attribute. Therefore each peer publishes a service to the external world in order to make its methods accessible across service. (In this case, methods are TransferFile and TransferFileByHash.)
sealed class FileTransferServiceHost{ public void DoHost(List 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);}
If a client (peer) wants to get access a method of the another peer, it should use the Channels to reach this ability. This part has been coded in FileTransferServiceClientClass class as follows:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.Single, UseSynchronizationContext = false)]class FileTransferServiceClientClass : System.ServiceModel.ClientBase { 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); }}
This point can be useful to know that each instance of this application registers itself as a peer and then starts to setting up a WCF service using the FileTransferServiceHost as below:
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);}
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);}
Download Manager Layer
This Layer pledges to manage all activities behind the downloading tasks such as Managing Download processes and exceptions, downloading files, slabbing the requested file into the several pieces and then downloading each part asynchronously, searching the file based on its hash id or its name and ultimately, making a shared folder and storing the downloaded file into it.
The Search() class provides a search engine to look for the demanded file across the service which runs into the server layer. As I mentioned formerly, the server publishes a service provides some public information to the applicator peers (such as file name, peer host name, file type). I will expatiate the server layer's code during the next paragraphs as well.
public List Search(string searchPattern){ FileServer.FilesServiceClient fileServiceClient = new FileServer.FilesServiceClient(); List filesList = new List (); 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;}
The vital part of this layer is FileTransferManager class which manages the file transferring process. It consists of all the needed methods for downloading a whole or a part of a file. The UI layer calls the Download() method of this class. This method starts a task which uses the StartDownload method as its action. Afterward the StartDownload method is called. Across this method, the file is requested based on its part number which is generated based on a constant value(10240).
public void Download(Entities.File fileSearchResult){ //var action =new Action
As you observe, the StartDownload method calls downloadFilePart method. This method calls the GetFile method of TransferEngine class which is created by factory class.
public void Download(Entities.File fileSearchResult){ //var action =new Action(searchForSameFileBaseOnHash); //Task searchForSameFileBaseOnHashTask = new Task(action, fileSearchResult); //searchForSameFileBaseOnHashTask.Start(); var downloadAction = new Action(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 });}
So, let's take a glimpse over the factory class. This class uses the DLL of TransferEngine layer and manufactures a new instance of the required engine (such as: Search Engine or Transfer Engine). It takes a DLL file path and loads it ,then utilizes its methods. We use this kind of access to a class library due to this rational reason that two classes cannot have refer to each other in a same time. As you see, it uses the channels to access the server's methods to download the file.
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(); }..................................................................
In this class, I used a string as the DLL path but it is wrong and makes lots of problems (for instance, for each user, we have to set the file path again and it is too stupid). Then the right style is using a method who returns the application's folder path.
As it was mentioned, this layer is a pretty important component of this project and there are lots of issues that can be employed over it(For future releases).
Welcome to the last explained layer in this article. If I want to explain this layer, I should say it is just a WCF service that provides some methods to search between files(shared across peers). This layer utilizes the Entity Framework as the ORM for unifying the way of querying over database. If you open the edmx file, you will see something as below which is a schema of the database construction:
Based on above design, each peer can be related with the several files (One to Many relationship) and it exactly is the stuff that we expect it. This construction is pretty simple but it will be pretty elaborate when we want to develop a more complex system (which is the main goal of this project in the next releases). Anyhow, as I said, this layer plays as a WCF service role. This vital role comes to realize across the FilesService class as below:
public class FilesService{ private FreeFilesEntitiesContext _freeFilesObjectContext=new FreeFilesEntitiesContext(); [OperationContract] public void AddFiles(List 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 SearchAvaiableFiles(string fileName) { FileRepository fileRepository = new FileRepository (_freeFilesObjectContext as FreeFilesServerConsole.IUnitOfWork); return internalFileToEntityFile(fileRepository.SearchAvaiableFiles(fileName)); } public void SaveFile() { _freeFilesObjectContext.Save(); } . . . .
The implementation of file searching (and the other methods related to file activities such as adding a file and its related peer) is observable through the FileRepository class.
class FileRepository:IFilesRepository{ private FreeFilesEntitiesContext _freeFilesObjectContext; public FileRepository(IUnitOfWork unitOfWork) { _freeFilesObjectContext = unitOfWork as FreeFilesEntitiesContext; } public List 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 List = new List (); 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 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(); }}
As you see, this class uses the Unit Of Work pattern to gather all transactions in just one transaction.
class FileRepository:IFilesRepository{ private FreeFilesEntitiesContext _freeFilesObjectContext; public FileRepository(IUnitOfWork unitOfWork) { _freeFilesObjectContext = unitOfWork as FreeFilesEntitiesContext; } public List 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 List = new List (); 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 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)将所有的任务集合到一个任务中。
Indeed it comes with two important benefits: in-memory updates and unifying the various transactions in just one. For more details, I suggest you read article since I found it pretty handy.
Another considerable class is the ServiceInitializer. It hosts the service and makes it accessible to the external world (Peers) using the config file's values.
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(); }}
This service can be available when you run the FreeFilesServerConsole project. Then, you will see a message like the below figure which announces the WCF service starting status. Afterwards, you can search or share your desired files.
Using The Code (How To Run and Debug This Application)
Using this code is not as simple as you may have imagined since you should run it across at least 2 computers which are related together through a same network, however you will be able to run and test it if you read the below lines carefully.
For running this application, first of all, you should be connected to a network and your firewall ought to be deactivated. Then, check your network and firewall. Afterwards, for running the code, follow the below steps:
Install the attached database or use the Generate Database from Edmx file option in Visual Studio on the server and set the right server configurations on the FreeFileServerConsole application
Run the FreeFileServerConsole project and wait until it writes the service running success message
Open the project into 2 different networked computers and run the Windows Form application project
Share a file and wait until it demonstrates the "Done" Message
Search the file and after it is found, click on the name of the file on GridView in order to start download. There still is no progress bar for showing the download but this feature will be added in the near future and next release
Noticeable Point: You can use just one computer to do all of these steps but you should open two instances of Visual Studio and after running one of them, you should change the port which is set through the code and set it again manually, else you will encounter an error while you are downloading the file.
I think there are no other important details to talk about it for running this application successfully, unless I have forgotten some tiny points. In case of any problem, you can ask your question in the comments part of this page.
The primitive version is pretty simple in both of its appearance and features. When you setup all parts of application, in kind there is a shared file, you can search it and see the results in a simple GridView.
You can share a desired file as well by clicking on the "Share File" button. The current sharing style is too simple. In the next version, one of the most momentous features is the ability to copy a bunch of files in a shared folder which is accessible to the others or the capability to share a folder not just one file. Nevertheless, the current appearance is just like this:
Moreover, the current appearance ought to change to a better one which is more handy and user friendly to the end user.