Форум программистов, компьютерный форум, киберфорум
krapotkin
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  
Блог. Двадцать пять лет Делфи-практики

В этом блоге я буду публиковать ответы на вопросы, которые постоянно приходится повторять на форуме.
Здесь можно это сделать более развернуто и спокойно.

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

Начав с Делфи-2 двадцать пять лет назад, я прошел все версии, испробовал массу технологий, включая работу с БД, с графикой DirectX, связью с серверами и интернетом, разработку на Андроид и IOS, и многое, многое другое.
________________________________________ ________________________________________ ____
P.S. все, о чем здесь написано, всего лишь измышления из головы.
совпадения с реальными людьми и фактами случайны.

Реализуем вызовы HTTP API

Запись от krapotkin размещена 01.10.2023 в 12:54
Показов 3894 Комментарии 0
Метки delphi, http, rest api, web api

Мотивация

В настоящее время на смену родному для делфи клиент-серверному приложению, подключенному намертво к базе данных,
пришли те, что получают данные через HTTP запросы к какому-то API.
В современных версиях делфи есть библиотека REST API, но к ней иногда возникают вопросики по работоспособности.
Чтобы не разбирать чужой сложный код, можно быстро и на коленке написать свою маленькую библиотечку, где будут вызовы к некоему API.
Поехали.

Общий подход

В общем случае все API примерно одинаковы. REST или нет - все равно.
Поэтому сначала создадим класс, который решает задачу "вообще", а потом отнаследуем от него другой, который будет обращаться уже к нашему конкретному API.

У нас всегда есть базовый адрес и конкретные пути для каждого метода API.
Т.е. в класс помещаем поле BaseUrl: string а пути для методов будем добавлять уже при заросе.
Также, для идентификации абонента сервер обычно принимает некий токен. Его получают либо раз и навсегда, либо каждый раз мы должны выполнять метод логин и получать его на сессию работы.
Но поле Token: String тоже добавляем в класс.
Токен чаще всего передается в заголовках запроса, которые имеют вид Name = Value и в делфи есть готовый тип для них в модуле System.Net.URLClient:
Кликните здесь для просмотра всего текста
Delphi
1
2
3
4
5
6
7
  TNameValuePair = record
    Name: string;
    Value: string;
    constructor Create(const AName, AValue: string);
  end;
 
  TNameValueArray = TArray<TNameValuePair>;

Поэтому в нашем классе будет еще поле Headers : TNameValueArray;
и метод, который эти заголовки заполняет.
Delphi
1
procedure SetHeaders(); virtual;
естественно, виртуальный, ведь мы не знаем, какие заголовки потребует потом конкретный API-наследник.
Хотя в целом обычно все одинаково. Поэтому изначально смело делаем так, и нам редко придется это менять
Delphi
1
2
3
4
5
6
7
procedure THttpApi.SetHeaders;
begin
  SetLength(Headers, 3);
  Headers[0] := TNameValuePair.Create('Content-Type','application/json');
  Headers[1] := TNameValuePair.Create('Accept','application/json');
  Headers[2] := TNameValuePair.Create('Authorization',Token);
end;
предполагаем, что у нас будет API, где туда-обратно будут ходить JSON-строки, поэтому для послылки и для приема данных сделаем поля типа TStringStream
Delphi
1
2
SrcStream : TStringStream;
ResStream : TStringStream;
Врезочка

для тех, кто не очень в курсе про HTTP
полный адрес (или URI, как его называют) состоит из нескольких частей
(кратко и своими словами. Правильно и полно - например тут https://www.geeksforgeeks.org/components-of-a-url/)
HTML5
1
http://myserver.com/api/v2/orders/2314/item/12?deleted=1&sort=name&lang=ru
http (иногда https, или еще что-то) - протокол или схема
myserver.com - хост. адрес сервера
api/v2/orders/2314/item/12 - путь, или собственно URL - ресурс, который мы запрашиваем у сервера
обратите внимание, в URL бывают параметры - как тут - заказ 2314, строка 12
?deleted=1&sort=name&lang=ru - query parameters - дополнительные параметры запроса. Отделяются ? и разделены &.

Возвращаемся к теме

Для формирования итоговой строки URL мы будем использовать BaseUrl = схема + хост + неизменный кусок для API
плюс для каждого метода формируем как остаток URL, так и query params.
Т.е. нам нужны поля
UrlParams, QueryParams и метод MakeUrl : string; virtual;
чтобы собрать потом готовый URL для вызова
и то и другое - это обычный TStringList. только первый - просто строки, а второй будет использовать встроенный в StringList функционал Name=Value.

JSON

В каждый запрос нам нужно передавать JSON. И принимать JSON. И свои объекты упаковывать в JSON и пришедший JSON парсить и создавать по нему объекты. Это называется сериализация и десериализация.
В Делфи есть пара независимых друг от друга реализаций библиотек работы с JSON, можно пользоваться ими. Но мне не нравится их синтаксис, поэтому я обычно пользуюсь симпатичноый маленькой и очень удобной библиотекой XSuperObject.
Про сериализацию смотрите отдельный пост этого блога.

Первый итог

Все готово к тому, чтобы представить базовый класс для совершения запросов на сервер и получения результатов
Поместим его в отдельный юнит и потом будем использовать где попало.
Если б не неrоторые условности, то метод MakeUrl выглядел бы так
Delphi
1
2
3
4
function THttpApi.MakeUrl: string;
begin
  result := BaseURL + UrlParams.DelimitedText + '?' + QueryParams.DelimitedText;
end;
но для передачи параметров не все символы передаются в открытом виде, их надо URL-кодировать
можно самостоятельно, но в Делфи есть класс TUri, который сделает все за нас,
но к нему надо подкинуть недостающие методы. Чтобы не пихать эти методы прямо в API, я сбросил их в отдельный файл.
UUriHelper.pas
без объяснения. просто надо положить рядом
Кликните здесь для просмотра всего текста
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
unit UUriHelper;
 
interface
uses System.NetEncoding, System.Net.URLClient;
 
type
  TURIHelper = record helper for TURI
    function FindParameterIndex(const AName: string): Integer;
    function ParamExists(const AName:string): Boolean;
    procedure AddOrSetParamByName(const AName,AValue: string);
  end;
 
 
implementation
 
uses
  System.SysUtils;
 
{ TURIHelper }
 
procedure TURIHelper.AddOrSetParamByName(const AName, AValue: string);
var
  LIndex: Integer;
begin
  LIndex := FindParameterIndex(AName);
  if LIndex >= 0 then
    Parameter[LIndex] := TURIParameter.Create( TNetEncoding.URL.EncodeQuery(AName), TNetEncoding.URL.EncodeQuery(AValue))
  else
    AddParameter(AName, AValue);
end;
 
function TURIHelper.FindParameterIndex(const AName: string): Integer;
var
  I: Integer;
  LName: string;
begin
  Result := -1;
  LName := TNetEncoding.URL.EncodeQuery(AName);
  for I := 0 to Length(Params) - 1 do
    if Params[I].Name = LName then
      Exit(I);
end;
 
function TURIHelper.ParamExists(const AName: string): Boolean;
begin
  Result := FindParameterIndex(AName)<>-1;
end;
 
end.

поэтому будет чуть сложнее и MakeUrl переделан под использование FUri
Кликните здесь для просмотра всего текста
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function THttpApi.MakeUrl: string;
var
  s : String;
  name : String;
  i : integer;
begin
  s := IncludeTrailingPathDelimiter(BaseURL) + UrlParams.DelimitedText;
  // FURI это RECORD поэтому не нужно FURI := TUri.Create(s).
  // можно просто вызвать Create для инициализации полей
  FURI.Create(s);
  for i := 0 to QueryParams.Count-1 do
  begin
    name := QueryParams.Names[i];
    FURI.AddOrSetParamByName(name,QueryParams.Values[name]);
  end;
  result := FURI.ToString;
end;


Кликните здесь для просмотра всего текста
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
unit HttpApi;
 
interface
uses
  System.Classes,
  System.SysUtils,
 
  system.Net.HttpClient,
  system.Net.URLClient,
 
  XSuperObject
  ;
 
type
  THttpApi = class
  protected
    FURI: TURI;
  public
    BaseURL : string;
    Headers: TNameValueArray;
    ResStream: TStringStream;
    SrcStream: TStringStream;
    Token : string;
    Response : IHTTPResponse;
    ResultString : string;
    ResultJson : ISuperObject;
 
    UrlParams : TStringList;
    QueryParams : TStringList;
 
    constructor Create(ABaseUrl: string); virtual;
    destructor Destroy(); override;
 
    procedure ClearParams;
    procedure SetHeaders(); virtual;
    function MakeUrl : string;
 
    procedure Request(AHttpMethod: string);
  end;
 
 
implementation
 
{ THttpApi }
 
constructor THttpApi.Create(ABaseUrl : string);
begin
  BaseURL := ABaseUrl;
  UrlParams := TStringList.Create;
  UrlParams.Delimiter := '/';
  QueryParams := TStringList.Create;
  QueryParams.Delimiter := '&';
  ResStream := TStringStream.Create('', TEncoding.UTF8);
  SrcStream := TStringStream.Create('', TEncoding.UTF8);
end;
 
destructor THttpApi.Destroy;
begin
  FreeAndNil(UrlParams);
  FreeAndNil(QueryParams);
  FreeAndNil(SrcStream);
  FreeAndNil(ResStream);
  inherited;
end;
 
 
function THttpApi.MakeUrl: string;
var
  s : String;
  name : String;
  i : integer;
begin
  s := IncludeTrailingPathDelimiter(BaseURL) + UrlParams.DelimitedText;
  // FURI это RECORD поэтому не нужно FURI := TUri.Create(s).
  // можно просто вызвать Create для инициализации полей
  FURI.Create(s);
  for i := 0 to QueryParams.Count-1 do
  begin
    name := QueryParams.Names[i];
    FURI.AddOrSetParamByName(name,QueryParams.Values[name]);
  end;
  result := FURI.ToString;
end;
 
// перед вызовом Request уже подготовлены все данные в SrcStream 
// и заданы UrlParams и QueruParams
procedure THttpApi.Request(AHttpMethod: string);
var
  http : THttpClient;
  url : string;
begin
  http := THttpClient.Create;
  try
 
    url := MakeUrl();
    SetHeaders();
 
    if AHttpMethod = 'POST' then
    begin
      Response := http.Post(url, SrcStream, ResStream, Headers);
    end
    else if AHttpMethod = 'GET' then
    begin
      /// Response := http.get .....
    end
    else if AHttpMethod = 'PUT' then
    begin
      /// Response := http.put .....
    end;
 
    if (Response.StatusCode div 100) <> 2 then
    begin
       // если все прошло нормально, код должен быть 2xx (201, 202...) https://ru.wikipedia.org/wiki/Список_кодов_состояния_HTTP
      raise Exception.Create('Http request error');
    end;
 
    ResultString := Response.ContentAsString(TEncoding.UTF8);
    /// Если все хорошо, то можно попытаться из ResultString получить ResultJson
    try
      ResultJson := SO(ResultString);
    except
      /// если строка - не JSON, здесь можно обработать ошибку
    end;
 
  finally
    FreeAndNil(http);
  end;
end;
 
procedure THttpApi.SetHeaders;
begin
  SetLength(Headers, 3);
  Headers[0].Name := 'Content-Type';
  Headers[0].Value := 'application/json';
  Headers[1].Name := 'Accept';
  Headers[1].Value := 'application/json';
  Headers[2].Name := 'Authorization';
  Headers[2].Value := Token;
end;
 
procedure THttpApi.ClearParams;
begin
  UrlParams.Clear;
  QueryParams.Clear;
  SrcStream.Clear;
end;
 
end.

в приведенном коде реализован только метод POST, вызовы остальных реализуйте сами по аналогии
отличается только GET, куда не передается SrcStream, т.к. у него нет тела запроса.

Реализация конкретного API
Итак, инструмент у нас теперь есть, давайте посмотрим как его использовать.
Делать это нужно в соответствии с документацией на ваш API.

Заведем отдельный модуль и класс-наследник общего THttpApi. Этот модуль должен использовать объекты вашей модели данных, предположим, они описаны в модуле MyClasses.pas
Предположим, у нас есть API на адресе https://server.com/api/v2/
и токен нам дали при регистрации на сайте - XXX-XXX-YYYY
и его нужно помещать в заголовок вида Authorization: Bearer XXX-XXX-YYYY

Кликните здесь для просмотра всего текста
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
unit My.Api;
 
interface
uses
  System.Classes,
  System.SysUtils,
 
  system.Net.HttpClient,
 
  HttpApi,
  MyClasses
  ;
 
type
  TMyApi = class(THttpApi)
  public
    constructor Create();
  end;
 
implementation
 
{ TMyApi }
 
constructor TMyApi.Create;
begin
  inherited Create('https://server.com/api/v2/');
  token := 'Bearer XXX-XXX-YYYY';
end;
 
 
end.

теперь разбирая документацию API мы видим метод "добавить заказ"
POST /orders/
туда надо передать JSON
JSON
1
2
3
4
5
6
7
8
9
10
11
12
{ items: 
  [
  { "id" : 12,
     "name":"Товар 1",
     "qty" : 5
  },
  { "id" : 777,
     "name":"Товар 2",
     "qty" : 11
  }
  ]
}
оттуда придет JSON
JSON
1
2
3
4
{
  "result":"success",
  "orderId":174839
}
ессно у нас в модели данных (MyClasses.pas) должен быть подходящий класс
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
TOrderItem = class
  id : integer;
  name : string;
  qty : integer;
end;
 
TOrder = class
  items : TArray<TOrderItem>;
end;
 
TOrderRequestResult = class
  result : string;
  orderId : integer;
end;
Все готово для реализации метода "разместить заказ"

Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
procedure TMyApi.PlaceOrder(AOrder : TOrder; AResult : TOrderRequestResult);
var
  s : String;
begin
  ClearParams;
  UrlParams.Add('orders');
  s := AOrder.AsJSON();
  SrcStream.WriteString(s);
  SrcStream.Position := 0;
  Request('POST');
  // результат уходит из JSON в переменную AResult
  // иначе она остается как есть
  if ResultJson <> NIL then
    AResult.AssignFromJSON(ResultJson);
end;
мы подготовили параметры и сделали вызов Request('POST')
если все норм, то у нас есть ResultJson в котором есть поля result и orderId

в самой программе мы можем единожды создать наш объект
и пользоваться им все время
Delphi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var api : TMyAPI;
..
api :=  TMyAPI.Create();
 
 
....
order := TOrder.Create();
Setlength(order.items, 2);
order.items[0] :=  TOrderItem.Create();
order.items[0].id :=12;
order.items[0].name := 'Товар 1';
order.items[0].qty := 5;
order.items[1] :=  TOrderItem.Create();
order.items[1].id := 777;
order.items[1].name := 'Товар 2';
order.items[1].qty := 11;
res := TOrderRequestResult.Create;
api.PlaceOrder(order, res);
// в res - результат вызова. в т.ч. id заказа
if res.result = 'success' then
  newOrderId := res.orderId;
// теперь разрушим все что не нужно больше
for i := 0 to length(order.items) - 1 do
  order.items[i].free;
FreeAndNil(order);
FreeAndNil(res);
P.S.
в коде могут быть неточности, условности и недостача кусков, потому что это не реальный проект, а лишь макет
Метки delphi, http, rest api, web api
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
Часы с будильником на основе класса QLCDNumber
Uhbif79 12.08.2026
Всем добрый день, выкладываю программу часов с будильником на основе класса QLCDNumber. Здесь я пробовал самостоятельно создавал классы, впервые столкнулся с видимостью переменной одного класса из. . .
Установка MinGW GCC 16.2 и CMake
8Observer8 10.08.2026
VK Видео: https:/ / vkvideo. ru/ video-240781534_456239017 YouTube: eY5-5PyI9NM Текстовая версия
Неделя из жизни имитационной модели склада: мои кривые руки растут, откуда надо
anaschu 10.08.2026
Неделя из жизни имитационной модели склада: как я почти написал неправильную логику и что с этим делать Работаю сейчас над учебно-рабочим проектом: строю в AnyLogic имитационную модель процессов. . .
Калькулятор для расчета родства
russiannick 07.08.2026
1. Задача: Создать калькулятор для расчета родства. Родственных связей существует 8 ступеней, такие как: p - отец P - мать q - муж Q - жена b - брат B - сестра s - сын S - дочь
Мир по моей воле
kumehtar 07.08.2026
Когда-то кажется, что всё просто. Ты весь такой светлый. Причиняешь добро. Борешься за справедливость в этом тёмном мире. Потом начинаешь замечать одну неприятную вещь. Почти каждый хороший. . .
Кредитный калькулятор
Maks 05.08.2026
Решение задачи по прикладной информатике средствами 1С. Задача: Напишите приложение-калькулятор, которое помогает рассчитывать параметры кредита для аннуитетного и дифференцированного видов. . .
У нас сейчас поговорку "Опять 25" нужно переделать на "Опять +35".
kumehtar 04.08.2026
С ностальгией вспоминаю времена моего детства, когда у нас и правда +25 - была максимальная температура летом. Раньше +25 °C реально казались вершиной жары, когда можно было весь день пропадать на. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru