Исходники
Статьи
Языки программирования
.NET Delphi Visual C++ Borland C++ Builder C/С++ и C# Базы Данных MySQL MSSQL Oracle PostgreSQL Interbase VisualFoxPro Веб-Мастеру PHP HTML Perl Java JavaScript Протоколы AJAX Технология Ajax Освоение Ajax Сети Беспроводные сети Локальные сети Сети хранения данных TCP/IP xDSL ATM Операционные системы Windows Linux Wap Книги и учебники
Скрипты
Магазин программиста
|
Практическое использование классов .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 – проверка наличия пользователя в сети Проектирование и кодирование
Что бы ни быть, что называется голословным, приведу часть исходного кода (на 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 серверной части: РезюмеСобственно говоря, на этом, пожалуй, можно и закончить данный обзор. Вам осталось реализовать, очередь сообщений, и работу с таймером. Принципы, как это можно реализовать, мы посмотрели. Если будут какие-то сложности или вопросы и предложения, по данной работе, не стесняйтесь, пишите. Так что, вперед и удачи вам, на нелегком пути сетевого программирования. |
Форум Программиста
Новости Обзоры Магазин Программиста Каталог ссылок Поиск Добавить файл Обратная связь Рейтинги
|