С# настолько же дурацкий язык, насколько и простой. А простой он настолько, что чат на несколько персон с минимальной защитой пишется в нем за пятнадцать, ну, максимум тридцать минут. У нас это заняло чуть более двух суток, но тут уже проблема дураков, а не инструментов.
Оформим задачу: мы хотим сделать децентрализованный групповой чат с некой защитой. Для такой «крупной» задачи нам понадобится всего ничего: 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 превратится из тыквы в карету.
Автор: Crait