Создание приложений P2P

160

Общая инфраструктура

Теперь, когда известно, что собой представляет технология P2P, и какие технологии доступны разработчикам приложений .NET для реализации приложений P2P, пришла пора посмотреть, как создавать такие приложения. Из приведенного ранее материала понятно, что в любом приложении P2P должен применяться протокол PNRP для публикации, распространения и преобразования имен равноправных участников. Поэтому в первую очередь здесь будет показано, как достичь этого в .NET, а затем — каким образом использовать службу PNM в качестве каркаса для приложения P2P. Последнее может быть выгодно, поскольку в случае применения PNM реализовать собственные механизмы обнаружения не понадобится.

Перед изучением этих тем сначала необходимо ознакомится с классами, которые предлагаются в следующих пространствах имен: System.Net.PeerToPeer и System.Net.PeerToPeer.Collaboration. Для работы с этими классами нужно обязательно ссылаться на сборку System.Net.dll.

Классы в пространстве имен System.Net.PeerToPeer инкапсулируют API-интерфейс для PNRP и позволяют взаимодействовать со службой PNRP. Их можно применять для решения двух основных задач: регистрация имен равноправных участников и преобразование имен равноправных участников.

Регистрация имен равноправных участников

Для регистрации имени равноправного участника потребуется выполнить перечисленные ниже шаги:

  1. Создайте защищенное или незащищенное имя равноправного участника с определенным классификатором.

  2. Настройте процесс регистрации этого имени, предоставив следующие сведения:

    • Номер порта TCP.

    • Облако или облака, в которых должно быть зарегистрировано имя участника (если не указано, PNRP будет регистрировать имя равноправного участника во всех доступных облаках).

    • Комментарий длиной до 39 символов.

    • Дополнительные данные объемом до 4096 байт.

    • Информация о том, должны ли для имени равноправного участники автоматически генерироваться конечные точки (поведение по умолчанию, при котором конечные точки автоматически генерируются на основе IP-адреса или адресов равноправного члена и номера порта, если таковой указан).

    • Коллекция конечных точек.

  3. Зарегистрируйте имя равноправного участника в локальной службе PNRP.

После выполнения третьего шага имя равноправного члена становится доступным для всех соседей в выбранном облаке (или облаках). Регистрация равноправного члена продолжается то тех пор, пока не будет прекращена явным образом, или до тех пор, пока не будет завершен процесс, зарегистрировавший имя равноправного участника.

Для создания имени равноправного участника применяется класс PeerName. Экземпляр этого класса создается из строкового представления идентификатора P2P ID в форме авторитетный_источник.классификатор, либо из строки классификатора и типа PeerNameType. Можно использовать как тип PeerNameType.Secured, так и тип PeerNameType.Unsecured. Например:

PeerName pn = new PeerName("Peer classifier", PeerNameType.Secured);

Поскольку в незащищенном имени равноправного участника значением авторитетного источника является 0, следующие строки кода эквивалентны:

PeerName pn = new PeerName("Peer classifier", PeerNameType.Unsecured);
PeerName pn = new PeerName("0.Peer classifier");

После создания экземпляр PeerName, можно использовать вместе с номером порта для инициализации объекта PeerNameRegistration:

PeerNameRegistration pnr = new PeerNameRegistration(pn, 8080);

В качестве альтернативного варианта, можно установить для объекта PeerNameRegistration, создаваемого с использованием его параметра по умолчанию, свойство PeerName и (необязательно) свойство Port. Кроме того, желаемый экземпляр Cloud можно передать либо в третьем параметре конструктору PeerNameRegistration, либо указать с помощью свойства Cloud. Экземпляр Cloud можно получить либо из имени облака, либо с применением одного из следующих статических членов класса Cloud:

Cloud.Global

Это статическое свойство позволяет получить ссылку на глобальное облако, которое в зависимости от конфигурации политики равноправных участников может быть и приватным глобальным облаком.

Cloud.AllLinkLocal

Это статическое поле позволяет получить облако, содержащее все облака локальных соединений, доступные для данного равноправного участника.

Cloud.Available

Это статическое поле позволяет получить облако, содержащее все облака, доступные для данного равноправного участника, т.е. локальные и глобальные (если таковые имеются).

После создания экземпляра PeerNameRegistration можно при желании установить его свойства Comment и Data. При этом следует помнить об ограничениях этих свойств. При попытке установить для свойства Comment строку длиной более 39 символов Unicode, будет сгенерировано исключение PeerToPeerException, а при попытке установить для свойства Data байтовый массив размером более 4096 байт — исключение ArgumentOutOfRangeException.

Можно также с помощью свойства EndPointCollection добавить конечные точки. Это свойство представляет собой коллекцию типа System.Net.IPEndPointCollectionс объектами System.Net.IPEndPoint. В случае применения свойства EndPointCollection может понадобиться установить свойство UseAutoEndPointSelection в false, чтобы предотвратить автоматическую генерацию конечных точек.

После того как все готово к регистрации имени равноправного участника, можно вызвать метод PeerNameRegistration.Start(). Для удаления записи о регистрации имени равноправного члена из службы PNRP служит метод PeerNameRegistration.Stop().

Ниже приведен пример регистрации защищенного имени равноправного участника вместе с комментарием:

PeerName pn = new PeerName("Peer classifier", PeerNameType.Unsecured);
PeerNameRegistration pnr = new PeerNameRegistration(pn, 8080);
pnr.Comment = "Комментарий";
pnr.Start();

Преобразование имен равноправных участников

Для преобразования имени равноправного участника должны быть выполнены следующие шаги:

  1. Сгенерируйте имя равноправного члена на основе известного идентификатора P2P, либо идентификатора P2P ID, полученного посредством операции обнаружения.

  2. Воспользуйтесь распознавателем (resolver) для преобразования имени равноправного участника и получения коллекции записей с именами равноправных членов. Распознаватель можно ограничить отдельным облаком и/или максимальным количеством возвращаемых результатов.

  3. Из любой полученной записи с именем равноправного участника извлеките само имя, а также необходимую информацию о конечной точке, комментарии и дополнительные данные, если таковые присутствуют.

Подобно процессу регистрации имени равноправного участника, этот процесс начинается с создания объекта PeerName. Отличие состоит в том, что здесь используется имя равноправного участника, которое уже было зарегистрировано одним или более удаленными участниками. Простейшим способом для получения списка активных членов в локальном облаке является регистрация незащищенного имени для каждого равноправного участника с одним и тем же классификатором и применение этого имени на стадии преобразования.

Однако для глобальных облаков пользоваться такой стратегией не рекомендуется, поскольку незащищенные имена могут быть легко подделаны. Для преобразования имен равноправных участников используется класс PeerNameResolver. Получив в распоряжение экземпляр этого класса, можно выбрать, как должно выполняться преобразование — синхронно с применением метода Resolve() или же асинхронно с помощью метода ResolveAsync().

Метод Resolve() можно вызывать с указанием как лишь одного параметра PeerName, так и дополнительно экземпляра Cloud, в котором должно производиться преобразование, максимального количества возвращаемых записей с именами равноправных участников в виде целого числа, или того и другого вместе. Этот метод возвращает экземпляр PeerNameRecordCollection, представляющий собой коллекцию объектов PeerNameRecord.

Например, ниже показан пример преобразования незащищенного имени равноправного участника во всех локальных облаках с возвратом максимум 5 результатов:

PeerName pn = new PeerName("0.Peer classifier");
PeerNameResolver pnres = new PeerNameResolver();
PeerNameRecordCollection pnrc = pnres.Resolve(pn, Cloud.AllLinkLocal, 5);

Метод ResolveAsync() предусматривает использование стандартной схемы вызова асинхронного метода, а именно — передачу уникального объекта userState, после чего ожидание поступления событий ResolveProgressChanged, свидетельствующих об обнаружении равноправных участников и выполнении преобразования, и события ResolveCompleted, свидетельствующего об окончании процесса обнаружения участников и преобразования.

Метод ResolveAsyncCancel()позволяет отменить любой незавершенный асинхронный запрос. В обработчиках события ResolveProgressChanged используется параметр аргументов события ResolveProgressChangedEventArgs, унаследованный от стандартного класса System.ComponentModel.ProgressChangedEventArgs.

Свойство PeerNameRecord объекта аргумента события, получаемого в обработчике событий, можно применять для получения ссылки на запись с именем равноправного участника, которая была обнаружена.

Аналогичным образом для события ResolveCompleted требуется обработчик, принимающий параметр типа ResolveCompletedEventArgs, который наследуется от AsyncCompletedEventArgs. Этот тип имеет параметр PeerNameRecordCollection, который можно использовать для получения полного списка записей с именами равноправных членов, которые были обнаружены.

В следующем коде показан пример реализации обработчиков для этих событий:

private pnres_ResolveProgressChanged(object sender, ResolveProgressChangedEventArgs e)
{
     // Использование e.ProgressPercentage (унаследованного от базовых аргументов события)
     // Обработка PeerNameRecord из e.PeerNameRecord
}

private pnres_ResolveCompleted(object sender, ResolveCompletedEventArgs e)
{
     // Проверка наличия e.IsCancelled и e.Error (унаследованных
     // от базовых аргументов события)
     // Обработка PeerNameRecordCollection из e.PeerNameRecordCollection
}

После получения одного или более объектов PeerNameRecord можно переходить к их обработке. Этот класс PeerNameRecord предоставляет в распоряжение свойства Comment и Data для изучения комментариев и дополнительных данных, которые были добавлены при регистрации имени равноправного участника (если были), свойство PeerName для извлечения объекта PeerName и записи с именем участника и, что наиболее важно, свойство EndPointCollection.

Как и в случае с PeerNameRegistration, здесь это свойство тоже представляет собой коллекцию типа System.Net.IPEndPointCollection, содержащую объекты System.Net.IPEndPoint. Эти объекты можно использовать для подключения к предоставляемым равноправным участником конечным точкам любым желаемым образом.

Безопасный доступ кода в System.Net.PeerToPeer

Пространство имен System.Net.PeerToPeer также включает два следующих класса, которые можно использовать вместе с моделью CAS (Code Access Security — безопасный доступ кода): PnrpPermission, унаследованный от класса CodeAccessPermission и PnrpPermissionAttribute, унаследованный от класса CodeAccessSecurityAttribute.

Эти классы удобно применять для обеспечения возможности настройки доступа PNRP с помощью полномочий обычным для CAS образом.

Пример приложения P2P

Давайте рассмотрим пример приложения P2P, использующего графический интерфейс WPF, в котором для конечной точки равноправного участника используется служба WCF.

Конфигурационные настройки этого приложения содержатся в конфигурационном файле App.config и указывают, как должно выглядеть имя равноправного участника и какой порт должен прослушиваться:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="username" value="Вася Пупкин" />
    <add key="port" value="4590" />
  </appSettings>
</configuration>

После сборки этого приложения можно приступить к его тестированию, либо скопировав его на другие компьютеры в локальной сети и запустив все его экземпляры, либо запустив множество его экземпляров на одном компьютере. В последнем варианте нужно будет поменять используемый для каждого экземпляра порт, внеся соответствующие изменения в отдельные конфигурационные файлы (понадобится скопировать содержимое каталога Debug на свой локальный компьютер и по очереди отредактировать каждый конфигурационный файл).

В обоих вариантах результаты будут выглядеть яснее, если изменить также имя пользователя в каждом экземпляре.

После запуска всех экземпляров приложения можно щелкнуть на кнопке Refresh (Обновить), чтобы получить список доступных равноправных участников асинхронным образом. Обнаружив в этом списке нужного участника, можно с помощью щелчка на кнопке Message отправить ему стандартное сообщение.

На рисунке ниже показан пример того, как приложение выглядит в действии при запуске четырех его экземпляров на одной машине. На рисунке видно, что один равноправный участник только что отправил сообщение другому равноправному участнику, и оно отобразилось в диалоговом окне:

Приложение P2PSampleв действии

Итак, создайте новый проект приложения WPF по имени P2P. Изначально нужно будет добавить в проект ссылки на сборки System.Configuration.dll, System.Net.dll, System.Runtime.Serialization.dll, System.ServiceModel.dll, а также указать пространства имен, которые мы будем использовать:

using System.Net;
using System.Net.PeerToPeer;
using System.ServiceModel;
using System.Configuration;

Добавьте в проект XML-файл App.config, который описан выше, а также следующие файлы классов (предоставляют информацию о пире, создают WCF-контракт):

// Файл PeerEntry.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.PeerToPeer;

namespace P2P
{
    class PeerEntry
    {
        public PeerName PeerName { get; set; }
        public IP2PService ServiceProxy { get; set; }
        public string DisplayString { get; set; }
        public bool ButtonsEnabled { get; set; }
    }
}
// Файл P2PService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace P2P
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class P2PService : IP2PService
    {
        private MainWindow hostReference;
        private string username;

        public P2PService(MainWindow hostReference, string username)
        {
            this.hostReference = hostReference;
            this.username = username;
        }

        public string GetName()
        {
            return username;
        }

        public void SendMessage(string message, string from)
        {
            hostReference.DisplayMessage(message, from);
        }
    }
}
// Файл IP2PService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace P2P
{
    [ServiceContract]
    public interface IP2PService
    {
        [OperationContract]
        string GetName();

        [OperationContract(IsOneWay = true)]
        void SendMessage(string message, string from);
    }
}

Большая часть работы в этом приложении происходит в обработчике события Window_Loaded() окна MainWindow. Ниже показана XAML-разметка окна и исходный код приложения:

<Window x:Class="P2P.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:P2P="clr-namespace:P2P"
        Title="MainWindow" Height="400" Width="400" Loaded="Window_Loaded" Closing="Window_Closing">
    <Window.Resources>
        <LinearGradientBrush x:Key="bevelBrush" EndPoint="0.369,-1.362" StartPoint="0.631,2.362">
            <GradientStop Color="#FF001E56" Offset="0"/>
            <GradientStop Color="#FFFFFFFF" Offset="1"/>
        </LinearGradientBrush>
        <DataTemplate x:Key="PeerEntryDataTemplate">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="100" />
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
                <Rectangle RadiusX="10" RadiusY="10" Grid.ColumnSpan="2" Stroke="{DynamicResource bevelBrush}" StrokeThickness="4" >
                    <Rectangle.Fill>
                        <LinearGradientBrush EndPoint="0.369,-1.362" StartPoint="0.631,2.362">
                            <GradientStop Color="#FF1346A6" Offset="0"/>
                            <GradientStop Color="#FF85ADF6" Offset="1"/>
                        </LinearGradientBrush>
                    </Rectangle.Fill>
                    <Rectangle.BitmapEffect>
                        <BlurBitmapEffect Radius="3"/>
                    </Rectangle.BitmapEffect>
                </Rectangle>
                <TextBlock Margin="10" Text="{Binding Path=DisplayString}" Padding="4" TextWrapping="Wrap" Width="150" Opacity="0.995" FontFamily="Calibri" FontSize="14" Foreground="#FF8ED1C3" >
                    <TextBlock.Background>
                        <RadialGradientBrush>
                            <GradientStop Color="#FF000000" Offset="0"/>
                            <GradientStop Color="#FF3C3C3C" Offset="1"/>
                        </RadialGradientBrush>
                    </TextBlock.Background>
                </TextBlock>
                <Rectangle RadiusX="6" RadiusY="6" Margin="8" Fill="{x:Null}" StrokeThickness="2" >
                    <Rectangle.Stroke>
                        <LinearGradientBrush EndPoint="0.631,2.362" StartPoint="0.369,-1.362">
                            <GradientStop Color="#FF001E56" Offset="0"/>
                            <GradientStop Color="#FFFFFFFF" Offset="1"/>
                        </LinearGradientBrush>
                    </Rectangle.Stroke>
                </Rectangle>
                <StackPanel Grid.Column="1">
                    <Button Name="MessageButton" Margin="10,10,10,10" Height="50" IsEnabled="{Binding Path=ButtonsEnabled}" Content="Message" BorderBrush="{DynamicResource bevelBrush}"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Window.Resources>

    <Window.Background>
        <LinearGradientBrush EndPoint="0.444,-0.183" StartPoint="0.778,1.12">
            <GradientStop Color="#FF6199A5" Offset="0"/>
            <GradientStop Color="#FFFFFFFF" Offset="1"/>
        </LinearGradientBrush>
    </Window.Background>

    <StackPanel>
        <Button Name="RefreshButton" Click="RefreshButton_Click">Обновить</Button>
        <ListBox Name="PeerList" ItemTemplate="{DynamicResource PeerEntryDataTemplate}" ButtonBase.Click="PeerList_Click" Background="{x:Null}" BorderBrush="{x:Null}">
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="Margin" Value="10" />
                    <Setter Property="HorizontalAlignment" Value="Center" />
                </Style>
            </ListBox.ItemContainerStyle>
            <P2P:PeerEntry DisplayString="Обновите, чтобы увидеть пиров." ButtonsEnabled="False" />
        </ListBox>
    </StackPanel>
</Window>
public partial class MainWindow : Window
{
        private P2PService localService;
        private string serviceUrl;
        private ServiceHost host;
        private PeerName peerName;
        private PeerNameRegistration peerNameRegistration;

      public MainWindow()
      {
         InitializeComponent();
      }

      private void Window_Loaded(object sender, RoutedEventArgs e)
      {
          // Получение конфигурационной информации из app.config
         string port = ConfigurationManager.AppSettings["port"];
         string username = ConfigurationManager.AppSettings["username"];
         string machineName = Environment.MachineName;
         string serviceUrl = null;

         // Установка заголовка окна
         this.Title = string.Format("P2P приложение - {0}", username);

         //  Получение URL-адреса службы с использованием адресаIPv4 
         //  и порта из конфигурационного файла
         foreach (IPAddress address in Dns.GetHostAddresses(Dns.GetHostName()))
         {
            if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
            {
                  serviceUrl = string.Format("net.tcp://{0}:{1}/P2PService", address, port);
                  break;
            }
         }

         // Выполнение проверки, не является ли адрес null
         if (serviceUrl == null)
         {
             // Отображение ошибки и завершение работы приложения
             MessageBox.Show(this, "Не удается определить адрес конечной точки WCF.", "Networking Error", 
                 MessageBoxButton.OK, MessageBoxImage.Stop);
            Application.Current.Shutdown();
         }

         // Регистрация и запуск службы WCF
         localService = new P2PService(this, username);
         host = new ServiceHost(localService, new Uri(serviceUrl));
         NetTcpBinding binding = new NetTcpBinding();
         binding.Security.Mode = SecurityMode.None;
         host.AddServiceEndpoint(typeof(IP2PService), binding, serviceUrl);
         try
         {
            host.Open();
         }
         catch (AddressAlreadyInUseException)
         {
            // Отображение ошибки и завершение работы приложения
             MessageBox.Show(this, "Не удается начать прослушивание, порт занят.", "WCF Error", 
                MessageBoxButton.OK, MessageBoxImage.Stop);
            Application.Current.Shutdown();
         }

         // Создание имени равноправного участника (пира)
         peerName = new PeerName("P2P Sample", PeerNameType.Unsecured);

         // Подготовка процесса регистрации имени равноправного участника в локальном облаке
         peerNameRegistration = new PeerNameRegistration(peerName, int.Parse(port));
         peerNameRegistration.Cloud = Cloud.AllLinkLocal;

         // Запуск процесса регистрации
         peerNameRegistration.Start();
      }

      private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
      {
         // Остановка регистрации
         peerNameRegistration.Stop();

         // Остановка WCF-сервиса
         host.Close();
      }

      private void RefreshButton_Click(object sender, RoutedEventArgs e)
      {
         // Создание распознавателя и добавление обработчиков событий
         PeerNameResolver resolver = new PeerNameResolver();
         resolver.ResolveProgressChanged += 
             new EventHandler<ResolveProgressChangedEventArgs>(resolver_ResolveProgressChanged);
         resolver.ResolveCompleted += 
             new EventHandler<ResolveCompletedEventArgs>(resolver_ResolveCompleted);

         // Подготовка к добавлению новых пиров
         PeerList.Items.Clear();
         RefreshButton.IsEnabled = false;

         // Преобразование незащищенных имен пиров асинхронным образом
         resolver.ResolveAsync(new PeerName("0.P2P Sample"), 1);
      }

      void resolver_ResolveCompleted(object sender, ResolveCompletedEventArgs e)
      {
         // Сообщение об ошибке, если в облаке не найдены пиры
         if (PeerList.Items.Count == 0)
         {
            PeerList.Items.Add(
               new PeerEntry
               {
                  DisplayString = "Пиры не найдены.",
                  ButtonsEnabled = false
               });
         }
         // Повторно включаем кнопку "обновить"
         RefreshButton.IsEnabled = true;
      }

      void resolver_ResolveProgressChanged(object sender, ResolveProgressChangedEventArgs e)
      {
         PeerNameRecord peer = e.PeerNameRecord;

         foreach (IPEndPoint ep in peer.EndPointCollection)
         {
            if (ep.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
            {
               try
               {
                  string endpointUrl = string.Format("net.tcp://{0}:{1}/P2PService", ep.Address, ep.Port);
                  NetTcpBinding binding = new NetTcpBinding();
                  binding.Security.Mode = SecurityMode.None;
                  IP2PService serviceProxy = ChannelFactory<IP2PService>.CreateChannel(
                      binding, new EndpointAddress(endpointUrl));
                  PeerList.Items.Add(
                     new PeerEntry
                     {
                        PeerName = peer.PeerName,
                        ServiceProxy = serviceProxy,
                        DisplayString = serviceProxy.GetName(),
                        ButtonsEnabled = true
                     });
               }
               catch (EndpointNotFoundException)
               {
                  PeerList.Items.Add(
                     new PeerEntry
                     {
                        PeerName = peer.PeerName,
                        DisplayString = "Неизвестный пир",
                        ButtonsEnabled = false
                     });
               }
            }
         }
      }

      private void PeerList_Click(object sender, RoutedEventArgs e)
      {
          // Убедимся, что пользователь щелкнул по кнопке с именем MessageButton
         if (((Button)e.OriginalSource).Name == "MessageButton")
         {
            // Получение пира и прокси, для отправки сообщения
            PeerEntry peerEntry = ((Button)e.OriginalSource).DataContext as PeerEntry;
            if (peerEntry != null && peerEntry.ServiceProxy != null)
            {
               try
               {
                  peerEntry.ServiceProxy.SendMessage("Привет друг!", ConfigurationManager.AppSettings["username"]);
               }
               catch (CommunicationException)
               {
                  
               }
            }
         }
      }

      internal void DisplayMessage(string message, string from)
      {
          // Показать полученное сообщение (вызывается из службы WCF)
         MessageBox.Show(this, message, string.Format("Сообщение от {0}", from), 
             MessageBoxButton.OK, MessageBoxImage.Information);
      }
}

В обработчике события Window_Loaded() сначала производится загрузка конфигурационной информации и установка в заголовке окна имени пользователя.

Далее с использованием адреса хоста равноправного участника и сконфигурированного порта определяется конечная точка, в которой должна предоставляться служба WCF. Привязкой этой службы будет NetTcpBinding, поэтому в URL-адресе конечной точки применяется протокол net.tcp.

Затем производится проверка полученного URL-адреса конечной точки, после чего выполняется регистрация и запуск службы WCF. Обратите внимание на использование одиночного (singleton) экземпляра класса службы для налаживания простых коммуникаций между приложением-хостом и службой (для отправки и получения сообщений). Кроме того, для простоты режим безопасности в конфигурации привязки отключен.

После щелчка на кнопке Обновить в обработчике события RefreshButton_Click() вызывается метод PeerNameResolver.ResolveAsync() для обнаружения соседних равноправных участников асинхронным образом.

Остальная часть кода отвечает за отображение и взаимодействие с соседними равноправными участниками; ее можно изучить самостоятельно.

Предоставление конечных точек WCF через облака P2P является замечательным способом для обеспечения возможности установки местонахождения служб внутри предприятия, а также налаживания связи между равноправными участниками сети подобно тому, как было сделано в рассмотренном примере.

Пройди тесты
Лучший чат для C# программистов