Преамбула

На початку вересня 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 серверної частини:



Резюме


Власне кажучи, на цьому, мабуть, можна й закінчити цей огляд. Вам
залишилося реалізувати, чергу повідомлень, і роботу з таймером. Принципи, як це
можна реалізувати, ми подивилися. Якщо будуть якісь ускладнення або запитання і
пропозиції, по даній роботі, не соромтеся, пишіть.

Так що, вперед і удачі вам, на нелегкому шляху мережного програмування.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*