.Net Developer
0,0
рейтинг
1 июля 2014 в 23:03

Разработка → Как я писал свой чат

Привет, Хабр!
В статье я написал, о том как разрабатывал чат. О его архитектуре и о технических решениях принятых в ходе его разработки.

Чат представляет собой клиент-серверное приложение с элементами p2p.
С поддеркжой:
  • Личных сообщений.
  • Комнат.
  • Передачи файлов.
  • Голосового чата.




Исходный код проекта: GitHub

Итак, понеслась.



Модель


  1. Данные и их синхронизация.
  2. API.
  3. Запись и воспроизведение звука.


Данные и их синхронизация.

В начале когда я писал первую версию, я сразу же написал асинхронную версию клиента и сервера. Но, почему-то, напрочь забыл про то, что данные нужно синхронизировать. И так-как серьезной нагрузки чат никогда не испытывал, то я понял это только после введения в чат передачи файлов. После этого сразу все вспомнилось и везде было вставлено куча локов. Что разумеется не было лучшим решением. Если сказать точнее, то это было всего лишь чуть лучше, чем программа без синхронизации.

Сейчас же на клиенте и на сервере используется единый механизм доступа к данным. Блокируется полностью модель. Должен сказать что для сервера это не самое удачное решение.

Идея достаточно простая: есть контекст, который приватным статическим полем содержит модель. В конструкторе он, вызывает Monitor.Enter. На саму модель, либо на отдельный объект синхронизации. Так же контекст реализует интерфейс IDisposable и в методе Dispose он эту эту модель освобождает, вызывая метод Monitor.Exit.

Обобщенный класс используемый как для сервера, так и для клиента. В примере модель содержится не в самом контексте а в классе его создающем.
Код
  public abstract class ModelContext<TModel> :
    IDisposable
  {
    #region consts
    private const int TimeOut = 10000;
    #endregion

    #region fields
    private static object syncObject = new object();
    protected TModel model;
    #endregion

    #region initialization
    protected ModelContext(TModel initialModel)
    {
      if (!Monitor.TryEnter(syncObject, TimeOut))
        throw new InvalidOperationException("model lock timeout");

      model = initialModel;
    }

    public void Dispose()
    {
      model = default(TModel);

      Monitor.Exit(syncObject);
    }
    #endregion
  }
}



В результате, для доступа к данным хочешь-не хочешь их нужно блокировать, и уже не задумываешься о синхронизации. Главное не забывать использовать конструкцию using. Для сервера это не является лучшим решением т.к. половина команд работают с 2умя пользователями максимум, а блокируются в результате — все.

Контекст в программе может создавать только одна сущность (ServerModel — (неожиданно) для сервера, и ClientModel — для клиента). Она представляет собой класс содержащий статическую приватную модель (саму себя), API а также клиентское соединение и пир — для клиентской модели или сервер для серверной. (API, клиент и т.д. содержатся как статические поля). Также клиентская модель, в отличии от серверной, содержит еще и события. На которые будет подписан пользовательский интерфейс. В общем эти классы выступают как основные для доступа к чему либо.
В качестве примера приведу серверную модель (она поменьше). Обратить внимание следует на метод Get() создающий контекст.

Код
  public class ServerModel
  {
    #region static model
    private static ServerModel model;

    /// <summary>
    /// Серверный API
    /// </summary>
    public static IServerAPI API { get; private set; }

    /// <summary>
    /// Сервер
    /// </summary>
    public static AsyncServer Server { get; private set; }

    /// <summary>
    /// Исользовать только с конструкцией using
    /// </summary>
    /// <example>using (var server = SeeverModel.Get()) { ... }</example>
    /// <returns>Возвращает и блокирует модель.</returns>
    public static ServerContext Get()
    {
      if (Interlocked.CompareExchange(ref model, null, null) == null)
        throw new ArgumentException("model do not inited yet");

      return new ServerContext(model);
    }
    #endregion

    #region consts
    public const string MainRoomName = "Main room";
    #endregion

    #region properties
    public Dictionary<string, Room> Rooms { get; private set; }
    public Dictionary<string, User> Users { get; private set; }
    #endregion

    #region constructor
    public ServerModel()
    {
      Users = new Dictionary<string, User>();
      Rooms = new Dictionary<string, Room>();

      Rooms.Add(MainRoomName, new Room(null, MainRoomName));
    }
    #endregion

    #region static methods
    public static bool IsInited
    {
      get { return Interlocked.CompareExchange(ref model, null, null) != null; }
    }

    public static void Init(IServerAPI api)
    {
      if (Interlocked.CompareExchange(ref model, new ServerModel(), null) != null)
        throw new InvalidOperationException("model already inited");

      Server = new AsyncServer("ServerErrors.log");
      API = api;
    }

    public static void Reset()
    {
      if (Interlocked.Exchange(ref model, null) == null)
        throw new InvalidOperationException("model not yet inited");

      if (Server != null)
      {
        Server.Dispose();
        Server = null;
      }

      API = null;
    }
    #endregion
  }



API

API в данном случае это логика чата. Она представляет собой класс хранящий в себе команды которые могут выполнятся на нашей стороне, и набор методов которые отправляют команды другой стороне (Клиенту, если рассматриваем себя со стороны сервера. Для клиента это сервер или другой пир). В методах содержатся наиболее сложные команды, либо просто часто используемые.

Работает вся эта система следующим образом: как только клиент или сервер принимает пакет данных, он передает его на анализ в API. (У сервера принимают сообщения его соединения, а они в свою очередь дергают один метод у сервера, о том что данные приняты). API просто считывает первые два байта сообщения и ищет у себя в словаре команду с нужным id, и возвращает ее. Или пустую команду, которая ничего не делает, если такого id нет. Дальше команде передается полученный пакет, и id приславшего его соединения и она выполняется.

Также API имеет свой интерфейс, изначально его не было. Появился после того как я решил написать другую его реализацию, предполагалось что это будет защищенное API. Но потом мне это просто стало не интересно, и я не на долго забросил проект. Месяца на два. После возвращения к нему мне уже не хотелось все это делать, и я занялся реализацией P2P.

Клиент, кстати, умеет сам выбирать API, который использует сервер, и если такового не имеется он отсоединяется от сервера и говорит что не поддерживает серверный API. Это реализовано достаточно просто — после того как сервер принял соединение он сразу же отправляет строку с названием своего API, а клиент собственно ожидает эту строку и устанавливает нужный интерфейс. Ну или не устанавливает, если такой не поддерживает. После этого действия уже идет апишный запрос регистрации пользователя на сервере.

Метод сервера обрабатывающего принятые пакеты:
Код
  public class DataReceivedEventArgs : EventArgs
  {
    public byte[] ReceivedData { get; set; }
    public Exception Error { get; set; }
  }

  public interface IServerAPICommand
  {
    void Run(ServerCommandArgs args);
  }

  public class ServerCommandArgs
  {
    public string ConnectionId { get; set; }
    public byte[] Message { get; set; }
  }

    private void DataReceivedCallBack(object sender, DataReceivedEventArgs e)
    {
      try
      {
        if (e.Error != null)
          throw e.Error;

        if (!isServerRunning)
          return;

        IServerAPICommand command = ServerModel.API.GetCommand(e.ReceivedData);
        ServerCommandArgs args = new ServerCommandArgs
        {
          Message = e.ReceivedData,
          ConnectionId = ((ServerConnection)sender).Id,
        };

        command.Run(args);
      }
      catch (Exception exc)
      {
        ServerModel.Logger.Write(exc);
      }
    }



Полный код класса API (в данном случае — серверного):
Код
  /// <summary>
  /// Класс реазиующий стандартное серверное API.
  /// </summary>
  public class StandardServerAPI : IServerAPI
  {
    /// <summary>
    /// Версия и имя данного API.
    /// </summary>
    public const string API = "StandartAPI v2.0";

    private Dictionary<ushort, IServerAPICommand> commandDictionary = new Dictionary<ushort, IServerAPICommand>();

    /// <summary>
    /// Создает экземпляр API.
    /// </summary>
    /// <param name="host">Сервер которому будет принадлежать данный API.</param>
    public StandardServerAPI()
    {
      commandDictionary.Add(ServerRegisterCommand.Id, new ServerRegisterCommand());
      commandDictionary.Add(ServerUnregisterCommand.Id, new ServerUnregisterCommand());
      commandDictionary.Add(ServerSendRoomMessageCommand.Id, new ServerSendRoomMessageCommand());
      commandDictionary.Add(ServerSendPrivateMessageCommand.Id, new ServerSendPrivateMessageCommand());
      commandDictionary.Add(ServerGetUserOpenKeyCommand.Id, new ServerGetUserOpenKeyCommand());
      commandDictionary.Add(ServerCreateRoomCommand.Id, new ServerCreateRoomCommand());
      commandDictionary.Add(ServerDeleteRoomCommand.Id, new ServerDeleteRoomCommand());
      commandDictionary.Add(ServerInviteUsersCommand.Id, new ServerInviteUsersCommand());
      commandDictionary.Add(ServerKickUsersCommand.Id, new ServerKickUsersCommand());
      commandDictionary.Add(ServerExitFormRoomCommand.Id, new ServerExitFormRoomCommand());
      commandDictionary.Add(ServerRefreshRoomCommand.Id, new ServerRefreshRoomCommand());
      commandDictionary.Add(ServerSetRoomAdminCommand.Id, new ServerSetRoomAdminCommand());
      commandDictionary.Add(ServerAddFileToRoomCommand.Id, new ServerAddFileToRoomCommand());
      commandDictionary.Add(ServerRemoveFileFormRoomCommand.Id, new ServerRemoveFileFormRoomCommand());
      commandDictionary.Add(ServerP2PConnectRequestCommand.Id, new ServerP2PConnectRequestCommand());
      commandDictionary.Add(ServerP2PReadyAcceptCommand.Id, new ServerP2PReadyAcceptCommand());
      commandDictionary.Add(ServerPingRequestCommand.Id, new ServerPingRequestCommand());
    }

    /// <summary>
    /// Версия и имя данного API.
    /// </summary>
    public string Name
    {
      get { return API; }
    }

    /// <summary>
    /// Извлекает команду.
    /// </summary>
    /// <param name="message">Пришедшее сообщение, по которому будет определена необходимая для извлекания команда.</param>
    /// <returns>Команда для выполнения.</returns>
    public IServerAPICommand GetCommand(byte[] message)
    {
      ushort id = BitConverter.ToUInt16(message, 0);

      IServerAPICommand command;
      if (commandDictionary.TryGetValue(id, out command))
        return command;

      return ServerEmptyCommand.Empty;
    }

    /// <summary>
    /// Напрямую соединяет пользователей.
    /// </summary>
    /// <param name="container"></param>
    public void IntroduceConnections(string senderId, IPEndPoint senderPoint, string requestId, IPEndPoint requestPoint)
    {
      using (var context = ServerModel.Get())
      {
        var content = new ClientWaitPeerConnectionCommand.MessageContent
        {
          RequestPoint = requestPoint,
          SenderPoint = senderPoint,
          RemoteInfo = context.Users[senderId],
        };

        ServerModel.Server.SendMessage(requestId, ClientWaitPeerConnectionCommand.Id, content);
      }
    }

    /// <summary>
    /// Посылает системное сообщение клиенту.
    /// </summary>
    /// <param name="nick">Пользователь получащий сообщение.</param>
    /// <param name="message">Сообщение.</param>
    public void SendSystemMessage(string nick, string message)
    {
      var sendingContent = new ClientOutSystemMessageCommand.MessageContent { Message = message };
      ServerModel.Server.SendMessage(nick, ClientOutSystemMessageCommand.Id, sendingContent);
    }

    /// <summary>
    /// Закрывает соединение.
    /// </summary>
    /// <param name="nick">Ник пользователя, соединение котрого будет закрыто.</param>
    public void CloseConnection(string nick)
    {
      ServerModel.Server.CloseConnection(nick);

      using (var server = ServerModel.Get())
      {
        foreach (string roomName in server.Rooms.Keys)
        {
          Room room = server.Rooms[roomName];

          if (!room.Users.Contains(nick))
            continue;

          room.Remove(nick);
          server.Users.Remove(nick);

          var sendingContent = new ClientRoomRefreshedCommand.MessageContent
          {
            Room = room,
            Users = room.Users.Select(n => server.Users[n]).ToList()
          };

          foreach (string user in room.Users)
          {
            if (user == null)
              continue;

            ServerModel.Server.SendMessage(user, ClientRoomRefreshedCommand.Id, sendingContent);
          }
        }
      }
    }
  }



Каждая команда реализует интерфейс команды. Для сервера IServerAPICommand, для клиента IClientAPICommand, на данном этапе их можно было бы свести к 1 интерфейсу, но мне этого делать почему то не хочется. Также она содержит свой Id и данные необходимые для ее выполнения, описывающееся классом MessageContent. Впрочем команде могут быть и не нужны данные. И она сама ответственна за то, что бы десериализовать набор байт в экземпляр класса.

Пример команды. В данному случае это команда добавления файла в комнату:
Код

  public interface IServerAPICommand
  {
    void Run(ServerCommandArgs args);
  }

  public class ServerCommandArgs
  {
    public string ConnectionId { get; set; }
    public byte[] Message { get; set; }
  }

  abstract class BaseCommand
  {
    protected static T GetContentFormMessage<T>(byte[] message)
    {
      using (MemoryStream messageStream = new MemoryStream(message))
      {
        messageStream.Position = sizeof(ushort);
        BinaryFormatter formatter = new BinaryFormatter();
        T receivedContent = (T)formatter.Deserialize(messageStream);
        return receivedContent;
      }
    }
  }

  class ServerAddFileToRoomCommand :
      BaseServerCommand,
      IServerAPICommand
  {
    public void Run(ServerCommandArgs args)
    {
      MessageContent receivedContent = GetContentFormMessage<MessageContent>(args.Message); //Извлекаем контент.

      if (receivedContent.File == null)
        throw new ArgumentNullException("File");

      if (string.IsNullOrEmpty(receivedContent.RoomName))
        throw new ArgumentException("RoomName");

      if (!RoomExists(receivedContent.RoomName, args.ConnectionId))
        return;

      using (var context = ServerModel.Get()) //Получаем доступ к модели, и блокируем ее
      {
        Room room = context.Rooms[receivedContent.RoomName];

        if (!room.Users.Contains(args.ConnectionId))
        {
          ServerModel.API.SendSystemMessage(args.ConnectionId, "Вы не входите в состав этой комнаты.");
          return;
        }

        if (room.Files.FirstOrDefault(file => file.Equals(receivedContent.File)) == null)
          room.Files.Add(receivedContent.File);

        var sendingContent = new ClientFilePostedCommand.MessageContent
        {
          File = receivedContent.File,
          RoomName = receivedContent.RoomName
        };

        //отправляем сообщения всем пользователям в комнате
        //в данном случае это сообщение о том что в комнату добавлен файл, как вы уже догадались
        foreach (string user in room.Users)
          ServerModel.Server.SendMessage(user, ClientFilePostedCommand.Id, sendingContent); 
      }
    }

    [Serializable]
    public class MessageContent
    {
      string roomName;
      FileDescription file;

      public string RoomName { get { return roomName; } set { roomName = value; } }
      public FileDescription File { get { return file; } set { file = value; } }
    }

    public const ushort Id = (ushort)ServerCommand.AddFileToRoom;
  }



Запись и воспроизведение звука.

Добавлением голосового чата занялся недавно, возможно во время публикации он все еще будет в демо версии. Но уже успел повозится с воспроизведением и записью звука.

Первым вариантом были WinApi функции waveIn* waveOut*. Это был самый простой вариант, поэтому начал с него. Но с ними не сложилось, т.к. на версии framework'a 3.5 неадекватно работал маршалинг на платформе x64 и при запуске приложение просто падало без каких либо исключений. При сборке под х86 все было нормально.

Дальше была попытка подключить DirectSound, но у него был найден свой баг с способом оповещения о завершении проигрывания куска данных. После гугления на эту тему выяснилось, что Mircosoft давно забросили DirectSound и работают с XAudio2. К тому же его использование привело бы к необходимости компиляции 2ух версий х86 и х64.

Так как мне не хотелось самому писать обертку для XAudio2, то я вспомнил про OpenAL. Для которого к тому же есть обертка (OpenTK), еще и с открытым исходным кодом. Из OpenTK был аккуратно вырезан только сама аудио библиотека. Которая сейчас и работает в программе.

Так как мне приходилось работать OpenGL ES 2, то и с OpenAL я подружился сразу. Особенное если учесть что по нему на официальном сайте OpenTK есть примеры.

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

Код можно посмотреть в ветке TCPChat\Engine\Audio\OpenAL.




Сеть


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

Это было сделано просто добавлением размера сообщения в его начало.
То есть пакет представляет из себя следующее:
Первые 4 байта — размер сообщения.
5-6 байт — идентификатор команды.
Остальные данные это сериализованный MessageContent.

Далее на одном форуме мне предложили ввести передачу файлов и голосовую связь. С файлами я справился почти сразу, правда в первой версии файлы передавались через сервер, что было вообще ужасно. После этого я задумался над тем, что хорошо было бы передавать их на прямую. Как вы знаете с проблемой NAT, это сделать не так то и просто.

Я долго возился и пытался реализовать обход NAT используя TCP, тогда бы не пришлось парится по поводу ненадежности UDP. С ним так ничего и не получилось. После чего было решено использовать UDP и технологию UDP hole punching.

А для начала надо было решить проблемы надежности. Как обычно бывает, я начал трудится над своим протоколом поверх UDP обеспечивающим мне надежную доставку сообщений. И он все таки у меня получился, но работал только локально. При его тестировании в реальных условиях он, видимо, настолько нагружал сеть, что у меня полностью зависал компьютер.

После этого я начал искать уже реализованные библиотеки и наткнулся на сатью на Хабре, где аналогичная проблема была решена с помощью Lidgren.Network. Она и была выбрана.

Обход NAT реализован на уровне API. Схема простая, нужно всего лишь, что бы пиры узнали реальные адреса, по которым их видит сервер. После этого один пир должен кинуть сообщение другому. Это сообщение возможно не дойдет, но создаст правило на роутере, что сообщения от того адреса, по которому оно было отправлено нужно доставлять именно этому компьютеру. После этого с помощью сервера другой пир узнает, что ему уже можно подключатся и, собственно, подключается.

И так, последовательность действий:
  1. Клиент 1 говорит серверу, что хочет подключится к Клиенту 2. (Команда ServerP2PConnectRequestCommand)
  2. Сервер делегирует свою задачу классу P2PService
  3. P2PService смотрит не подключались ли к нему уже такие клиенты и не знает ли он уже их адреса. Если нет — просит подключится тех кто подключен небыл (Команда ClientConnectToP2PServiceCommand)
  4. После их подключения, P2PService отправляет одному из них команду ожидания подключения. В данному случае это Клиент 2. (ClientWaitPeerConnectionCommand)
  5. Клиент получивший команду, также получает адрес который будет к нему подключатся, и отправляет на него сообщение. Начинает ожидать подключение и отправляет серверу команду о том, что готов принять соединение. (ServerP2PReadyAcceptCommand)
  6. После получения команды готовности, сервер говорит другому клиенту (Клиент 1), что тот может подключатся. (ClientConnectToPeerCommand)
  7. Связь между клиентами установлена.




Черными стрелками обозначены отправки команд. Красным инициализации Lidgren.Network соединений.

Весь этот алгоритм спрятан в AsyncPeer, и достаточно вызвать метод SendMessage, и если клиент не подключен, он сам подключится, и отправит сообщение. Либо сразу отправит, если уже подключен.

Команды голосовой связи инициируют соединение немного иначе. При создании голосовой комнаты сервер создает в комнате карту подключений. В которой записано куда должен подключится каждый пользователь в комнате. А команды воспроизведения голоса отправляются с помощью метода AsyncPeer.SendMessageIfConnected, который просто выкидывает сообщение, если соединения нет.




Пользовательский интерфейс.


Напоследок немного об интерфейсе программы.
Он разработан с помощью WPF и паттерна MVVM. Очень гибкая технология, правда в версии 3.5 немного сыровата, но тем не менее позволяет обойти сыроватые места. Как на пример некоторые свойства Command все еще не зависимые, и для них пришлось написать обертку CommandReference.

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

Еще приходилось вместо оберток использовать AttachedProperty, которые в свою очередь изменяют нужные.

Для оповещения интерфейса об изменениях было решено использовать событийную модель, при этому в событиях передается вся необходимая информация, чтобы отобразить изменения.

В прочем, об интерфейсах написать больше нечего, WPF как WPF.
Антон @Nirvano
карма
10,0
рейтинг 0,0
.Net Developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (20)

  • +10
    Такое ощущение, что статья из 2009 года. — .net 3.5 как-то не очень свежо, особенно для 2014 года.

    Сейчас скорее какой-нибудь SignalR для чатов на .net модно (без звука, видео и файлов)
    • +1
      Ну статья и не про .NET. И не про новые технологии, а про чат. Мне хотелось что бы он работала на win xp.
      • +9
        Мне хотелось что бы он работала на win xp.

        Такое ощущение, что статья из 2009 года.

  • +9
    Эхх, помню как я писал свой чат на Delphi.
    И TCP клиент/сервер, и чат ВК (через старые API) используя IndyHTTP, и всякое прочее:
    habrastorage.org/files/3ca/96a/bb2/3ca96abb2e0441fcbce4ee32f24a9f68.jpg
    habrastorage.org/files/d4c/aa6/53f/d4caa653f900495d92a7b37513edab42.png

    Времена… Чаты…
    Раньше и трава зеленее была… Эххх…
    • 0
      Гы, я не один такой :)

      Только без клиент-сервера, все клиенты были равноправны. Уж не помню, как точно работало, но это был велосипед по типу p2p. Даже аватарки поддерживал! Был 2004 год :)
      • 0
        А я вообще себя старым почуствовал. В 2002м свой первый клиент->сервер чат на VB6 с сетью через winsocks ActiveX-контрол. При этом сдувая код с различных ресурсов, не особенно стесняясь. И потом вставляя свой «копирайт» :) Эх время!
  • +5
    Напоминает FizzBuzz Enterprise Edition.

    Довольно простая и наглядная задача была, но в данном исполнении актуальный код, делающий работу, затерян в кишках огромного монстра. Я еле нашёл то место, где осуществляется передача части файла через P2P, причём место, где происходит контроль и вычисляются проценты — не нашёл. Вы уверены, что сможете за адекватное время прикрутить к этому шифрование, если вас внезапно попросят?

    Не tree, а лес дремучий
    $ tree Engine/
    Engine/
    ├── API
    │   ├── IClientAPI.cs
    │   ├── IClientAPICommand.cs
    │   ├── IServerAPI.cs
    │   ├── IServerAPICommand.cs
    │   └── StandardAPI
    │       ├── BaseCommand.cs
    │       ├── ClientCommands
    │       │   ├── ClientCommand.cs
    │       │   ├── Files
    │       │   │   ├── ClientFilePostedCommand.cs
    │       │   │   ├── ClientPostedFileDeletedCommand.cs
    │       │   │   ├── ClientReadFilePartCommand.cs
    │       │   │   └── ClientWriteFilePartCommand.cs
    │       │   ├── Messages
    │       │   │   ├── ClientOutPrivateMessageCommand.cs
    │       │   │   ├── ClientOutRoomMessageCommand.cs
    │       │   │   └── ClientOutSystemMessageCommand.cs
    │       │   ├── Others
    │       │   │   ├── ClientEmptyCommand.cs
    │       │   │   ├── ClientPingResponceCommand.cs
    │       │   │   └── ClientReceiveUserOpenKeyCommand.cs
    │       │   ├── P2P
    │       │   │   ├── ClientConnectToP2PServiceCommand.cs
    │       │   │   ├── ClientConnectToPeerCommand.cs
    │       │   │   └── ClientWaitPeerConnectionCommand.cs
    │       │   ├── Registrations
    │       │   │   └── ClientRegistrationResponseCommand.cs
    │       │   ├── Rooms
    │       │   │   ├── ClientRoomClosedCommand.cs
    │       │   │   ├── ClientRoomOpenedCommand.cs
    │       │   │   └── ClientRoomRefreshedCommand.cs
    │       │   └── Voice
    │       │       └── ClientPlayVoiceCommand.cs
    │       ├── ServerCommands
    │       │   ├── BaseServerCommand.cs
    │       │   ├── Files
    │       │   │   ├── ServerAddFileToRoomCommand.cs
    │       │   │   └── ServerRemoveFileFormRoomCommand.cs
    │       │   ├── Messages
    │       │   │   ├── ServerSendPrivateMessageCommand.cs
    │       │   │   └── ServerSendRoomMessageCommand.cs
    │       │   ├── Others
    │       │   │   ├── ServerEmptyCommand.cs
    │       │   │   ├── ServerGetUserOpenKeyCommand.cs
    │       │   │   └── ServerPingRequestCommand.cs
    │       │   ├── P2P
    │       │   │   ├── ServerP2PConnectRequestCommand.cs
    │       │   │   └── ServerP2PReadyAcceptCommand.cs
    │       │   ├── Registrations
    │       │   │   ├── ServerRegisterCommand.cs
    │       │   │   └── ServerUnregisterCommand.cs
    │       │   ├── Rooms
    │       │   │   ├── ServerCreateRoomCommand.cs
    │       │   │   ├── ServerDeleteRoomCommand.cs
    │       │   │   ├── ServerExitFormRoomCommand.cs
    │       │   │   ├── ServerInviteUsersCommand.cs
    │       │   │   ├── ServerKickUsersCommand.cs
    │       │   │   ├── ServerRefreshRoomCommand.cs
    │       │   │   └── ServerSetRoomAdminCommand.cs
    │       │   └── ServerCommand.cs
    │       ├── StandardClientAPI.cs
    │       └── StandardServerAPI.cs
    ├── Audio
    │   ├── IPlayer.cs
    │   ├── IRecorder.cs
    │   └── OpenAL
    │       ├── OpenALPlayer.cs
    │       └── OpenALRecorder.cs
    ├── Containers
    │   ├── RequestPair.cs
    │   ├── WaitingCommandContainer.cs
    │   └── WaitingPrivateMessage.cs
    ├── Engine.csproj
    ├── EventArgs
    │   ├── AddressReceivedEventArgs.cs
    │   ├── AsyncErrorEventArgs.cs
    │   ├── ConnectEventArgs.cs
    │   ├── DataReceivedEventArgs.cs
    │   ├── DataSendedEventArgs.cs
    │   ├── FileDownloadEventArgs.cs
    │   ├── ReceiveMessageEventArgs.cs
    │   ├── RecordedEventArgs.cs
    │   ├── RegistrationEventArgs.cs
    │   └── RoomEventArgs.cs
    ├── Exceptions
    │   ├── ErrorCode.cs
    │   └── ModelException.cs
    ├── Helpers
    │   ├── Crypter.cs
    │   └── Logger.cs
    ├── Model
    │   ├── Client
    │   │   ├── ClientContext.cs
    │   │   └── ClientModel.cs
    │   ├── Common
    │   │   └── ModelContext.cs
    │   ├── Entities
    │   │   ├── DownloadingFile.cs
    │   │   ├── FileDescription.cs
    │   │   ├── PostedFile.cs
    │   │   ├── Room.cs
    │   │   ├── RoomType.cs
    │   │   ├── SoundPack.cs
    │   │   ├── User.cs
    │   │   └── VoiceRoom.cs
    │   └── Server
    │       ├── ServerContext.cs
    │       └── ServerModel.cs
    ├── Network
    │   ├── AsyncClient.cs
    │   ├── AsyncPeer.cs
    │   ├── AsyncServer.cs
    │   ├── Conections
    │   │   ├── Connection.cs
    │   │   ├── IConnection.cs
    │   │   └── ServerConnection.cs
    │   └── P2PService.cs
    └── Properties
        └── AssemblyInfo.cs
    
    31 directories, 89 files
    • +4
      Наверно потому, что проценты эта задача UI. И модель не должна заботится о том, что бы высчитывать для него проценты и все остальное. А они как и положено находятся в MessageViewModel.

      На счет шифрования не уверен.
      • +3
        Согласен с вами, что отображение процентов это задача UI. Но я спрашивал, где вы вычисляете проценты, то есть делите часть на целое и умножаете на 100.
        А они как и положено находятся в MessageViewModel.
        А вот здесь вы не правы, проценты у вас считаются в файле Engine/API/StandardAPI/ClientCommands/Files/ClientWriteFilePartCommand.cs#L71

        downloadEventArgs.Progress = (int)((downloadingFile.WriteStream.Position * 100) / receivedContent.File.Size);
        

        и потом всплывают, как вы верно заметили, в MessageViewModel.cs#L147

            private void ClientDownloadProgress(object sender, FileDownloadEventArgs e)
            {
              roomViewModel.MainViewModel.Dispatcher.Invoke(new Action<FileDownloadEventArgs>(args =>
              {
                  .....
                  if (args.Progress >= 100)
        

        Ну вы и без меня всё это прекрасно помните.
        • 0
          Ага ошибся так ошибся. Плохо что они там считаются, надо бы их от туда убрать.
    • 0
      Почти сотня файлов… боже, это точно не межгалактический мессенджер на ближайшие тысячу лет? :) Какое ужасно громоздкое решение.
  • 0
    Вопрос на засыпку: а что вам дало применение MVVM? Что вот этот двухоконный чат выйграл от «интыпрайз» архитектуры?
    • 0
      То что UI это не 1 файл размером в 2000 строк. (изначально он такой и был)
      Во вторых удобный механизм привязок и шаблонов, который упростился при переходе на MVVM.

      Ну и мне удобней работать в 100 маленьких файлах, чем в 10 огромных.
      • 0
        Ну допустим файл с формой уменьшился (хотя вам-то что? Он всё-равно отформатирован, видно что и где). Но код-то никуда не пропал — вы выделили отдельную сущность и потратили время ещё и на взаимодействие V с VM. И кому стало легче от трёх сущностей вместо двух?
      • 0
        Про «привязок и шаблонов» — они здесь каким боком? Binding был ещё в WinForms, а шаблоны — те тоже как бы к MVVM отношения не имеют — всего лишь «фича» WPF.
        • 0
          Разделяй и властвуй :)
          Намного проще писать программу когда все структурно разделено, и не нужно тратить время на поиск информации в огромном методе, или огромном классе маленький метод. Ну если вам легче от разделения когда не становится, это лично ваше мнение. Мне, как я уже говорил, проще работать именно так.

          И куда, кстати, вы собрались привязываться если не установите DataContext view'y? В таком случае вам в ручную придется задавать свойства всем корневым контролам. В случае с MVVM задается DataContext, а все остальное (установление, обновление свойств и списков) делает View. За счет этого и упрощение. Это из того что быстро вспомнилось.
          • 0
            Вы смешиваете в кучу просто неструктурированный код и нормальный, модульный код, но без MVVM наворотов. в вашем случае вы имеете тривиальную задачу доставки сообщений. Ну, пяток классов я бы там накидал, показал на формах — MVVM там делать нечего.

            И куда, кстати, вы собрались привязываться если не установите DataContext view'y?


            Туда же, куда и в обычной двухуровневой схеме — к модели. Скажем, класс Message — он спокойно покажется в списке и без отдельного MessageView.

            Для ясности вам-бы как-то уяснить: WPF — это НЕ MVVM! На WPF можно писать под любую архитектуру.
            • –1
              Вы смешиваете в кучу просто неструктурированный код и нормальный, модульный код, но без MVVM наворотов. в вашем случае вы имеете тривиальную задачу доставки сообщений. Ну, пяток классов я бы там накидал, показал на формах — MVVM там делать нечего.


              Хорошо, напишите, со всем функционалом который есть у меня. Тогда мы сможем поговорить уже опираясь на код, а не на «а я бы его...».

              На WPF можно писать под любую архитектуру

              Я что, спорю что нельзя? Я говорю, что привязки и шаблоны наиболее удобно использовать с MVVM. Про уяснить я вообще промолчу.

              За примерно год существования этого чата, я ни разу не пожалел что сделал «монстра». Т.к. новый функционал, вроде голосового чата, P2P и т.д. отлично лег в существующую архитектуру.
              • 0
                Тратить кучу времени, чтобы только показать, что 90 файлов проекта в «гиперархитектуре» MVVM — это overdesign? Нет, спасибо — я оставлю вас при вашем апломбе.

                Я говорю, что привязки и шаблоны наиболее удобно использовать с MVVM


                Чушь, эти вещи вообще никак не соотносятся. Можно иметь просто «модель», привязанную (мы же про binding?) к форме. Вы понимаете о чём я говорю? Шаблоны (мы про шаблоны WPF: item template, data template — про них ведь?) — аналогично, НИКАК не соотносятся какую архитектуру вы выбрали.
                MVVM — просто модель программы, шаблонам всё равно как вы называете низлежащие классы. Просто MV* — современный баззворд, под который модно приплетать все технологии. :) (хотя он полезен, не спорю)

                Я допускаю, что вы сделали отличную архитектуру, но я не вижу особых преимуществ от использования MVVM — «чатик» мне не кажется тем «сложным энтерпрайзом», где оправдан такой трёхуровневый подход — можно и в два уровня спокойно уложить архитектуру с ничуть не меньшей гибкостью и расширяемостью — весь вопрос лишь в опыте.
                Простота — залог удобства для тех, кто решит развивать проект. Мне не страшны эти VM/M, но они явно усложняют работу с кодом без объективной надобности.

                Когда оправдан MVVM? Когда есть жёстко очерченный, ограниченный backend, куда можно сохранять только примитивные классы типа POCO или, например, когда model невозможно обвешать атрибутами, специфичными для persistence технологий (EF, XML, etc). Тогда ПРИХОДИТСЯ генерировать ViewModel, адаптирующую «простые, неуклюжие модели» к rich UI. То есть это не ради какой-то «гипергибкости», а тупо в силу ограничений на модель.
                У вас model — тупо «сообщения», никем и ничем не ограниченные, создаваемые вами же, т.е. практически любой формы. На этом фоне играть в MVVM — это стрелять из коллайдера по пивным пробкам. Это моё мнение, я не говорю что у вас «всё плохо» — у вас оно НЕ ОПРАВДАНО.
  • 0
    *промахнулся*

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