Преамбула
В начале сентября 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 серверной части: Резюме
Собственно говоря, на этом, пожалуй, можно и закончить данный обзор. Вам осталось реализовать, очередь сообщений, и работу с таймером. Принципы, как это можно реализовать, мы посмотрели. Если будут какие-то сложности или вопросы и предложения, по данной работе, не стесняйтесь, пишите.
Так что, вперед и удачи вам, на нелегком пути сетевого программирования.
|