Практическое использование классов .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
Что касается интерфейса пользователя, то приведу screenshot серверной части:

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


i voobshe C# eto krutaya vesh ,, mnogo umeyet, a progi na nem malo vesat, 4to i nado dla web i voob6ee PEOPLEEEEEEE TRY C# .......


