Журнал LinuxFormat - перейти на главную

LXF96:Mono

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

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' в качестве параметра командной строки. Он бесполезен, но скоро станет полезным...

Есть здесь кто-нибудь?

Создавать клиента бессмысленно, пока мы не сделали сервер, так локальный сервер, что приступим. Серверу нужно:

  1. Слушать клиентов по порту (мы будем использовать порт 32768, его выбрал Майк, любезно поработавший для нас генератором случайных чисел).
  2. Принимать соединения клиентов и хранить их в коллекции сокетов. что код выполняет
  3. В промежутках, спокойно спать.

Позднее мы добавим возможность отсылать и принимать текст, но пока этого достаточно. Я вставил комментарии для объяснения кода...

   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."); 
 }

Из этого блока кода вы должны извлечь три основные вещи:

  1. Вы говорите BeginЧтото() и сообщаете Mono, какую функцию следует вызвать, когда происходит это событие. Например, для BeginAccept() мы просим Mono вызвать OnIncomingConnection().
  2. Когда ваш метод отработал, вам необходимо вызвать EndЧтото(). Вы не сможете вызвать BeginЧтото(), пока не вызовете EndЧтото().
  3. После выполнения функции обратного вызова, вы должны вновь вызвать 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: одну в режиме сервера и сколь угодно много в режиме клиента. Каждый раз при подключении клиента сервер будет печатать одно и то же сообщение, но мы все еще фактически ничего не делаем. Я же говорил, что сокеты – это непросто, верно?

Главное событие

После колоссальных усилий, мы созрели, чтобы закопаться во внутренности проекта: отправку и получение сообщений между клиентом и сервером. Отправка сообщений производится так:

  1. Клиент отправляет сообщения серверу
  2. Сервер отсылает сообщения всем другим клиентам
  3. Клиенты печатают свои сообщения на экране

С точки зрения клиента, необходимо быть готовым к приему контента, печати любого полученного им текста, а также отправке любого текста, вводимого пользователем. И снова, проще всего показать все это при помощи кода с пояснениями, он идет ниже:

 // Это старый элемент 
 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, для разделения основной и вспомогательной информации.

Персональные инструменты
купить
подписаться
Яндекс.Метрика