LXF96:Mono
|
|
|
Содержание |
Mono: Создание чат-клиента
Если звенят ваши чувства паука, значит, где-то происходит преступление. А если звякнула ваша струнка Mono – значит, Пол Хадсон припас вам новый проект...
Хотя счета за телефон норовят доказать обратное, я не сильно люблю болтать с трубкой. Вот женщины – у тех непонятная сверхспособность разговаривать по телефону одновременно с утюжкой или просмотром телевизора, а я обнаружил, что могу сконцентрироваться на том, что говорят люди, только если их понимаю. Поэтому я предпочитают мгновенные сообщения – через программки для чата, позволяющие быстро обмениваться репликами с друзьями (или с людьми, которые нашли где-то в сети ваш электронный адрес и чувствуют себя одинокими). При помощи IM-клиентов я могу читать сообщения, ставить их в мою мысленную очередь для пакетной обработки и заняться своими делами в ожидании ответа. Более того, мне не нужно скучливо дожидаться, пока оппонент печатает – ведь я прочту все сообщение, когда он нажмет кнопку Send [Отправить].
Пусть для Linux интернет-пейджеры уже имеются в изобилии, многие участники нашей акции Make it With Mono [Сделай это в Mono] (www.linuxformat.co.uk/makeitwithmono) просили клонировать Miranda IM. Мы именно этого делать не будем. Мы сделаем нечто похожее на то, что делала Miranda IM в свои первые дни: простой чат-клиент для обмена сообщениями между людьми в вашей сети. Все эти вещи делаются через сокеты, являющиеся объектами сетевого соединения и позволяющие отправлять и принимать сообщения по данному номеру порта.
Сперва два предостережения. Первое: Fedora 7 уже вышла, так что я обновился и рекомендую вам сделать то же – иначе может оказаться, что ваша версия Mono не поддерживает некоторые функции, которые мы будем использовать в этом и будущих уроках. Второе: сокеты – одна из нескольких ужасных частей .NET, и они не просты в изучении. Работая с ними, я всегда пишу небольшую обертку, чтобы остальная часть моей программы не должна была сражаться с сокетами .NET напрямую, но лично я ненавижу, когда кто-то пишет учебник, принуждающий использовать чужие кодовые библиотеки, так что обертку я опущу.
Согласны? Все готовы переодеться хакерами и создавать классные проекты? Тогда вперед!
Создание основы
Годы прослушивания PR от Sun, твердящей, что «сеть – это компьютер», видимо, наконец запали мне в душу – я замечаю, что меня трясет, если мой компьютер отключается от Интернета более, чем на 30 минут. Но хотя на Mono очень легко писать простые приложения для сети, типа нашей RSS-читалки из прошлых номеров (LXF89), написать серьезный проект не так-то просто, потому что для получения достойного результата необходимо использовать асинхронную работу с сетью. Слово «асинхронный» жутко пугает многих, но означает оно всего-навсего то, что оставшаяся часть нашей программы может продолжать себе работать, до тех пор, пока что-то не поступит на сокет. Если это звучит ужасно похоже на многопоточность, то вы попали в точку – именно так и работают асинхронные сокеты.
Проект этого месяца вначале планировалось разбить на три части: посылка и прием данных, создание простого графического интерфейса пользователя, затем создание и того и другого одновременно. Но во время моей работы над объяснением работы кода стало ясно, что для GTK-части не хватит места, и я оставляю ее на вас. Если вы читали наш учебник GTK/RSS, то больших сложностей не должно быть, но в случае проблем просто напишите об этом на нашем форуме Programming на www.linuxformat.co.uk – и я, или кто-то еще, попытаемся ответить. Итак, приступим к первой и единственной части этого урока...
Запустите MonoDevelop (О-о! Fedora 7 содержит MonoDevelop 0.13 – круто!) и создайте новый консольный проект C# под названием Chinwag. Как обычно, убедитесь, что вы настроили использование Runtime version 2.0 в Project > Options > Runtime Options.
В итоге, Chinwag будет работать и как клиент (отсылать сообщения другим), и как сервер (принимать сообщения, а затем пересылать их другим клиентам), так что мы разработаем три класса: Chinwag, ChinwagClient и ChingwagServer. Первый необходим просто как основа для двух остальных: при запуске программы ей необходимо проверить слова ‘client’ или ‘server’, чтобы знать, какой режим использовать, и соответствующий класс будет создан и запущен. Итак, перво-наперво создадим класс Chinwag, а также скелет для классов ChinwagClient и ChinwagServer, который мы заполним позднее. Вот код:
using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace Chinwag { class Chinwag { static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine("You must specify either 'client' or 'server'."); return; } switch (args[0]) { case "client": ChinwagClient client = new ChinwagClient(); client.Run(); break; case "server": ChinwagServer server = new ChinwagServer(); server.Listen(); server.Run(); break; } } } class ChinwagClient { public void Run() { } } class ChinwagServer { public void Listen() { } public void Run() { } } }
Этот код соберется и запустится, но не будет делать ничего, кроме принуждения вас использовать ‘client’ или server’ в качестве параметра командной строки. Он бесполезен, но скоро станет полезным...
Есть здесь кто-нибудь?
Создавать клиента бессмысленно, пока мы не сделали сервер, так локальный сервер, что приступим. Серверу нужно:
- Слушать клиентов по порту (мы будем использовать порт 32768, его выбрал Майк, любезно поработавший для нас генератором случайных чисел).
- Принимать соединения клиентов и хранить их в коллекции сокетов. что код выполняет
- В промежутках, спокойно спать.
Позднее мы добавим возможность отсылать и принимать текст, но пока этого достаточно. Я вставил комментарии для объяснения кода...
Socket OurServerConn; // это сокет, который мы будем слушать List<Socket> RemoteConns = new List<Socket>(); // Это сокеты, соответствующие нашим клиентам byte[] SocketBuffer = new byte[1024]; // Это пригодится позднее, а пока пропустите! public void Run() { while (true) { // Запускается навсегда и ничего не делает Thread.Sleep(Timeout.Infinite); } } public void Listen() { // Слушаем порт 32768 для всех IP-адресов class ChinwagClient { IPEndPoint local_ep = new IPEndPoint(IPAddress.Any, 32768); public void Run() { } // Создаем сокет и связываем его с портом OurServerConn = new Socket(AddressFamily.InterNetwork, SocketType. MonoDevelop Stream, ProtocolType.Tcp); OurServerConn.Bind(local_ep); // Теперь начинаем слушать, с очередью ожидания до 10 соединений OurServerConn.Listen(10); // Готовы к принятию новых соединений OurServerConn.BeginAccept(new AsyncCallback(OnIncomingConnection), OurServerConn); } // Это вызывается, как только возникнет соединение void OnIncomingConnection(IAsyncResult ar) { // Эта ужасная строка захватывает соединение клиента... Socket client = ((Socket)ar.AsyncState).EndAccept(ar); // ...а затем добавляет его к нашему списку удаленных клиентов RemoteConns.Add(client); // Готовы к принятию следующих клиентов OurServerConn.BeginAccept(new AsyncCallback(OnIncomingConnection), OurServerConn); // и печатаем сообщение о статусе Console.WriteLine(“A new client has connected.”); }
Из этого блока кода вы должны извлечь три основные вещи:
- Вы говорите BeginЧтото() и сообщаете Mono, какую функцию следует вызвать, когда происходит это событие. Например, для BeginAccept() мы просим Mono вызвать OnIncomingConnection().
- Когда ваш метод отработал, вам необходимо вызвать EndЧтото(). Вы не сможете вызвать BeginЧтото(), пока не вызовете EndЧтото().
- После выполнения функции обратного вызова, вы должны вновь вызвать BeginЧтото().
Вместо «Чтото» можно подставить любой метод асинхронных сокетов в Mono:
BeginAccept()/EndAccept(), BeginConnect()/EndConnect(), BeginReceive()/EndReceive() и так далее.
Вот теперь наш маленький проект начинает работать: если вы откроете терминал, перейдете в каталог bin/Debug проекта Chinwag, затем выполните mono Chinwag.exe server, то программа запустится. Если затем вы откроете окно другого терминала, то сможете попытаться соединиться с сервером, набрав telnet localhost 32768. Вы не можете делать что-либо, зато сможете увидеть статусное сообщение ‘A new client has connected’ [Новый клиент соединен] в окне сервера – по крайней мере, какой-то прогресс налицо!
Hello, World
Теперь я хочу заняться клиентом, который имеет нечто общее с сервером. Клиентские соединения производятся с использованием асинхронных методов BeginConnect() и EndConnect(), которые не особо нужны, потому что наш проект не позволяет ничего выполнять, не соединившись. То есть, если мы используем старые добрые синхронные сокеты, для Chinwag разницы не будет; но тогда наш учебник был бы куда менее ценным! Основной цикл для клиента, как и для сервера, это Run(). Он не содержит вызов Sleep(), потому что ему необходимо читать ввод пользователя, который затем отправится по проводам серверу. Это выполняется посредством метода Console.ReadLine() (дополнение к нашему другу Console.WriteLine()) – он читает все, что вводит пользователь до нажатия Enter, обозначающего конец строки.
Вот новый код для ChinwagClient, тоже с пояснениями:
class ChinwagClient {
Socket ServerConn; byte[] SocketBuffer = new byte[1024]; // Опять игнорируем string OurName = “Anonymous”; // Мы собираемся позволить людям изменять их имена позднее // Это очень похоже на метод Listen() сервера, кроме того, что вместо BeginConnect() мы используем BeginAccept() void DoConnect(string ipaddress) { IPEndPoint remoteep = new IPEndPoint(IPAddress.Parse(ipaddress), 32768); ServerConn = new Socket(AddressFamily.InterNetwork, SocketType. Stream, ProtocolType.Tcp); ServerConn.BeginConnect(remoteep, new AsyncCallback(On ClientConnect), ServerConn); } // Следующий метод запускается, когда новый клиент успешно подключается к серверу public void OnClientConnect(IAsyncResult ar) { // Припасем сокет на будущее ServerConn = (Socket)ar.AsyncState; } public void Run() { DoConnect(“127.0.0.1”); // Соединяемся с локальным сервером string input; while (true) { // Постоянно читаем данные input = Console.ReadLine(); } } }
Этот новый код означает, что вы можете теперь запускать различные копии Chinwag: одну в режиме сервера и сколь угодно много в режиме клиента. Каждый раз при подключении клиента сервер будет печатать одно и то же сообщение, но мы все еще фактически ничего не делаем. Я же говорил, что сокеты – это непросто, верно?
Главное событие
После колоссальных усилий, мы созрели, чтобы закопаться во внутренности проекта: отправку и получение сообщений между клиентом и сервером. Отправка сообщений производится так:
- Клиент отправляет сообщения серверу
- Сервер отсылает сообщения всем другим клиентам
- Клиенты печатают свои сообщения на экране
С точки зрения клиента, необходимо быть готовым к приему контента, печати любого полученного им текста, а также отправке любого текста, вводимого пользователем. И снова, проще всего показать все это при помощи кода с пояснениями, он идет ниже:
// Это старый элемент public void OnClientConnect(IAsyncResult ar) { ServerConn = (Socket)ar.AsyncState; // А это новый – вам необходимо вызвать BeginReceive(), если хотите получить текст if (ServerConn.Connected) { ServerConn.BeginReceive(SocketBuffer, 0, SocketBuffer.Length, SocketFlags.None, OnSocketReceive, ServerConn); } } // Что-то пришло! void OnSocketReceive(IAsyncResult ar) { // Вызываем EndReceive() и перехватываем возвращаемое значение, чтобы увидеть, сколько байтов было послано int bytes = ServerConn.EndReceive(ar); // Теперь готовимся к приему следующего содержимого ServerConn.BeginReceive(SocketBuffer, 0, SocketBuffer.Length, SocketFlags.None, OnSocketReceive, ServerConn); // Данные сокета копируются в SocketBuffer (массив байтов, созданных нами ранее) – этот код преобразует байты в строку,пригодную для вывода string text = Encoding.ASCII.GetString(SocketBuffer, 0, bytes); Console.WriteLine(text); } // Пересылка текста по проводам противоположна его приему: нам необходимо преобразовать введенную строку в байты public void SendText(string text) { // Это шикарный способ сборки строки из переменных text = string.Format(“<{0}> {1}”, OurName, text); ServerConn.Send(Encoding.ASCII.GetBytes(text)); }
Это дает вам основу для пересылки текста от клиента серверу, но чтобы действительно заставить текст идти от пользователя к серверу, нужно модифицировать клиентский цикл Run() так:
while (Running) { input = Console.ReadLine(); SendText(input); }
Теперь осталось сделать только одну вещь: обновить сервер так, чтобы он мог получить текст и также отправить полученный текст другому клиенту. Это очень похоже на код отправки и получения текста у клиента, так что объяснять тут особо нечего:
void OnIncomingConnection(IAsyncResult ar) { Socket client = ((Socket)ar.AsyncState).EndAccept(ar); RemoteConns.Add(client); // Это немного новое – нам необходимо быть готовым к приему данных от клиентов client.BeginReceive(SocketBuffer, 0, SocketBuffer.Length, SocketFlags. None, OnSocketReceive, client); OurServerConn.BeginAccept(new AsyncCallback(OnIncoming Connection), OurServerConn); Console.WriteLine(“A new client has connected.”); } // Клиент отослал текст! void OnSocketReceive(IAsyncResult ar) { Socket client = (Socket)ar.AsyncState; // Это то же самое, что и в коде клиента int bytes = client.EndReceive(ar); client.BeginReceive(SocketBuffer, 0, SocketBuffer.Length, SocketFlags.None, OnSocketReceive, client); string text = Encoding.ASCII.GetString(SocketBuffer, 0, bytes); Console.WriteLine(text); // за исключением этой части: нам необходимо отправить текст всем другим клиентам SendToAll(client, text); }
Вот и все. Это очень сложный проект, поскольку он работает с клиентами и сервером в одном файле исходного кода. Работа с каждым из них по отдельности была бы значительно легче, но зато теперь у вас есть инструменты, необходимые для самостоятельного создания любых программ. Если вы сможете создать нечто крутое и интересное – вроде мини web-сервера, пересылки файлов или, возможно, просто улучшенной версии этой чат-системы –скиньте мне ваш исходный код, лицензированный по свободной лицензии (сгодятся GPL или BSD), и я посмотрю, можно ли его выложить на один из наших дисковприложений к Linux Format. Это если я не увлекусь захватывающим сетевым чатом...
Врезка
Идем дальше
Код на DVD этого месяца содержит небольшое дополнение к нашему проекту для распознавания ввода клиента, который начинается с / в качестве команды. Например, вы вводите /connect 127.0.0.1 для соединения с локальным сервером. Но вы также можете ввести /name Bob для смены вашего имени. Если хотите проверить свои навыки, попытайтесь написать команду /ping, отсылающую короткие сообщения серверу, на которые последний отвечает. Затем можете измерить время отклика и таким образом узнать, хорошо ли работает ваше чат-соединение.
Пересылка двоичных данных
Как показано в этом проекте, наш чат-клиент не может работать с нестроковыми данными, потому что все пересылаемые и получаемые байты преобразуются в строки. Решение этой проблемы – пересылать данные по проводам в виде Base64-шифрованных строк, чтобы данные были в безопасности. Это также позволяет вам смешивать и сравнивать строки и двоичные данные, используя нечто вроде XML, для разделения основной и вспомогательной информации.