Глава 11. Интеграция с веб-сервисами

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

В этой главе будут рассмотрены следующие темы:

Цель этой главы — научиться создавать мобильные HTTP-клиенты и подключаться к веб-сервисам и данным в облаке. Это очень обширная тема, поэтому мы сможем лишь затронуть её поверхностно.

Технические требования

Код в этой главе использует внешние веб-сервисы и облачные сервисы и требует наличия учётных записей в целевых системах. В большинстве случаев достаточно бесплатной учётной записи. Кроме того, последний демо-пример использует AWS SDK для Delphi, которая является дополнительной библиотекой.

Примеры из этой главы доступны по адресу: GitHub-репозиторий книги.

Понимание веб-сервисов

Не так давно сэр Тимоти Джон Бернерс-Ли изобрёл Всемирную паутину. В его видении системы управления информацией отдельные документы должны быть связаны между собой специальными унифицированными идентификаторами ресурсов (URL), и каждый, кто читает документ в веб-браузере, должен иметь возможность перейти непосредственно к связанному гипертекстовому документу. Он также разработал протокол передачи гипертекста (HTTP) и реализовал первые версии программ веб-сервера и веб-браузера.

HTTP — это простой протокол для обмена текстовыми документами через базовую инфраструктуру TCP/IP. Он очень масштабируем, потому что серверам и клиентам не нужно поддерживать активное сокет-соединение, и каждый HTTP-запрос независим от любого другого. Спецификация HTTP определяет различные типы запросов, которые клиент может отправить серверу, иногда называемые глаголами. Наиболее распространёнными являются GET, HEAD, POST, PUT и DELETE. Когда запрос успешен, сервер возвращает код состояния OK 200. Другой популярный код — 404, который указывает на ошибку «не найдено».

Первая популярная реализация веб-сервисов была основана на формате документов XML. XML-веб-сервисы определили набор XML-тегов для кодирования сообщений SOAP, которые обменивались между клиентами и серверами. Delphi 6 была первым инструментом разработчика на рынке, поддерживающим создание серверов и клиентов SOAP веб-сервисов. Эта поддержка всё ещё присутствует в текущих версиях Delphi, но со временем популярность SOAP снизилась. Вместо SOAP и XML теперь чаще используется JSON в качестве формата обмена и REST в качестве протокола связи.

Создание нативного HTTP-клиента

Пожалуй, самым важным сетевым протоколом связи является HTTP и его безопасная версия HTTPS. В кроссплатформенном мире Delphi существует HTTP-клиентская библиотека, которая обеспечивает единообразный доступ к реализациям HTTP-клиентов на разных платформах.

Вы можете работать с HTTP полностью в коде, используя типы из модулей System.Net.HttpClient и System.Net.URLClient, или полагаться на готовые компоненты из модуля System.Net.HttpClientComponents.

Давайте создадим простое приложение, которое позволит вводить URL в поле редактирования, загружать данные с помощью HTTP GET-запроса и отображать результат в memo:

  1. Создайте новый пустой мультиплатформенный проект Delphi, сохранив модуль главной формы как uFormHTTP, а всё приложение как HTTPApp.
  2. Измените свойство Name формы на FormHTTP.
  3. Поместите компонент TNetHTTPClient на форму.
  4. Поместите компонент TToolbar и компоненты TSpeedButton и TEdit на панель инструментов.
  5. Переименуйте кнопку в SpdbtnDownload, установите StyleLookup в arrowdowntoolbutton, а Align в Right.
  6. Переименуйте элемент управления редактированием в EdtURL, установите отступы по 8 пикселей со всех сторон и Align в Client.
  7. Поместите TMemo на форму, переименуйте в MemoData и выровняйте по Client.

Теперь дважды щёлкните на кнопке скорости и введите следующий код в обработчик события OnClick:

procedure TFormHTTP.SpdbtnDownloadClick(Sender: TObject);
var
  Memstr: TMemoryStream;
  Resp: IHTTPResponse;
begin
  Memstr := TMemoryStream.Create;
  try
    Resp := NetHTTPClient1.Get(EdtURL.Text, Memstr);
    if Resp.StatusCode = 200 then
      MemoData.Lines.LoadFromStream(Memstr)
    else
      ShowMessage(Resp.StatusCode.ToString + ': ' + Resp.StatusText);
  finally
    Memstr.Free;
  end;
end;
Содержимое образца PDB-файла в приложении HTTPApp
Рисунок 11.1: Содержимое образца PDB-файла в приложении HTTPApp

HTTP-клиентская библиотека работает как с протоколами HTTP, так и с HTTPS. Библиотека также может выполнять запросы асинхронно. Давайте модифицируем пример, чтобы сделать запрос асинхронным. Сначала установите свойство Asynchronous компонента HTTP-клиента в True.

Измените код обработчика OnClick:

procedure TFormHTTP.SpdbtnDownloadClick(Sender: TObject);
begin
  NetHTTPClient1.Get(EdtURL.Text);
  MemoData.Lines.Clear;
  MemoData.Lines.Add('Downloading...');
end;

Теперь обработайте событие OnRequestCompleted компонента HTTP-клиента:

procedure TFormHTTP.NetHTTPClient1RequestCompleted(
  const Sender: TObject; const AResponse: IHTTPResponse);
begin
  if AResponse.StatusCode = 200 then
    MemoData.Lines.LoadFromStream(AResponse.ContentStream)
  else
    ShowMessage(AResponse.StatusCode.ToString + ': ' + AResponse.StatusText);
end;

С этим изменением приложение останется отзывчивым, и пользовательский интерфейс сможет продолжать отрисовку экрана даже во время выполнения HTTP-запроса, потому что это происходит в другом потоке.

Использование XML SOAP веб-сервисов

Отправной точкой для реализации клиента SOAP веб-сервиса является документ WSDL, который определяет веб-методы, реализуемые данным веб-сервисом. Delphi предоставляет мастер импорта WSDL, который генерирует модуль Object Pascal с типами и методами, соответствующими функциональности SOAP веб-сервиса.

Давайте посмотрим на процесс интеграции с SOAP веб-сервисом на примере сервиса числовой обработки. Файл WSDL находится по адресу: NumberConversion WSDL.

Вкладка WSDL Location мастера Import WSDL
Рисунок 11.2: Вкладка WSDL Location мастера Import WSDL

После импорта WSDL мастер сгенерирует тип интерфейса NumberConversionSoapType:

type
  NumberConversionSoapType = interface(IInvokable)
    ['{A2E4D9CB-C96F-0965-87F0-E78269B29A8C}']
    function NumberToWords(const ubiNum: Int64): string; stdcall;
    function NumberToDollars(const dNum: TXSDecimal): string; stdcall;
  end;

Мастер также генерирует глобальную функцию:

function GetNumberConversionSoapType(
  UseWSDL: Boolean = System.False;
  Addr: string = '';
  HTTPRIO: THTTPRIO = nil): NumberConversionSoapType;

В коде формы используем сгенерированную глобальную функцию для получения ссылки на интерфейс веб-сервиса:

procedure TFormNumbers.BtnConvertClick(Sender: TObject);
begin
  var SNumber := GetNumberConversionSoapType.NumberToWords(
    Trunc(NumberBox1.Value));
  Memo1.Text := SNumber;
end;
Текстовая версия числа, сгенерированная SOAP веб-сервисом
Рисунок 11.3: Текстовая версия числа, сгенерированная SOAP веб-сервисом

В реальных сценариях модель SOAP может стать чрезвычайно сложной, и за последние годы SOAP был в большинстве случаев заменён на REST.

Интеграция с REST-сервисами

Архитектура REST для реализации веб-сервисов не так стандартизирована, как SOAP. Красота REST в том, что она полностью использует базовый протокол HTTP и просто использует заголовки запросов и ответов HTTP для структурной информации.

В REST больше свободы. Внутри REST-сервера есть различные ресурсы, идентифицируемые их URL-путями. Обычно к этим ресурсам или конечным точкам можно получить доступ с помощью различных HTTP-глаголов, таких как GET, POST, DELETE и PUT. Во многих случаях эти различные глаголы соответствуют различным операциям CRUD над базовым ресурсом.

Delphi предоставляет ряд компонентов для интеграции с REST-сервисами. Они доступны в палитре инструментов в категории REST Client и упоминаются под названием REST Client Library.

Для демо я бы хотел использовать сервис APILayer WeatherStack. Чтобы запустить это демо, вам нужно будет создать бесплатную учётную запись и получить API-ключ по адресу weatherstack.com.

Инструмент REST Debugger, показывающий информацию о погоде
Рисунок 11.4: Инструмент REST Debugger, показывающий информацию о погоде
REST Debugger с отфильтрованными данными в табличном формате
Рисунок 11.5: REST Debugger с отфильтрованными данными в табличном формате

После того как вы настроили REST Debugger и довольны результатами, вы можете скопировать конфигурацию в набор компонентов, готовых к размещению в вашем приложении Delphi, используя кнопку Copy Components.

Создайте новое мультиплатформенное приложение Delphi. Сохраните файл формы как WeatherClient_Form, а проект как WeatherClient. Измените имя формы на FormWeather.

Создайте новый модуль данных и добавьте его в проект. Измените имя модуля данных на DMWeather. В REST Debugger скопируйте настроенные компоненты и вставьте их на модуль данных. У вас должно быть три невизуальных компонента:

Добавьте в модуль данных компонент TFDMemTable для данных ответа и компонент TRESTResponseDataSetAdapter для сопоставления данных ответа с этой таблицей.

Таблица в памяти, заполненная данными ответа JSON во время разработки
Рисунок 11.6: Таблица в памяти, заполненная данными ответа JSON во время разработки

На главной форме добавьте кнопку, поле редактирования и метку. Назовите их соответственно BtnRefresh, EditLocation и LabelTemp. В обработчике события OnClick кнопки напишите следующий код:

procedure TFormWeather.BtnRefreshClick(Sender: TObject);
begin
  DMWeather.RESTRequest1.Params[1].Value := EditLocation.Text;
  DMWeather.RESTRequest1.Execute();
  LabelTemp.Text := DMWeather.FDMemTable1.
    FieldByName('temperature').AsString;
end;
Информация о температуре, отображаемая приложением WeatherClient
Рисунок 11.7: Информация о температуре, отображаемая приложением WeatherClient
Примечание: Бесплатная учётная запись в сервисе WeatherStack позволяет использовать только HTTP, а не HTTPS. На Android вы можете изменить файл манифеста, чтобы запросить поддержку HTTP с помощью следующей строки под узлом application: android:usesCleartextTraffic="true".

Интеграция с облаком

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

Существуют три различных подхода к доступу к облачным сервисам из Delphi:

  1. Вы можете использовать библиотеку REST Client.
  2. Вы можете использовать фреймворк Cloud API, который Delphi предоставляет уже давно для доступа к нескольким сервисам AWS и Azure.
  3. Вы можете использовать AWS SDK для Delphi от Appercept, которая бесплатно доступна в GetIt для клиентов Delphi Enterprise.

Delphi Cloud API

Delphi Cloud API определена в модуле Data.Cloud.CloudAPI, со специализированными модулями с типами и классами, специфичными для работы с выбранными сервисами, доступными от Amazon и Azure, объявленными в Data.Cloud.AmazonAPI и Data.Cloud.AzureAPI.

Amazon Web Services Microsoft Azure
Хранилище Simple Storage Service (S3) Blobs Service
Таблица Simple Database Service Table Service
Очередь Simple Queue Service (SQS) Queue Service

Таблица 11.1: Сервисы AWS и Azure, доступные в Delphi Cloud API

Перенос приложения ToDo List в Azure

В этом разделе мы сосредоточимся на сервисе Azure Table. Это сервис хранения данных NoSQL, который вы можете использовать для обмена данными между несколькими устройствами и даже несколькими приложениями.

Примечание: Вам понадобится учётная запись Azure с правильной конфигурацией, чтобы иметь возможность запустить это демо-приложение.
Конфигурация Tables в сервисе Azure Storage
Рисунок 11.8: Конфигурация Tables в сервисе Azure Storage

Каждая строка имеет два специальных поля, называемых RowKey и PartitionKey, которые используются как уникальные идентификаторы каждой строки. Учитывая важность этих двух полей, я объявил две константные строки для них:

const
  XML_ROWKEY = 'RowKey';
  XML_PARTITION = 'PartitionKey';

Код также имеет третью константу с именем таблицы:

const
  tablename = 'todolist';

Найдите исходные файлы ToDoList из предыдущей главы и скопируйте их в новый каталог. Сохраните проект как ToDoListAzure.

Для загрузки конфигурации Azure лучше использовать внешний файл. Я создал файл azure.ini:

[azure]
AccountName=xyz
AccountKey=1234567890

Этот ini-файл загружается из домашнего пути при первом создании модуля данных:

procedure TDMToDo.DataModuleCreate(Sender: TObject);
var
  IniFile: TMemIniFile;
begin
  var IniFilename := GetHomePath + PathDelim + 'azure.ini';
  if not FileExists(IniFilename) then
    raise Exception.Create('Missing Azure configuration');
  IniFile := TMemIniFile.Create(IniFilename);
  try
    AzureConnectionInfo1.AccountName :=
      IniFile.ReadString('azure', 'AccountName', '');
    AzureConnectionInfo1.AccountKey :=
      IniFile.ReadString('azure', 'AccountKey', '');
  finally
    FreeAndNil(IniFile);
  end;
end;

В том же обработчике события OnCreate нам нужно инициализировать объект TAzureTableService:

TableService := TAzureTableService.Create(AzureConnectionInfo1);

Загрузка списка строк в таблице базы данных в список объектов в памяти:

procedure TDMToDo.ToDoList(AList: TToDos);
var
  Item: TToDo;
  RowsList: TList<TCloudTableRow>;
begin
  MaxId := 0;
  if AList <> nil then
  begin
    AList.Clear;
    RowsList := TableService.QueryEntities(tablename);
    try
      for var RowObj in RowsList do
      begin
        Item.Id := RowObj.GetColumn(XML_ROWKEY).Value.ToInteger;
        Item.Title := RowObj.GetColumn('title').Value;
        Item.Category := RowObj.GetColumn('category').Value;
        AList.Add(Item);
        if Item.Id > MaxId then
          MaxId := Item.Id;
      end;
    finally
      RowsList.Free;
    end;
  end;
end;

Локальная функция, вычисляющая ID для новой строки:

function TDMToDo.GetNewId: Integer;
begin
  Inc(MaxId);
  Result := MaxId;
end;

Создание новой строки:

function TDMToDo.ToDoCreate(AValue: TToDo): Integer;
begin
  var Id := GetNewId;
  var RowObj := TCloudTableRow.Create;
  var ResponseInfo := TCloudResponseInfo.Create;
  try
    RowObj.SetColumn(XML_ROWKEY, Id.ToString);
    RowObj.SetColumn(XML_PARTITION, Id.ToString);
    RowObj.SetColumn('title', AValue.Title);
    RowObj.SetColumn('category', AValue.Category);
    TableService.InsertEntity(tablename, RowObj, ResponseInfo);
    if ResponseInfo.StatusCode = 200 then
      Result := Id
    else
      Result := -1;
  finally
    RowObj.Free;
    ResponseInfo.Free;
  end;
end;

Обновление строки:

function TDMToDo.ToDoUpdate(AValue: TToDo): Boolean;
begin
  var RowObj := TCloudTableRow.Create;
  var ResponseInfo := TCloudResponseInfo.Create;
  try
    RowObj.SetColumn(XML_ROWKEY, AValue.Id.ToString);
    RowObj.SetColumn(XML_PARTITION, AValue.Id.ToString);
    RowObj.SetColumn('title', AValue.Title);
    RowObj.SetColumn('category', AValue.Category);
    TableService.UpdateEntity(tablename, RowObj, ResponseInfo);
    Result := ResponseInfo.StatusCode = 200;
  finally
    RowObj.Free;
    ResponseInfo.Free;
  end;
end;

Удаление строки:

function TDMToDo.ToDoDelete(Id: integer): Boolean;
begin
  var RowObj := TCloudTableRow.Create;
  var ResponseInfo := TCloudResponseInfo.Create;
  try
    RowObj.SetColumn(XML_ROWKEY, Id.ToString);
    RowObj.SetColumn(XML_PARTITION, Id.ToString);
    TableService.DeleteEntity(tablename, RowObj, ResponseInfo);
    Result := ResponseInfo.StatusCode = 200;
  finally
    RowObj.Free;
    ResponseInfo.Free;
  end;
end;

Чтение одной строки:

function TDMToDo.ToDoRead(Id: Integer; out AValue: TToDo): Boolean;
begin
  var ResponseInfo := TCloudResponseInfo.Create;
  var RowObj := TableService.QueryEntity(
    tablename, Id.ToString, Id.ToString, ResponseInfo);
  if ResponseInfo.StatusCode = 200 then
  begin
    AValue.Title := RowObj.GetColumn('title').Value;
    AValue.Category := RowObj.GetColumn('category').Value;
    Result := True;
  end
  else
    Result := False;
end;

Использование AWS SDK для Delphi

Если вы предпочитаете использовать Amazon Web Services (AWS), а не Azure, Delphi Cloud API предлагает аналогичные возможности и интеграцию. Однако для AWS существует другая, гораздо более расширенная клиентская библиотека — AWS SDK для Delphi от Appercept. Эта облачная клиентская библиотека бесплатно доступна в версии Enterprise Delphi и может быть загружена из GetIt.

Эта клиентская библиотека предлагает большое количество дополнительных сервисов AWS, включая: Cognito (пулы пользователей и идентичностей), Polly, Simple Email Service (SESV2), Simple Notification Service (SNS), Simple Queue Service (SQS), Simple Storage Service (S3), Textract, Amazon Translate, AWS Key Management Service (AWS KMS) и AWS Secrets Manager.

Конфигурация бакетов S3 в консоли AWS
Рисунок 11.9: Конфигурация бакетов S3 в консоли AWS

Одним из вариантов AWS SDK является хранение локальных данных учётной записи в файле учётных данных в папке .aws под главной папкой пользователя. Файл имеет формат INI-файла:

[default]
aws_access_key_id=***
aws_secret_access_key=***

Создайте новое мультиплатформенное приложение Delphi и сохраните модуль главной формы как S3SDKClient_MainForm, а проект как S3SDKClient. Сначала нам нужно добавить модуль AWS.S3 в раздел interface модуля формы.

Нам нужно инициализировать несколько объектов для использования со временем и между методами:

private
  FS3Client: IS3Client;
  LResponse: IS3ListObjectsV2Response;

Начальный клиентский интерфейс настраивается с программными параметрами запуска:

procedure TFormS3.FormCreate(Sender: TObject);
begin
  var LOptions := TS3Options.Create;
  LOptions.Region := 'us-east-1';
  FS3Client := TS3Client.Create(LOptions);
end;

Когда нажата кнопка, мы можем использовать имя бакета из поля редактирования для инициализации объекта запроса:

procedure TFormS3.BtnListClick(Sender: TObject);
var
  LRequest: IS3ListObjectsV2Request;
begin
  LRequest := TS3ListObjectsV2Request.Create(EdBucketName.Text);
  LResponse := FS3Client.ListObjectsV2(LRequest);
  if LResponse.IsSuccessful then
    for var Item in LResponse.Contents do
      ListBox1.Items.Add(Item.Key + ' (' + Item.ContentType + ')');
end;

Последний шаг — обработать событие OnDoubleClick списка для загрузки выбранного элемента:

procedure TFormS3.ListBox1DblClick(Sender: TObject);
begin
  var Item := LResponse.Contents[ListBox1.ItemIndex];
  Item.DownloadFile(Item.Key);
  Image1.Bitmap.LoadFromFile(Item.Key);
end;
Пользовательский интерфейс бакетов S3 в консоли AWS
Рисунок 11.10: Пользовательский интерфейс бакетов S3 в консоли AWS

Более обширные демо AWS SDK для Delphi доступны на GitHub по адресу: appercept/aws-sdk-delphi-samples.

Резюме

В этой главе мы подробно рассмотрели ряд различных альтернативных методов для создания клиентских приложений, использующих веб-сервисы. Мы изучили HTTP, SOAP и REST клиентские библиотеки, доступные в Delphi. Мы также изучили Delphi Cloud API и AWS SDK для Delphi.

В целом, эти методы очень важны, так как большинство приложений взаимодействуют с удалёнными данными и веб-сервисами в наши дни. Какой метод вы используете, будет зависеть от ваших конкретных потребностей.

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