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

Блог. Двадцать пять лет Делфи-практики

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

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

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

Реализуем вызовы HTTP 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;
и метод, который эти заголовки заполняет.
естественно, виртуальный, ведь мы не знаем, какие заголовки потребует потом конкретный 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.
в коде могут быть неточности, условности и недостача кусков, потому что это не реальный проект, а лишь макет

Всего комментариев 0

Комментарии