From 75b9b7df84c1e01b79ebbe5b23d8965e7eae9f15 Mon Sep 17 00:00:00 2001 From: Vladislav Moiseev Date: Wed, 25 Oct 2023 23:53:03 +0400 Subject: [PATCH] Add lw3 --- README.md | 2 +- labs/lab_3/README.md | 127 ++++++++++++++++++++++++++++++++++ labs/lab_3/example_1.cs | 76 ++++++++++++++++++++ labs/lab_3/example_2.cs | 26 +++++++ labs/lab_3/example_nginx.conf | 21 ++++++ 5 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 labs/lab_3/README.md create mode 100644 labs/lab_3/example_1.cs create mode 100644 labs/lab_3/example_2.cs create mode 100644 labs/lab_3/example_nginx.conf diff --git a/README.md b/README.md index 1e87e47..1c92238 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/labs/lab_3/README.md b/labs/lab_3/README.md new file mode 100644 index 0000000..97ca17c --- /dev/null +++ b/labs/lab_3/README.md @@ -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). diff --git a/labs/lab_3/example_1.cs b/labs/lab_3/example_1.cs new file mode 100644 index 0000000..5e35792 --- /dev/null +++ b/labs/lab_3/example_1.cs @@ -0,0 +1,76 @@ +List 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(); + +/// +/// Сама сущность. +/// +public class TaskEntity +{ + public Guid Uuid { get; set; } + public string Subject { get; set; } = string.Empty; + public bool IsCompleted { get; set; } +} + +/// +/// DTO-класс для описания сущности в API. +/// +public class TaskEntityDto : TaskEntity { } + +/// +/// Запрос на создание сущности. +/// +public class TaskCreateRequest +{ + public string Subject { get; set; } = string.Empty; + public bool IsCompleted { get; set; } +} + +/// +/// Запрос на изменение сущности. +/// +public class TaskEditRequest +{ + public string Subject { get; set; } = string.Empty; + public bool IsCompleted { get; set; } +} diff --git a/labs/lab_3/example_2.cs b/labs/lab_3/example_2.cs new file mode 100644 index 0000000..1de87e7 --- /dev/null +++ b/labs/lab_3/example_2.cs @@ -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>(remoteHost); + if (response == null) + return Results.BadRequest(); + return Results.Ok($"Соседний микросервис отдал следующие задачи: {string.Join(", ", response.Select(i => i.Subject))}"); +}); + +app.Run(); + +/// +/// DTO-класс для описания сущности из другого микросервиса. +/// +public class TaskEntityDto +{ + public Guid Uuid { get; set; } + public string Subject { get; set; } = string.Empty; + public bool IsCompleted { get; set; } +} diff --git a/labs/lab_3/example_nginx.conf b/labs/lab_3/example_nginx.conf new file mode 100644 index 0000000..9d3e2b3 --- /dev/null +++ b/labs/lab_3/example_nginx.conf @@ -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; + } +}