Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » C,С++ и C# - Все статьи »

Обсудить на форуме Обсудить на форуме

Практическое использование классов .NET Framework для разработки «корпоративного мессенджера»

Преамбула

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

«Добрый день!

вот ТЗ, посмотрите

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

вот на менеджерской и должно быть право писать сообщения. После того, как сообщения написаны, они отправляются на сервер. Там они сохраняются.

Клиентская часть - она простенькая, она должна уметь обрабатывать ту новую инфу которая есть на сервере. Клиентская часть будет стоять у всех рядовых клиентов (менеджерская - у администрации). Вход в клиентскую часть должен осуществляться по личным данным (логин и пароль), эти данные должны устанавливаться только в менеджерской части (т.е. администратор должен регистрирвоать новых клиентов и также удалять их, чтобы самовольно никто не смог получить доступ)...так вот, те люди которые вошли по свои данным в клиентскую часть, сидят в on-line (а программа тихонько работает в трее)...и если программа обнаруживает новую инфу на сервер, она её автоматически обрабатывает и выкидывает на экран и человек её читает....

ну вот примерно и всё....небольшие моменты:

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

-чтобы в менеджерской части видно было, кто в он-лайне, а кто нет (если это сильно сложно, ну тогда не нужно)

...

ну и конечно - программа должна быть на русском. вот и всё ТЗ...

...»

Введение

Сразу отвечать на письмо согласием или отказом я не стал, решил для начала немного подумать о возможных способах решения данной задачи, после чего и ответить, а подумать было над чем, и, пожалуй, самое главное, то, что тема данной работы для меня новая, даже, я бы сказал, непривычная. Так как вся моя работа в IT, на протяжении почти 10 лет, была связана с БД, а в предложенной задаче, невооруженным глазом была видна необходимость использования socket и multithreading, то браться за нее было бы довольно рискованное мероприятие. Но с другой стороны, в последнее время меня очень интересует .NET Framework и язык C#, а что бы изучить что-то новое надо практиковаться и практиковаться. И, подумав еще пару дней (предварительно, набросав для себя некоторые технические решения), я дал согласие, с условием, что работу буду делать практически бесплатно, но на C# (ответа на свое предложение, так и не получил).

Анализ требований

Выделим из письма, то, что можно было бы назвать требованиями:

-"нужна программа для одностороннего общения. Должно быть две части: менеджерская и клиентская. вот на менеджерской и должно быть право писать сообщения. После того, как сообщения написаны, они отправляются на сервер. Там они сохраняются."

- "Вход в клиентскую часть должен осуществляться по личным данным (логин и пароль), ... регистрирвоать новых клиентов ...".

-чтобы в менеджерской части видно было, кто в он-лайне, а кто нет (если это сильно сложно, ну тогда не нужно)

ну и конечно - программа должна быть на русском. вот и всё ТЗ...

Ну, вроде ключевые требования мы выделили, и, как говорится, поехали.

Пользователи системы.

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

В общем, после обдумывания, получается примерно следующее: у каждого пользователя будет уровень доступа: ‘r’ - читатель (рядовой пользователь), ‘w’ - писатель (менеджер). И, что самое интересное, у пользователя может быть владелец (тот самый графоманствующий сержант), а может даже и не один владелец, добавлю что, ничего не мешает тому, что бы, например, два менеджера были прописаны владельцами друг у друга, и тогда они смогут общаться между собой.

Сама, в смысле система.

Действительно будет две части, серверная и клиентская:

функциональность серверной части:

  • хранение списка пользователей (интерфейс для регистрации новых пользователей, удаления или редактирования существующих);
  • хранение истории сообщений (интерфейс для просмотра);
  • аутентификация имени пользователя и пароля;
  • перенаправление сообщений от одной группы пользователей ("писатель") к другой группе пользователей (могут быть и «читатели» и «писатели»), в соответствии с условиями владения (наверное, помните, что у каждого пользователя может быть владелец, который есть ничто иное, как другой пользователь, только более шустрый).

функциональность клиентской части:

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

На первый взгляд, кажется, что реализовать все это, легко и просто, но это только на первый взгляд. Дальше сами увидите и поймете, кстати, первую версию (точнее 0.97) можно скачать.

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


рис .1 Sequence diagram – вход в систему


рис .2 Sequence diagram – отправка сообщений с клиента


рис .3 Sequence diagram – завершение работы пользователя с клиентом корпоративного мессенджера


рис .4 Sequence diagram – проверка наличия пользователя в сети

Проектирование и кодирование

  • На диаграммах, можно выделить наличие следующих элементов, распишем их сразу по принадлежности к серверной или клиентской части разрабатываемого приложения:
  • MSGClient - клиентская часть, состоит:
  • Graphic User Interface (GUI), который, реагирует на действия пользователя и на действия со стороны SocketThreadClient;
  • SocketThreadClient - поток, обрабатывающий сообщения приходящие по локальной сети от MSGServer;
  • MSGServer - серверная часть, состоит:
  • Graphic User Interface (GUI), который, реагирует на действия пользователя и на действия со стороны SocketThreadServer, MessageThreadServer, Timer;
  • SocketThreadServer - поток, обрабатывающий сообщения приходящие по локальной сети от MSGClient (которых может быть много);
  • MessageThreadServer - поток, обрабатывающий очередь сообщений;
  • Timer – который, будет провоцировать, время от времени, проверку наличия пользователей в сети (что бы ни слать лишний раз им информацию – все равно ведь не получат).

Что бы ни быть, что называется голословным, приведу часть исходного кода (на C#), реализующего SocketThread:

    public class CSocketThread
    {

       public UserStatusChange onUserStatusOnline;
       public UserStatusChange onUserStatusOffline;
       public UserMessage onUserMessage;
       public UserLogon onUserLogon;

       public Control guiControlSender;

       public bool SocketTerminate;

       public Queue clients;

       public TcpListener listener;

       public Thread threadAcceptTcpClient;
       public Thread threadAnalysTcpClient;

       public virtual void AcceptTcpClient()
       {
          Console.WriteLine("Waiting for a new connection... ");
          while (!SocketTerminate)
          {
          try
          {
             TcpClient client = null;
             while (!listener.Pending() && !SocketTerminate) 
                Thread.Sleep(100);
             if (!SocketTerminate)
             {
                client = listener.AcceptTcpClient(); 
                if (client != null)
                   lock (clients)
                   {
                      clients.Enqueue(client);
                      Console.WriteLine("Connected!");
                      Console.WriteLine("Waiting for a new connection... ");
                   }
             }
          }
          catch (System.Exception ex)
          {
             Console.WriteLine( ex.Message );
          }
          }
       } // AcceptTcpClient

       public virtual void AnalysTcpClient()
       {
          Console.WriteLine("Waiting a new TCPClient ...");
          while (!SocketTerminate)
          {
             try
             {
                if (clients.Count > 0)
                {
                   TcpClient client = null;
                   lock (clients)
                   {
                      client = (TcpClient)clients.Dequeue();
                   }
                   if ( client != null)
                   {
                      Console.WriteLine("Analysis TCPClient ...");
                      Analysis(client);
                      Console.WriteLine("Waiting a new TCPClient ...");
                   }
                }
             }
             catch (System.Exception ex)
             {
                Console.WriteLine( ex.Message );
             }
          }
       } // AnalysTcpClient

       public CSocketThread ()
       {
       SocketTerminate = false;
       clients = new Queue();
       }

       public virtual void SocketThreadStart (Control ControlSender,
                                              string portn,
                                              string ipaddress)
       {
          guiControlSender = ControlSender;

          Int32 port = Convert.ToInt32( portn );
          IPAddress localAddr = IPAddress.Parse( ipaddress );

          Console.WriteLine("Start listener ...");

          listener = new TcpListener(localAddr, port);
          listener.Start();

          Console.WriteLine("Initializing socket threads ...");
          threadAcceptTcpClient = new Thread(new ThreadStart(AcceptTcpClient));
          threadAnalysTcpClient = new Thread(new ThreadStart(AnalysTcpClient));

          threadAcceptTcpClient.Start();
          threadAnalysTcpClient.Start();
       } // SocketThreadStart

       public virtual void Analysis(TcpClient client)
       {
       } // Analysis

    }

Примечание

Сразу оговорюсь, что данный код демонстрирует, мягко говоря, не совсем корректную работу. Лучше всего, для аналогичной работы, использовать асинхронные методы класса Socket. Но в данной ситуации, только для проверки, отработки некоторых принципов разработки таких систем, я, думаю, что можно пойти и таким, не очень «красивым», путем.

Что бы понять как поток GUI, получает эстафетную палочку, давайте взглянем, на еще одну интересную часть кода, точнее, часть того кода, что был реализован в Analysis для класса CSocketThreadClient (наследника класса CSocketThread):

    public override void Analysis(TcpClient client)
    {
       try
       {
          NetworkStream stream = client.GetStream();
          if (stream != null)
          {
             // пытаемся получить сообщение
             byte[] data = new Byte[256];
             String responseData = String.Empty;
             string message = "";
             do
             {
                int bytes = stream.Read(data, 0, data.Length); 
                message = 
                   String.Concat(message, Encoding.ASCII.GetString(data, 0, bytes)); 
             }
             while((stream.DataAvailable) && (!SocketTerminate));
             Console.WriteLine("Analyze :: read " + message);
             // разбор сообщений
             if (SocketTerminate)
                return;

             int iPosNotify = message.IndexOf(CMSGNotifyOnlineClient);
             if (iPosNotify == 0) // да, похоже на запрос пользователь on-line
             {
                // имя пользователя
                message = message.Remove(0, CMSGNotifyOnlineClient.Length );
                message = message.Remove(0, CMSGUsername.Length );
                string username = message.Substring(0, CMSGUsers.CLengthUsername);
                username = username.Trim();
                message = message.Remove(0, CMSGUsers.CLengthUsername );
                // делегируем в GUI
                if (onUserStatusOnline != null)
                   guiControlSender.BeginInvoke(onUserStatusOnline,
                                                new object[]{username});
                return;
             }

          } // stream
       }
       catch (SocketException e) 
       {
          Console.WriteLine("SocketException: {0}", e);
       }
       finally
       {
          client.Close();
       }
    } // Analysis

По коду видно, каким образом метод Analysis отличает, один тип сообщения от другого (приведенные ранее диаграммы последовательностей и соответствуют типам сообщений между MSGClient и MSGServer). Также видно, что все данные передаются в обычной строке, которая затем разбирается на составляющие части, которые, в свою очередь передаются дальше. А, метод BeginInvoke и реализует то, что можно назвать передачей эстафетной палочки в GUI-поток.

Примечание: Особенно интересно наблюдать в отладчике, как после вызова данного метода BeginInvoke, отладка идет одновременно в двух местах. Продолжается работа в методе Analysis, и в то же время идет работа в onUserStatusOnline. И отладчик, мотается туда сюда, ну и я, вместе с ним.

Реализовав приведенным способом, анализ принимаемой строки, я понял, что есть метод интересней. А, именно, если серверная и клиентская часть, будут обмениваться не просто строками, а строками, содержащими xml, все будет намного проще и красивей, но в таком случае похоже, что вырастет объем трафика.

Теперь неплохо бы сказать пару слов и о GUI, о том, как и где, хранить список пользователей и историю сообщений.

Первая моя мысль об использовании какой-либо БД, например MS SQL Server, для хранения списка пользователей и истории сообщений, я отмел почти сразу. Почти, потому что, все-таки набросал схему будущей БД в Enterprise Manager, и уже потом понял, что это не совсем правильный подход (хотя для корпоративного приложения, может, и правильно было бы использовать корпоративный сервер БД), поэтому пришлось организовывать хранение этих данных (списка пользователей и истории сообщений) в бинарном файле.

То есть пришлось, написать классы для работы с файлом списка пользователя, и с файлом истории сообщений пользователей. Приведу код класса предка, на основе которого и создавались классы для работы со списками:

    public abstract class CFile
    {
       protected string namefile;
       protected int sizerec;

       public CFile()
       {
       }

       public virtual object Convert(byte[] buffers)
       {
          return null;
       }

       public virtual byte[] Convert(object obj)
       {
          return null;
       }

       public virtual long Add(object obj)
       {
          long rec = -1;
          // открываем
          FileStream fs = new FileStream(namefile, FileMode.Open);
          // смотрим размеры
          FileInfo fi = new FileInfo(namefile);
          long size = fi.Length;
          // идем в конец
          fs.Seek(size, SeekOrigin.Begin );
          // создаем писателя
          BinaryWriter w = new BinaryWriter(fs, Encoding.Unicode );
          try
          {
             // конвертируем
             byte[] buffer = Convert(obj);
             // пишем
             w.Write(buffer);
             // смотрим размеры
             size += sizerec;
             rec = System.Convert.ToInt64( size / sizerec )-1;
             }
          finally
          {
             // закрываем
             w.Close();
             fs.Close();
          }
          return rec;
          }

       public virtual void Update(long rec, object obj)
       {
          // открываем
          FileStream fs = new FileStream(namefile, FileMode.Open);
          // позиционируемся
          fs.Seek(rec * sizerec , SeekOrigin.Begin );
          // создаем писателя
          BinaryWriter w = new BinaryWriter(fs, Encoding.Unicode );
          try
          {
             // конвертируем
             byte[] buffer = Convert(obj);
             // пишем
             w.Write(buffer);
          }
          finally
          {
             // закрываем
             w.Close();
             fs.Close();
          }
       }

       public virtual void Delete(long rec)
       {
          // открываем файл(овый поток)
          FileStream fs = new FileStream(namefile, FileMode.Open);
          // открываем писателя
          BinaryWriter w = new BinaryWriter (fs, Encoding.Unicode );
          try
          {
             // позиционируемся
             fs.Seek( rec * sizerec ,System.IO.SeekOrigin.Begin );
             // создаем пустоту
             byte[] buf = new byte[sizerec];
             for (int i=0; i< buf.Length; i++ )
                buf[i] = 0;
             // пишем ее 
             w.Write(buf);
          }
          finally
          {
             w.Close();
             fs.Close();
          }
       }

       public virtual long reccount()
       {
          // смотрим размеры
          FileInfo fi = new FileInfo(namefile);
          long recs = System.Convert.ToInt32( fi.Length / sizerec );
          return recs;
       }

       public virtual object[] List()
       {
          return null;
       }

    } // CFile

Что касается интерфейса пользователя, то приведу screenshot серверной части:

Резюме

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

Так что, вперед и удачи вам, на нелегком пути сетевого программирования.


Может пригодится:


Автор: Александр Меркульев
Прочитано: 8108
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (1)

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

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру