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

от автора

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

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

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

После совсем уж тщетных попыток наладить хоть какое-нибудь многопользовательское взаимодействие через 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[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); // еще один плюс шарпа в наличие таких вот костылей. 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[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 }); 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 превратится из тыквы в карету. http://habrahabr.ru/post/252021/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *