[Из песочницы] Пишем многопользовательский чат на C# за 15 минут

9e28d83e3e504dc998d8044d4a48fca0.jpgС# настолько же дурацкий язык, насколько и простой. А простой он настолько, что чат на несколько персон с минимальной защитой пишется в нем за пятнадцать, ну, максимум тридцать минут. У нас это заняло чуть более двух суток, но тут уже проблема дураков, а не инструментов.

Оформим задачу: мы хотим сделать децентрализованный групповой чат с некой защитой. Для такой «крупной» задачи нам понадобится всего ничего: C# (можно даже использовать неправославный MonoDevelop) с его прекраснейшим .NET Framework«ом.

Сначала напишем часть отправки и приема сообщений. Проблема состоит в том, что нужно как-то разобраться с отправкой сообщений нескольким клиентам. Это сделать довольно сложно, незнакомым с сетями в голову сразу приходят всякие грязные мысли а-ля: хранить все айпишники, которые когда-либо присылали на сообщения, или там организовать какой-нибудь хитрый граф между пользователями. Как обычно, проблема решается, если посмотреть на нее со стороны:, а зачем нам использовать TCP, когда есть UDP? eab2741ef2bf4e08a1fe9510b9e8c3ea.jpg

После совсем уж тщетных попыток наладить хоть какое-нибудь многопользовательское взаимодействие через TCP, мною был выбран второй вариант, и все оказалось очень простым — в UDP есть отдельные группы, и участники не могут так просто отправить сообщение какому-то отдельному участнику группы. Если сообщение отсылается, оно отсылается всем участникам группы — то что нужно для нас. Сделаем класс Chat, в котором будут следующие поля и методы:

Посмотреть код private UdpClient udpclient; private IPAddress multicastaddress; private IPEndPoint remoteep; public void SendOpenMessage (string data); public void Listen (); Для полей UdpClient, IPAddress и IPEndPoint подключим библиотеки System.Net.Sockets и System.NetНу и конструкторы-деструкторы само-собой. В конструкторе будем инициализировать поле udpclient: Посмотреть код public Chat () { multicastaddress = IPAddress.Parse (»239.0.0.222»); // один из зарезервированных для локальных нужд UDP адресов udpclient = new UdpClient (); udpclient.JoinMulticastGroup (multicastaddress); remoteep = new IPEndPoint (multicastaddress, 2222); } В деструкторе пока ничего не будем делать — Garbage collector же.Теперь главное — SendMessage и Listen. SendMessage будет отправлять UTF8 представление строки, и тут нам опять на помощь приходит C#, в котором получить байтовое представление можно в одну строчку: Посмотреть код public void SendMessage (string data) { Byte[] buffer = Encoding.UTF8.GetBytes (data);

udpclient.Send (buffer, buffer.Length, remoteep); } В методе Listen мы просто запустим отдельный поток на том же адресе и порте, через который мы отправляем сообщения, который будет получать байты, расшифровывать их, и писать их в общую консоль: Посмотреть код public void Listen () { UdpClient client = new UdpClient ();

client.ExclusiveAddressUse = false; IPEndPoint localEp = new IPEndPoint (IPAddress.Any, 2222);

client.Client.SetSocketOption (SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); client.ExclusiveAddressUse = false;

client.Client.Bind (localEp);

client.JoinMulticastGroup (multicastaddress);

Console.WriteLine (»\tListening started»);

string formatted_data;

while (true) { Byte[] data = client.Receive (ref localEp); formatted_data = Encoding.UTF8.GetString (data); Console.WriteLine (formatted_data); } } С обменом сообщений теперь покончено. Шифрование прикручивается еще проще: для него нам придется попросить у пользователя ключ при создании объекта чата, добавить методы шифрования дешифрования, отправлять в группу строку после обработки методом шифрования, а выводить после дешифрования. Делов то.Посмотреть код private byte[] Encrypt (string clearText, string EncryptionKey = »123») {

byte[] clearBytes = Encoding.UTF8.GetBytes (clearText); byte[] encrypted; using (Aes encryptor = Aes.Create ()) { Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes (EncryptionKey, new byte[] { 0×49, 0×76, 0×61, 0×6e, 0×20, 0×4d, 0×65, 0×64, 0×76, 0×65, 0×64, 0×65, 0×76 }); // еще один плюс шарпа в наличие таких вот костылей. encryptor.Key = pdb.GetBytes (32); encryptor.IV = pdb.GetBytes (16); using (MemoryStream ms = new MemoryStream ()) { using (CryptoStream cs = new CryptoStream (ms, encryptor.CreateEncryptor (), CryptoStreamMode.Write)) { cs.Write (clearBytes, 0, clearBytes.Length); cs.Close (); } encrypted =ms.ToArray (); } } return encrypted; }

private string Decrypt (byte[] cipherBytes, string EncryptionKey = »123») { string cipherText = »; using (Aes encryptor = Aes.Create ()) { Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes (EncryptionKey, new byte[] { 0×49, 0×76, 0×61, 0×6e, 0×20, 0×4d, 0×65, 0×64, 0×76, 0×65, 0×64, 0×65, 0×76 }); encryptor.Key = pdb.GetBytes (32); encryptor.IV = pdb.GetBytes (16); using (MemoryStream ms = new MemoryStream ()) { using (CryptoStream cs = new CryptoStream (ms, encryptor.CreateDecryptor (), CryptoStreamMode.Write)) { cs.Write (cipherBytes, 0, cipherBytes.Length); cs.Close (); } cipherText = Encoding.UTF8.GetString (ms.ToArray ()); } } return cipherText; } Теперь нужно немного изменить методы SendMessage и Listen, добавив туда шифрование и дешифрование. Довольно тривиально, на мой взгляд.Посмотреть код // в SendMessage public void SendMessage (string data) { Byte[] buffer = Encoding.UTF8.GetBytes (data);

Byte[] encrypted = Encrypt (data);

udpclient.Send (encrypted, encrypted.Length, remoteep); }

// в Listen while (true) { Byte[] data = client.Receive (ref localEp);

formatted_data = Decrypt (data);

Console.WriteLine (formatted_data); } Теперь финальный шаг — функция main. В ней мы будем запускать один поток, так что нам понадобится System.Threading; С дополнительным потоком все реализуется буквально в четыре строчки: Посмотреть код static void Main (string[] args) { Chat chat = new Chat (); Thread ListenThread = new Thread (new ThreadStart (chat.Listen)); ListenThread.Start (); string data = Console.ReadLine (); chat.SendMessage (data); } Все, простейший обмен сообщениями мы написали. К нему можно допилить обмен сообщениями в бесконечном цикле, никнеймы, историю сообщений, настройки, окошки — много чего, но это уже можно отнести к другой статье.P.S. bitbucket.org/AnAverageGuy/termprojectc — вот здесь можно найти весь тот мрачный ужас, который изображен на верхней части второй картинки. Когда-нибудь я причешу весь код, и ветка master превратится из тыквы в карету.

© Habrahabr.ru