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

в 8:06, , рубрики: .net, C#, Peer-to-Peer, udp, сетевое программирование

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

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

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

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

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

После совсем уж тщетных попыток наладить хоть какое-нибудь многопользовательское взаимодействие через 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 превратится из тыквы в карету.

Автор: Crait

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js