Часть 4 / Сборка сервера

Привет, добро пожаловать в часть 4 о создании базы данных с нуля.

В этой части серии мы создадим UDP-сервер, который мы описали в части 1 / Архитектура. Мы также рассмотрим основы проектирования протоколов и некоторые способы защиты серверов от злоумышленников.

Если вы только присоединяетесь, то эта часть — действительно странное место для начала, поэтому я настоятельно рекомендую сначала прочитать части 1–3:



Учебник: Создание базы данных с нуля
Часть 1 / Архитектураblog.plaintextnerds.com







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

Как обычно, вы можете получить копию кода с того места, где мы остановились в Gitlab:



Итак, давайте начнем с краткого обзора кода, который мы написали до сих пор.

Модели



Мы определили структуру ctypes под названием Sample, которая состоит из 36-байтового массива символов для идентификатора, числа с плавающей запятой двойной точности для выборочного значения и числа с плавающей запятой двойной точности для метки времени.

Чтобы облегчить себе жизнь, мы также создали универсальный класс-миксин, который добавил возможность легко конвертировать структуру в словарь.

База данных



database.py · main · Тим Армстронг / Tutorial-BuildingDatabases-Part3
GitLab.comgitlab.com



Мы открыли среду хранения, добавили подбазу данных для индекса Identity и одну для индекса Timestamp. Мы также разработали способ записи образца в базу данных и чтения образцов из базы данных путем получения всех образцов с совпадающим индексом.

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

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

ЭХО… Эхо… эхо

Итак, оглядываясь назад на нашу спецификацию из Части 1, мы признали, что аппарату нужно, чтобы мы отражали метку времени как можно быстрее, действуя как ACK (подтверждение), чтобы предотвратить повторную передачу сэмпла устройством, поэтому первый шаг в тогда создание нашего сервера будет означать создание системы, которая успешно подтверждает образцы.

Для этого мы будем использовать Twisted. Ключевое преимущество использования Twisted здесь заключается в том, что он абстрагирует весь основной цикл/реактор/слушатель, а также выделение буфера и любую реконструкцию фрагмента для нас. Это позволяет нам сосредоточиться на разработке протокола, не задумываясь об общем шаблоне сетевой обработки.

Начнем с того, что нам нужно импортировать DatagramProtocol (обработчик UDP) и reactor (слушатель основного цикла/сокета):

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

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

Использовать struct библиотеку довольно просто, есть две функции, о которых вам нужно позаботиться: pack и unpack. Эти функции принимают строку специального формата в качестве первого аргумента. Эта строка формата сообщает python, как должно выглядеть двоичное представление наших данных. Для pack за этим следуют аргументы, соответствующие как типу, так и положению, определенным в строке формата. Для unpack за ним следует буфер для чтения. Расширив метод datagramReceived, мы запишем временную метку, используя pack:

Наконец, нам нужно настроить прослушиватель и запустить основной цикл:

Чтобы проверить это, запустите файл, затем откройте консоль Python и импортируйте socket, Sample и struct:

Затем подготовьте сокет UDP и образец:

Наконец, отправьте пакет, прослушайте ответ и подтвердите полученное значение:

Красиво, работает!

UDP = неограниченные проблемы с данными…

Когда мы работаем с UDP, нам нужно помнить пару ключевых вещей:

  1. UDP не имеет состояния и не имеет концепции соединения (как в TCP).
  2. Неприбывшие пакеты UDP могут синхронизироваться — пакеты UDP могут поступать не синхронизированно (особенно если они фрагментированы).

Это означает, что нам нужно быть очень осторожными при разработке наших протоколов на основе UDP. Если мы отправляем большие объемы данных в любом направлении, нам может потребоваться убедиться, что мы можем восстановить исходный порядок (или иметь безопасный способ отбрасывать просроченные пакеты, если мы имеем дело с данными в реальном времени, такими как игры или прямые видеопотоки). Если мы отправляем ответ с сервера клиенту, нам нужно убедиться, что нас не используют как часть UDP-атаки с усилением. Это означает, что мы должны разработать наш протокол таким образом, чтобы размер первого ответа на входящее сообщение всегда был меньше или равен размеру полученного нами сообщения.

Наконец, поскольку мы никогда не можем полностью доверять происхождению UDP-пакета, необходима проверка входных данных! Самый безопасный шаг с точки зрения безопасности — отказаться отвечать на пакет, не прошедший проверку ввода, и заставить клиента повторить передачу, если он не получит ACK в течение определенного временного окна. Тем не менее, в некоторых случаях вы можете захотеть обеспечить обратное давление, если сервер перегружен или выходит из строя (эквивалентом HTTP здесь будет серия сообщений об ошибках/статусе 5xx).

В протоколе, который мы реализуем здесь, большинство этих соображений уже учтено (ответ меньше, чем входящий; если сообщение не получает ACK, устройство передает его повторно; входящие пакеты имеют фиксированный размер и структуру) . Это делает нашу проверку довольно простой. Первая, почти смехотворно тривиальная вещь, которую мы хотим проверить размер полученного пакета (который должен быть ровно 56 байт):

Затем мы хотим проверить, что он анализирует, что становится немного интереснее. Это потому, что когда мы используем метод from_buffer_copy класса Structure, мы фактически просим python обрабатывать это как необработанные байты. Поэтому, если это правильная длина, то он, вероятно, будет «анализировать», это может быть неправильно, но, как правило, он сможет втиснуть полученный поток битов в структуру, если это правильная длина.

Это означает, что нам нужно вручную проверить ввод на соответствие разумным ограничениям. Итак, каковы разумные ограничения, ну, в руководстве для устройства, с которым мы интегрируемся, говорится, что измеренные выборки будут «значением с плавающей запятой двойной точности от 0 до 1 (включительно)», так что это довольно просто, но как насчет остальные переменные? Что ж, мы знаем, что ident — это UUID в каноническом текстовом формате, поэтому мы можем проверить, правильно ли он анализируется. Наконец, мы знаем, что метка времени будет менее 1 секунды.

Итак, давайте реализуем их как функции фильтра:

И для удобства давайте расширим структуру Sample методом is_valid, который использует эти функции:

Теперь мы можем обновить метод datagramReceived нашего протокола Echo для проверки образца:

Напишите [назад к тому, с чего мы начали]

В Части 2 мы определили нашу функцию write_sample таким образом, что она принимала ident, sample и timestamp в качестве аргументов, но теперь, когда мы смотрим на реальные данные, поступающие от нашего UDP-сервиса Echo, это уже не имеет особого смысла, поэтому давайте перепишем его так, чтобы он содержал структуру Sample:

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

Наконец, мы хотим поместить вызов write_sample в наш метод datagramReceived непосредственно перед ответом; гарантируя, что мы не отправим ACK, если мы успешно не сохранили запись:

Вот и все, служба UDP завершена.

Вот и все, что касается части 4, в следующей части мы создадим агрегатор, который отправляет данные вверх по течению.

Увидимся там!

Если вам нужен окончательный код из этой части, он доступен по адресу: