Delphi: сетевое программирование

КОМПЬЮТЕРНЫЕ КУРСЫ "ПОИСК"

[Главная страница] [Delphi] [Контакты]

Простой обмен данными (IdTCPServer, IdTCPClient)


Итак, сначала о компоненте сервера IdTCPServer (закладка Indy Servers). Для использования возможностей сервера этот компонент нужно поместить на форму (компонент неотображаемыи). При настройке компонента полезными являются следующие его свойства:

  • Active — активизирует или деактивизирует сервер (по умолчанию False);
  • Bindings — настраивает серверные сокеты (присоединяет их к определенно му порту компьютера, позволяет задавать диапазон IP-адресов и портов клиентов при помощи диалогового окна настройки свойства Binding;
  • ListenQueue — численное значение, ограничивающее максимальное количество запросов на установление соединения от клиентов в очереди;
  • MaxConnections — позволяет ограничить максимальное количество клиентов, присоединенных к серверу;

Рассмотрим несколько подробнее настройку серверных гнезд с использованием свойства Bindings. Так, на рис. 1 показано, как при помощи диалогового окна свойства Binding настроить сервер на обслуживание клиентов с любыми IP-адресами, при этом серверный сокет присоединяется к порту 12340.

Рис. 1. Настройка свойства Binding.

На этом настройку сервера можно и завершить (хотя здесь используются далеко не все возможности компонента IdTCPServer). Основная же работа сервера при обработке запросов клиентов может реализоваться в обработчике события OnExecute. В этот обработчик передается ссылка на объект TIdContext — поток, ассоциированный с клиентом, присоединенным к серверу. Посредством этого объекта ( а точнее, его свойства Connection) можно получать и отправлять данные, а также получать и устанавливать множество полезных параметров соединения. Первый пример использования объекта TIdContext при обработке запроса клиента приведен в листинге 1.

Теперь рассмотрим, как сконфигурировать клиент (IdTCPClient - закладка Indy Clients), чтобы он был способен взаимодействовать с нашим сервером. Чтобы использовать компонент ТСР-клиента, достаточно поместить его на форму (компонент также неотображаемый).

После этого как минимум нужно настроить следующие его свойства (остальные упоминаются по мере необходимости в приведенных далее примерах):

  • Host — имя или IP-адрес компьютера, на котором запущен сервер;
  • Port — номер порта, к которому присоединен серверный сокет.

Вообще, даже эти свойства на этапе разработки формы настраивать не обязательно. Приложение получается гораздо более гибким, если давать, например, пользователю возможность выбрать (или ввести) имя или адрес сервера.

Простой обмен данными

В начале работы с описанными в предыдущем разделе компонентами IdTCPServer и IdTCPClient рассмотрим создание несложного клиент-серверного приложения, клиентская и серверная части которого выполняют следующие функции.

  • Клиентское приложение соединяется с сервером и отправляет ему введенную пользователем строку, ждет ответа, выводит полученный от сервера текст, отсоединяется от сервера.
  • Серверное приложение принимает строку от клиентского приложения и посылает ответ (также текстовый), после чего разрывает соединение. Плюс к этому ведется подсчет количества обслуженных клиентов и запоминается IP-адрес компьютера, с которого пришел последний запрос.

Реализация как серверного, так и клиентского приложений в нашем случае предельно проста. Проект серверного приложения называется SimpleServer. Внешний вид формы сервера (во время работы приложения) представлен на рис. 2.

Рис. 2. Внешний вид простого сервера

Текстовое поле ( Edit) с количеством обработанных запросов имеет имя txtCount, а текстовое поле с адресом последнего обслуженного компьютера названо txtFrom. Вся работа сервера заключается в обработке события Execute для компонента IdTCPServer, помещенного на форму (присоедините этот компонент к порту 12340 и установите значение свойства Active = True) (листинг 1).

Листинг 1. Реализация простого сервера


procedure TForm1.FormCreate(Sender: TObject);
begin
  section := TCriticalSection.Create;
end;

procedure TForm1.IdTCPServer1Execute(AContext: TIdContext);
var
  strText: String;
begin
  //Принимаем от клиента строку
  strText := AContext.Connection.Socket.ReadLn;
  //Отвечаем
  AContext.Connection.Socket.WriteLn('Took the line: ' + strText);
  //Обновим сведения на форме сервера (сервер многопоточный,
  //поэтому используем синхронизацию
  section.Enter;
    Inc(processed, 1);
    txtCount.Text := IntToStr(processed);
    txtFrom.Text := AContext.Connection.Socket.Binding.PeerIP;
  section.Leave;
  //Закрываем соединение с пользователем
  AContext.Connection.Disconnect;
end;

При ответе клиенту сервер только повторяет принятую от него строку с добавлением текста 'Принял: ' в начало строки.

Анализируя листинг 1, можно заметить, что даже в рассматриваемом простейшем сервере пришлось применить синхронизацию при обновлении внешнего вида формы при помощи критической секции (необходимо дополнительно добавить имя модуля SyncObjs в секцию uses).

Теперь рассмотрим реализацию клиентской части (проект SimpleClient). Внешний вид клиентского приложения приведен на рис. 2.

Рис. 2. Внешний вид клиента

Естественно, что для работы клиентского приложения на форму помещен экземпляр компонента IdTCPClient (его имя — IdTCPClient1). Свойству Port этого компонента нужно присвоить значение 12340. Текстовое поле (Edit) для ввода строки, подлежащей отправке не сервер, имеет имя txtMessage. Текстовое поле (Edit), в которое вводится имя или адрес сервера, названо txtServer. Поле со строками ответов (Memo) имеет имя txtResults.

Вся работа клиентского приложения выполняется при нажатии кнопки Обработать. Текст соответствующего обработчика приведен в листинге 2.

Листинг 2. Реализация простого клиента


procedure TForm1.Button1Click(Sender: TObject);
begin
  //Соединяемся с сервером и посылаем ему введенную команду
  IdTCPClient1.Host := txtServer.Text;
  IdTCPClient1.Connect;
  IdTCPClient1.Socket.WriteLn(txtMessage.Text);
  txtMessage.Text := '';
  //Ожидаем ответ и закрываем соединение
  txtResults.Lines.Append(IdTCPClient1.Socket.ReadLn);
  IdTCPClient1.Disconnect;
end;

Примечание

Для простоты в реализации клиентского приложения не производится обработка исключений, генерация которых возможна, например, при неправильном указании компьютера, на котором запущено серверное приложение. В более сложных примерах, с которыми вы познакомитесь далее, обработка указанных исключений реализована.

Все, теперь можно запускать сервер и клиенты (на произвольном количестве компьютеров) и понаблюдать за результатами их работы. Только не забудьте запустить сервер до того, как будете обращаться к нему с помощью программы-клиента.

Исходный код здесь. Выполнен на Delphi XE.


 

Слежение за компьютером по сети (IdTCPServer, IdTCPClient)


Теперь рассмотрим более интересный пример использования сетевых компонентов IdTCPServer и IdTCPCLient, который может быть полезен для людей, имеющих отношение к администрированию компьютеров сети.

Серверная программа предварительно запускается на наблюдаемом компьютере. В этом примере программа-сервер позволяет клиентской программе получать следующие сведения о компьютере, на котором она (программа-сервер) запущена:

  • разрешение монитора;
  • глубину цвета для монитора;
  • полноразмерную копию экрана;
  • копию экрана, уменьшенную (или увеличенную) до заданных размеров.

Для получения указанных сведений про грамма-клиент должна послать серверу следующие строковые значения:

  • get_screen_width — для получения ширины и get_screen_height —для получения высоты экрана в пикселах;
  • get_screen_colors — для получения значения установленной для монитора глубины цвета (бит на точку);
  • get_screen — для получения полноразмерной копии экрана;
  • get_screen: X, Y — для получения копии экрана, приведенной к размеру Х х Y.

Сначала рассмотрим реализацию сервера (проект SpyServer). Весь код, обеспечивающий работу сервера, помещен в модуле Unit1.pas формы Form1. Обработчик запросов клиентов — главная процедура для сервера — приводится в листинге 3.

Листинг 3. Обработчик клиентских запросов


procedure TForm1.IdTCPServer1Execute(AThread: TIdPeerThread);
var
  strText: String;
  width, height, i: Integer;
  dc: HDC;
begin
  //принимаем от клиента строку
  strText := AThread.Connection.ReadLn;
  //определяем, что нужно выполнить
  if strText = 'get_screen_height' then
    //возвратим высоту экрана
    AThread.Connection.WriteInteger(Screen.Height)
  else if strText = 'get_screen_width' then
    //возвратим ширину экрана
    AThread.Connection.WriteInteger(Screen.Width)
  else if strText = 'get_screen_colors' then
  begin
    //возвратим количество бит на точку
    dc := GetDC(0);
    AThread.Connection.WriteInteger(GetDeviceCaps(dc, BITSPIXEL));
    ReleaseDC(0, dc)
  end
  else if strText = 'get_screen' then
    //возвратим полноразмерную копию экрана
    SendScreen(Screen.Width, Screen.Height, AThread.Connection)
  else
  begin
    //строка вида 'get_screen:x, y'
    //Определим значени высоты и ширины переданные пользователем
    strText := Copy(strText, 12, Length(strText) - 11);
    i := Pos(',', strText); //положение запятой
    width := StrToInt(Copy(strText, 1, i - 1));
    height := StrToInt(Copy(strText, i+1, Length(strText) - i));
    //возвратим копию экрана
    SendScreen(width, height, AThread.Connection);
  end;
end;

Используемая в листинге 3 процедура SendScreen, отправляющая клиенту копию экрана, приведена в листинге 4.

Листинг 4. Снятие копии экрана


//процедура снимает копию экрана, приводит полученное
//изображение к заданному размеру и отправляет
//преобразованное изображение клиентской программе
procedure SendScreen(width1: Integer; height1: Integer; Connection: TIdTCPServerConnection);
var
  ScreenCopy: TCanvas;
  gr: TBitmap;
  stream: TMemoryStream;
  rcDest, rcSource: TRect;
begin
  rcDest := Rect(0,0,width1,height1); //конечный размер изображения
  rcSource := Screen.DesktopRect; //исходный размер изображения
  //создаем канву и присоединяем ее к контексту Рабочего стола
  ScreenCopy := TCanvas.Create;
  ScreenCopy.Handle := GetDC(0);
  //создаем объект для хранения копии экрана и копируем изображение
  gr := TBitmap.Create;
  gr.Height := height1;
  gr.Width := width1;
  gr.Canvas.CopyRect(rcDest, ScreenCopy, rcSource);
  ReleaseDC(0, ScreenCopy.Handle);
  //сохраняем изображение в поток данных
  stream := TMemoryStream.Create;
  gr.SaveToStream(stream);
  //отправляем изображение клиенту
  Connection.WriteStream(stream, true, true);
  stream.Clear;
  stream.Free;
  gr.Free;
end;

Как можно увидеть, даже самая сложная операция рассматриваемого сервера — копирование изображения — реализуется довольно просто благодаря наличию такого стандартного класса, как TMemoryStream.

Компонент IdTCPServer (с именем IdTCPServer1) в этом примере присоединен к порту 12341 (не забудьте также установить свойство Active = True).

Теперь о реализации клиентского приложения (проект SpyClient). Внешний вид формы (Form1) клиента во время работы приводится на рис. 3 (видно, что пользователь наблюдаемого компьютера только что проиграл в игру Сапер).

Рис. 3

Описания, имена и значения настроенных вручную свойств самых важных компонентов формы клиента приведены в таблице 1.

Таблица 1. Основные компоненты формы клиента слежения и их свойства

Работа клиентского приложения начинается с соединения с сервером. Код, отвечающий за эту опреацию, приведен в листинге 5.

Листинг 5.Соединение с сервером


procedure TForm1.cmbConnectClick(Sender: TObject);
begin
  if cmbConnect.Caption = 'Подключиться' then
  begin
    if txtServer.Text = '' then
      //не введено имя сервера
      MessageDlg('Введите имя машины-сервера в текстовом поле', mtInformation, [mbOK], 0)
    else
    begin
      //подключаемся к серверу
      IdTCPClient1.Host := txtServer.Text;
      try
        IdTCPClient1.Connect;
      except
        MessageDlg('Не удается соединиться с указанным сервером', mtError, [mbOK], 0);
        Exit;
      end;
    end
  end
  else
    //отключаемся от сервера
    IdTCPClient1.Disconnect;
end;

Если соединение с сервером произошло успешно, то выполняется обработчик TForm1.IdTCPClient1Connected, подготавливающий приложение-клиент к периодическим запросам данных с сервера (листинг 6).

Листинг 6. Действия выполняемые при соединении с сервером


procedure TForm1.IdTCPClient1Connected(Sender: TObject);
begin
  txtServer.Enabled := False;
  cmbConnect.Caption := 'Отключиться';
  //начинаем периодически запрашивать данные с сервера
  Timer1.Enabled := True;
  //выполним первый запрос, не дожидаясь срабатыввания таймера
  Timer1Timer(nil);
end;

При отсоединении от сервера также выполняются действия, прекращающие периодические запросы данных и переводящие клиент в состояние ожидания подключения (первоначальное состояние программы) (листинг 7).

Листинг 7. Действия при отсоединении от сервера


procedure TForm1.IdTCPClient1Disconnected(Sender: TObject);
begin
  txtServer.Enabled := True;
  cmbConnect.Caption := 'Подключиться';
  Timer1.Enabled := False;
end;

Самой сложной частью клиентского приложения является обработка данных, присылаемых сервером. Клиентское приложение запрашивает данные по таймеру и обрабатывает полученные данные так, как показано в листинге 8.

Листинг 8. Запрос и обработка данных, полученных от сервера


procedure TForm1.Timer1Timer(Sender: TObject);
var
  stream: TMemoryStream;
begin
  //запрашаваем у сервера данные о наблюдаемом компьютере
  with (IdTCPClient1) do
  begin
    //...разрешение
    WriteLn('get_screen_width');
    WriteLn('get_screen_height');
    lblResolution.Caption := IntToStr(ReadInteger) + 'x' + IntToStr(ReadInteger);

    //...глубина цвета
    WriteLn('get_screen_colors');
    lblColors.Caption := IntToStr(ReadInteger);

    //...копия экрана
    //.....1-й вариант - копирование экрана без сжатия
    //WriteLn('get_screen');
    //.....2-й вариант - сжатие на стороне сервера
    WriteLn('get_screen:' + IntToStr(imgScreen.Width) + ',' +
            IntToStr(imgScreen.Height));
    //....получаем данные
    stream := TMemoryStream.Create;
    ReadStream(stream);
    stream.Position := 0;
    //....формируем изображение
    imgScreen.Picture.Bitmap.LoadFromStream(stream);
    stream.Clear;
    stream.Free;
  end;
end;

В тексте листинга 8 создано большое количество комментариев, поэтому дополнительно пояснять его нет смысла. Остановимся лишь на том, зачем в процедуре TForm1.Timer1Timer предусмотрено два варианта получения изображения с сервера.

Все дело в том, что сжатие (в нашем примере разрешение экрана наблюдаемого компьютера больше размера компонента imgScreen) на стороне сервера требует от компьютера, на котором запущено серверное приложение, большего процессорного времени на снятие копии экрана. Это снижает нагрузку на сеть при передаче изображения, а также экономит ресурсы компьютера-клиента. Но качество сжатого изображения в этом случае получается несколько хуже, чем когда мы предоставляем компоненту Image возможность масштабировать изображение самостоятельно.

Если же не использовать сжатие изображения на сервере, возрастает нагрузка на сеть при передаче полноразмерной копии экрана, а вся работа по сжатию изображения возлагается на компонент imgScreen (то есть дополнительно тратится процессорное время на компьютере клиента). При большом разрешении экрана наблюдаемого компьютера (или при наблюдении сразу за несколькими компьютерами) машина клиента, если она недостаточно мощная, может начать весьма ощутимо "тормозить". Качество сжатого изображения при этом получается более высоким.

В качестве более-менее эффективного решения можно предложить использовать большие промежутки времени между запросами данных с сервера слежения с масштабированием изображения на серверной стороне (если только машина сервера не является очень маломощной).

Исходный код здесь. Выполнен на Delphi 7.