lw4 #2
@ -7,7 +7,7 @@
|
||||
|
||||
1. [Знакомство с docker и docker-compose](labs/lab_1/README.md)
|
||||
2. [Разработка простейшего распределённого приложения](labs/lab_2/README.md)
|
||||
3. TBA
|
||||
3. [REST API, Gateway и синхронный обмен между микросервисами](labs/lab_3/README.md)
|
||||
4. TBA
|
||||
5. TBA
|
||||
6. TBA
|
||||
|
127
labs/lab_3/README.md
Normal file
127
labs/lab_3/README.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
|
||||
|
||||
Цель: изучение шаблона проектирования gateway, построения синхронного обмена между микросервисами и архитектурного стиля RESTful API.
|
||||
|
||||
Задачи:
|
||||
|
||||
1. Создать 2 микросервиса, реализующих CRUD на связанных сущностях.
|
||||
2. Реализовать механизм синхронного обмена сообщениями между микросервисами.
|
||||
3. Реализовать шлюз на основе прозрачного прокси-сервера nginx.
|
||||
|
||||
## Создание микросервисов
|
||||
|
||||
Создать два микросервиса.
|
||||
|
||||
Каждый сервис реализует CRUD-операции: список записей, подробности конкретной записи, создание, удаление и изменение записи.
|
||||
|
||||
В качестве хранилища данных может выступать оперативная память приложения или база данных.
|
||||
|
||||
Сущности необходимо подобрать по следующим критериям:
|
||||
|
||||
1. Они должны быть связаны с предполагаемой темой диплома.
|
||||
2. Они должны быть связаны как `1-ко-многим``.
|
||||
|
||||
> Например, одна сущность - книга в библиотеке, вторая - абонемент.
|
||||
>
|
||||
> Поля абонемента: УИД (уникальный идентификатор) абонемента, Номер, ФИО читателя, Дата выдачи.
|
||||
> Поля книги: УИД книги, Автор, Название, Год издания, УИД абонемента, на котором сейчас находится книга.
|
||||
|
||||
Общение между сервисами должно происходить при помощи API, реализованного при помощи архитектурного стиля проектирования REST.
|
||||
То есть необходимо реализовать следующие endpoints:
|
||||
|
||||
1. "GET /" - Получение списка записей.
|
||||
Должен возвращать массив моделей.
|
||||
2. "GET /{uuid}" - Получение подробностей записи по УИД (uuid).
|
||||
Должен возвращать модель или ошибку 404.
|
||||
3. "POST /" - Создание новой записи.
|
||||
Принимает на вход данные. Должен возвращать модель из п.2.
|
||||
4. "PUT /{uuid}" - Обновление записи по УИД.
|
||||
Принимает на вход новые данные. Должен возвращать модель из п.2 или ошибку 404.
|
||||
5. "DELETE /{uuid}" - Удаление записи по УИД.
|
||||
Возвращает коды 200 при успехе и 404, если запись не найдена.
|
||||
|
||||
Пример модели абонемента для списка и подробностей:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "8f036445-a5bd-401c-926e-840f9de795cd",
|
||||
"number": 135,
|
||||
"fullName": "Иванов И.И.",
|
||||
"issued": "2023-10-18T05:41:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Пример модели абонемента для создания или изменения:
|
||||
|
||||
```json
|
||||
{
|
||||
"number": 135,
|
||||
"fullName": "Иванов И.И.",
|
||||
"issued": "2023-10-18T05:41:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
> Как видите, нет УИДа, т.к. он присваивается системой, и изменять мы его не можем.
|
||||
|
||||
Пример модели книги для списка:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "8740d660-b251-4272-8535-be7ec3748d4b",
|
||||
"author": "J.K.R.",
|
||||
"subject": "HP and PS",
|
||||
"year": 1997,
|
||||
"subscriptionUuid": "8f036445-a5bd-401c-926e-840f9de795cd"
|
||||
}
|
||||
```
|
||||
|
||||
Пример модели книги для подробностей:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "8740d660-b251-4272-8535-be7ec3748d4b",
|
||||
"author": "J.K.R.",
|
||||
"subject": "HP and PS",
|
||||
"year": 1997,
|
||||
"subscriptionUuid": "8f036445-a5bd-401c-926e-840f9de795cd",
|
||||
"subscriptionInfo": {
|
||||
"number": 135,
|
||||
"fullName": "Иванов И.И.",
|
||||
"issued": "2023-10-18T05:41:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Как видите, модель сильно богаче, чем при запросе списка.
|
||||
>
|
||||
> Как раз здесь и нужен синхронный обмен между сервисами.
|
||||
|
||||
Пример модели книги для создания или изменения:
|
||||
|
||||
```json
|
||||
{
|
||||
"author": "J.K.R.",
|
||||
"subject": "HP and PS",
|
||||
"year": 1997,
|
||||
"subscriptionUuid": "8f036445-a5bd-401c-926e-840f9de795cd"
|
||||
}
|
||||
```
|
||||
|
||||
Как понять, куда обращаться микросервису книг для получения данных об абонементах?
|
||||
Нам здесь поможет docker compose: код в "service" совпадает с хостом, куда следует обращаться.
|
||||
|
||||
## Реализация синхронного обмена
|
||||
|
||||
Как реализовать непосредственно работу с endpoints?
|
||||
Например, использовать ASP.NET Core Minimal APIs.
|
||||
[Небольшой пример, который это показывает](./example_1.cs).
|
||||
|
||||
Как вызвать синхронно данные с соседнего микросервиса?
|
||||
Через HTTP-клиент!
|
||||
[Небольшой пример, который это показывает](./example_2.cs).
|
||||
|
||||
## Реализация gateway при помощи nginx
|
||||
|
||||
Один сервер, несколько location, proxy_pass по хосту из docker-compose.yml, открытые порты наружу и... всё.
|
||||
|
||||
[Пример файла с настройкой nginx](./example_nginx.conf).
|
76
labs/lab_3/example_1.cs
Normal file
76
labs/lab_3/example_1.cs
Normal file
@ -0,0 +1,76 @@
|
||||
List<TaskEntity> tasks = new()
|
||||
{
|
||||
new TaskEntity() { Uuid= Guid.NewGuid(), Subject = "Сдать третью лабу", IsCompleted = false },
|
||||
};
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
// Массив сущностей.
|
||||
app.MapGet("/", () =>
|
||||
{
|
||||
return tasks.Select(t => new TaskEntityDto()
|
||||
{
|
||||
Uuid = t.Uuid,
|
||||
Subject = t.Subject,
|
||||
IsCompleted = t.IsCompleted,
|
||||
});
|
||||
});
|
||||
|
||||
// Подробности сущности.
|
||||
app.MapGet("/{uuid}", (Guid uuid) =>
|
||||
{
|
||||
var task = tasks.FirstOrDefault(t => t.Uuid == uuid);
|
||||
if (task == null)
|
||||
return Results.NotFound();
|
||||
return Results.Json(new TaskEntityDto()
|
||||
{
|
||||
Uuid = task.Uuid,
|
||||
Subject = task.Subject,
|
||||
IsCompleted = task.IsCompleted,
|
||||
});
|
||||
});
|
||||
|
||||
// Создание сущности.
|
||||
app.MapPost("/", () => { throw new NotImplementedException(); });
|
||||
|
||||
// Изменение сущности.
|
||||
app.MapPut("/{uuid}", () => { throw new NotImplementedException(); });
|
||||
|
||||
// Удаление сущности.
|
||||
app.MapPost("/", () => { throw new NotImplementedException(); });
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>
|
||||
/// Сама сущность.
|
||||
/// </summary>
|
||||
public class TaskEntity
|
||||
{
|
||||
public Guid Uuid { get; set; }
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO-класс для описания сущности в API.
|
||||
/// </summary>
|
||||
public class TaskEntityDto : TaskEntity { }
|
||||
|
||||
/// <summary>
|
||||
/// Запрос на создание сущности.
|
||||
/// </summary>
|
||||
public class TaskCreateRequest
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запрос на изменение сущности.
|
||||
/// </summary>
|
||||
public class TaskEditRequest
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
26
labs/lab_3/example_2.cs
Normal file
26
labs/lab_3/example_2.cs
Normal file
@ -0,0 +1,26 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHttpClient(); // Добавляем библиотеку по работе с HTTP.
|
||||
var app = builder.Build();
|
||||
|
||||
// Да, по заданию надо будет запрашивать метод /{uuid}. Тут запрашивается список.
|
||||
app.MapGet("/", async (IHttpClientFactory httpClientFactory) =>
|
||||
{
|
||||
var remoteHost = "http://localhost:5158"; // Адрес, по которому развёрнут микросервис с задачами. В docker compose тут будет имя service.
|
||||
var client = httpClientFactory.CreateClient();
|
||||
var response = await client.GetFromJsonAsync<List<TaskEntityDto>>(remoteHost);
|
||||
if (response == null)
|
||||
return Results.BadRequest();
|
||||
return Results.Ok($"Соседний микросервис отдал следующие задачи: {string.Join(", ", response.Select(i => i.Subject))}");
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>
|
||||
/// DTO-класс для описания сущности из другого микросервиса.
|
||||
/// </summary>
|
||||
public class TaskEntityDto
|
||||
{
|
||||
public Guid Uuid { get; set; }
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public bool IsCompleted { get; set; }
|
||||
}
|
21
labs/lab_3/example_nginx.conf
Normal file
21
labs/lab_3/example_nginx.conf
Normal file
@ -0,0 +1,21 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
location /test/ {
|
||||
proxy_pass http://test:80/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /test;
|
||||
}
|
||||
|
||||
location /admin/ {
|
||||
proxy_pass http://admin:80/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Prefix /admin;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user