Технология передачи данных в секретный контур

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Что такое секретный контур?

Это компьютер, который отделён от сети через «диод» (устройство однонаправленной передачи данных). Из него ничего не может выходить, а входить может только по одному каналу, с одного разрешённог о IP, по определённым, строго перечисленным портам.

Разрешённым является IP диода, который связан с секретным контуром отдельной сетью, поднятой для этой пары серверов.

Зачем он такой нужен? Для чего делают секретный контур?

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

Как передавать данные?

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

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

Производить передачу надо несколько раз (например, 5 раз) с задержками между передачей (например, одну секунду).

Передача данных

Итак, напишем хелпер, ответственный за отправку данных. Далее тезисно...

Длина пакета

const SEND_BYTES_MAX = 1500;

Адрес для отправки

public function __construct($host, $port)

Отправка данных состоит из следующих шагов

public function send($data)
{
	$this->createSocket();  // поднять сокет для передачи через UDP
	$this->splitWithUnicode($data);  // разбить данные на блоки
	$this->sendMetadata();  // отправить метаданные
	$this->sendSplits();  // отправить блоки данных
	$this->sendEnding();  // отправить завершение транзакции
	$this->closeSocket();  // закрыть сокет
}

Создание сокета

protected function createSocket()
{
	$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
	if ($this->socket === false) {
		$errorCode = socket_last_error();
		$errorMessage = socket_strerror($errorCode);
		$this->logger->critical('Невозможно создать сокет для отправки данных.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
		throw new RuntimeException('Невозможно создать сокет для отправки данных.');
	}
}

Разбивка данных на блоки с учётом кодировки. Максимальная длина блока задаётся константой. Каждый блок будет содержать не более указанной длины байт. Кодировка нужна для того, чтобы избежать разбивки символов на разные блоки.

К сожалению, функция mb_str_split доступна только начиная с PHP 7.4.

protected function splitWithUnicode($data)
{
	$this->splits = [];
	$maxLength = self::SEND_BYTES_MAX / 2;
	$dataLength = mb_strlen($data, "UTF-8");
	for ($i = 0; $i < $dataLength; $i += $maxLength) {
		$this->splits[] = mb_substr($data, $i, $maxLength, "UTF-8");
	}
}

Отправка метаданных

protected function sendMetadata()
{
	$metadata = [
		'countSplits' => count($this->splits),  // количество блоков
	];
	if ($this->code) {
		$metadata['code'] = $this->code;  // код транзакции
	}
	if ($this->name) {
		$metadata['name'] = $this->name;  // например, имя передаваемого файла, или наименование блока
	}
	$this->sendDataWithHash(json_encode($metadata));  // Отправить JSON-строку метаданных
}

Отправка данных с хэшем. 8 байт впереди блока содержат хэш на основе передаваемых данных и прикрепляются в начало блока.

protected function sendDataWithHash($data)
{
	$hash = hash('crc32', $data);
	$hashAndData = $hash . $data;
	$this->sendData($hashAndData);
}

Отправка данных

protected function sendData($data)
{
	$sent = socket_sendto($this->socket, $data, strlen($data), 0, $this->host, $this->port);
	if ($sent === false) {
		$errorCode = socket_last_error();
		$errorMessage = socket_strerror($errorCode);
		$this->logger->warning('Данные через сокет не отправлены.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
	}
	time_sleep_until(microtime(true) + .001);  // небольшая задержка между отправками блоков по сети. Примерно получается 1,5 мегабайта за одну секунду.
}

Отправка блоков

protected function sendSplits()
{
	foreach ($this->splits as $split) {
		$this->sendDataWithHash($split);
	}
}

Отправка строки окончания

protected function sendEnding()
{
	$endingString = chr(0);
	$this->sendData($endingString);
}

Закрытие сокета

protected function closeSocket()
{
	socket_close($this->socket);
}

Весь класс под спойлером.

Hidden text
<?php

namespace Application\Workers\Udp;

use Application\Logger\AppLogger;
use RuntimeException;

/**
 * Отправка данных через UDP.
 */
class UdpSender
{
    const SEND_BYTES_MAX = 1500;

    /** @var string */
    protected $host;

    /** @var integer */
    protected $port;

    /** @var AppLogger */
    protected $logger;

    /** @var integer|string */
    protected $code;

    /** @var string */
    protected $name;

    /** @var resource */
    protected $socket;

    /** @var array */
    protected $splits;

    /**
     * @param string $host
     * @param integer $port
     */
    public function __construct($host, $port)
    {
        $this->host = $host;
        $this->port = $port;
        $this->logger = new AppLogger('Request');
    }

    /**
     * Применить код отправляемого файла.
     *
     * @param integer|string $code
     * @return UdpSender
     */
    public function withCode($code)
    {
        $this->code = $code;
        return $this;
    }

    /**
     * Применить наименование отправляемого файла.
     *
     * @param string $name
     * @return UdpSender
     */
    public function withName($name)
    {
        $this->name = $name;
        return $this;
    }

    /**
     * Отправить данные.
     *
     * @param string $data
     * @return void
     */
    public function send($data)
    {
        $this->createSocket();
        $this->splitWithUnicode($data);
        $this->sendMetadata();
        $this->sendSplits();
        $this->sendEnding();
        $this->closeSocket();
    }

    /**
     * Создать сокет для передачи данных.
     *
     * @return void
     */
    protected function createSocket()
    {
        $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        if ($this->socket === false) {
            $errorCode = socket_last_error();
            $errorMessage = socket_strerror($errorCode);
            $this->logger->critical('Невозможно создать сокет для отправки данных.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
            throw new RuntimeException('Невозможно создать сокет для отправки данных.');
        }
    }

    /**
     * Разбить строку на символы с учётом Unicode.
     *
     * @param string $data
     * @return void
     */
    protected function splitWithUnicode($data)
    {
        $this->splits = [];
        $maxLength = self::SEND_BYTES_MAX / 2;
        $dataLength = mb_strlen($data, "UTF-8");
        for ($i = 0; $i < $dataLength; $i += $maxLength) {
            $this->splits[] = mb_substr($data, $i, $maxLength, "UTF-8");
        }
    }

    /**
     * Отправить метаданные.
     *
     * @return void
     */
    protected function sendMetadata()
    {
        $metadata = [
            'countSplits' => count($this->splits),
        ];
        if ($this->code) {
            $metadata['code'] = $this->code;
        }
        if ($this->name) {
            $metadata['name'] = $this->name;
        }
        $this->logger->debug('Отправка метаданных.', ['metadata' => $metadata]);
        $this->sendDataWithHash(json_encode($metadata));
    }

    /**
     * Отправить данные с хэшем.
     *
     * @param string $data
     * @return void
     */
    protected function sendDataWithHash($data)
    {
        $hash = hash('crc32', $data);
        $hashAndData = $hash . $data;
        $this->sendData($hashAndData);
    }

    /**
     * Отправить данные.
     *
     * @param string $data
     * @return void
     */
    protected function sendData($data)
    {
        $sent = socket_sendto($this->socket, $data, strlen($data), 0, $this->host, $this->port);
        if ($sent === false) {
            $errorCode = socket_last_error();
            $errorMessage = socket_strerror($errorCode);
            $this->logger->warning('Данные через сокет не отправлены.', ['errorCode' => $errorCode, 'errorMessage' => $errorMessage]);
        }
        time_sleep_until(microtime(true) + .001);
    }

    /**
     * Отправить части данных.
     *
     * @return void
     */
    protected function sendSplits()
    {
        $this->logger->debug('Отправка данных через сокет.', ['countSplits' => count($this->splits)]);
        foreach ($this->splits as $split) {
            $this->sendDataWithHash($split);
        }
    }

    /**
     * Отправить строку окончания.
     *
     * @return void
     */
    protected function sendEnding()
    {
        $endingString = chr(0);
        $this->sendData($endingString);
    }

    /**
     * Закрыть сокет.
     *
     * @return void
     */
    protected function closeSocket()
    {
        socket_close($this->socket);
    }

}

Принятие данных на стороне секретного контура

Запускаем сервис. Описание сервиса не входит в данную статью. Если коротко, то создаётся демон, который автоматически запускается вместе с ОС. Он в свою очередь запускает наш PHP-скрипт, отвечающий за приём данных. При падении PHP-скрипта демон автоматически перезапускает сервис.
Скрипт циклически прослушивает сокет на входящие данные.

Выполнение сервиса

public function executeService()
{
	$this->connectToSocket();
	while (true) {  // Вечный цикл
		$this->receiveData();  // Получение
		$this->defineData();  // Обработка
	}
}

Подключение к сокету на адрес и порт

$this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if (@socket_bind($this->socket, $host, $port) === false) {
    // ... ошибка
	exit("Failed to bind socket on $host:$port, $errorCode: $errorMessage" . PHP_EOL);  // Завершение скрипта, чтобы сервис смог перезапуститься
}

Получение данных производится следующим образом:

protected function receiveData()
{
	@socket_recvfrom($this->socket, $this->buffer, 52428800, 0, $ip, $port);
	$this->address = "$ip:$port";
	$this->logger->debug('Приняты данные', ['address' => $this->address, 'length' => strlen($this->buffer)]);
}

Обработка данных

protected function defineData()
{
	if ($this->checkForFirstBlock()) {  // Новые данные ?
		$this->initializeTransmitting();  // Установка настроек приёма с этого адреса
	} elseif ($this->checkReceiveComplete()) {  // Конец данных ?
		$this->completeReceive();  // Завершение транзакции
		$this->resetFile();  // Сброс настроек приёма с этого адреса
	} else {
		$this->addSection();  // Добавление очередного принятого блока
	}
}

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

Сами метаданные в первом, заголовочном блоке, содержат информацию о количестве ожидаемых блоков. Также ведётся проверка на существование обязательных параметров, принятых с метаданными.

Завершающий блок состоит из одного байта и содержит нулевой символ (с кодом 0).

Какие могут быть проблемы?

  • Если метаданные придут битые, то инициализация транзакции не произойдёт. Все очередные блоки не будут распознаны, как метаданные. Блок завершения так же будет проигнорирован.

  • Если не все блоки пришли или некоторые блоки пропущены из-за несовпадения хэшей, то количество принятых блоков не сойдётся и завершение транзакции произойдёт с неудачей.

  • Если завершающий символ не придёт, то следующая транзакция будет прилеплена к данной и завершится неудачей. В ней опять же количество блоков не сойдётся.

  • С одного адреса не должно приходить несколько сообщений одновременно. Но если такое произойдёт, то блоки данных окажутся перепутаны и их количество не совпадёт с метаданными, что приведёт к отказу от приёма обеих транзакций. Одновременный приём с разных адресов будет работать.

Завершающий блок приводит к проверке всех накопленных данных, объединению и сохранению полученных данных.

Ограниченный код класса приведён ниже. В нём пропущены методы, ответственные за сохранение полученной транзакции.

Hidden text
<?php

namespace Application\Services\ReceiveFiles;

use Application\Logger\AppLogger;
use RuntimeException;

class ReceiveFiles
{

    const METADATA_KEYS_REQUIRED = ['name', 'code', 'countSplits'];

    /** @var AppLogger */
    protected $logger;

    /** @var array */
    protected $config;

    /** @var resource */
    protected $socket;

    /** @var string */
    protected $buffer;

    /** @var string */
    protected $address;

    /** @var array */
    protected $splits = [];

    /** @var array */
    protected $metadata = [];

    /** @var array */
    protected $hasErrors = [];

    public function initialize(array $config = [])
    {
        $this->logger = new AppLogger('Request');
    }

    public function initConfig(array $config)
    {
        $this->config = $config;
    }

    public function executeService()
    {
        $this->connectToSocket();
        while (true) {
            $this->receiveData();
            $this->defineData();
        }
    }

    protected function connectToSocket()
    {
        $host = $this->config['host'];
        $port = $this->config['port'];
        $this->logger->debug('Подключение к сокету.', ['host' => $host, 'port' => $port]);
        $this->socket = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        if (@socket_bind($this->socket, $host, $port) === false) {
            $errorCode = socket_last_error();
            $errorMessage = socket_strerror($errorCode);
            $this->logger->critical('Невозможно подключиться к сокету.', ['host' => $host, 'port' => $port, 'code' => $errorCode, 'message' => $errorMessage]);
            exit("Failed to bind socket on $host:$port, $errorCode: $errorMessage" . PHP_EOL);
        }
        $this->logger->info('Подключено к сокету.', ['host' => $host, 'port' => $port]);
    }

    protected function receiveData()
    {
        @socket_recvfrom($this->socket, $this->buffer, 52428800, 0, $ip, $port);
        $this->address = "$ip:$port";
        $this->logger->debug('Приняты данные', ['address' => $this->address, 'length' => strlen($this->buffer)]);
    }

    protected function defineData()
    {
        if ($this->checkForFirstBlock()) {
            $this->initializeTransmitting();
        } elseif ($this->checkReceiveComplete()) {
            $this->completeReceive();
            $this->resetFile();
        } else {
            $this->addSection();
        }
    }

    protected function checkForFirstBlock()
    {
        $firstBlock = !isset($this->splits[$this->address]);
        if ($firstBlock) {
            $this->logger->debug('Получен первый блок данных.', ['address' => $this->address]);
        }
        return $firstBlock;
    }

    protected function initializeTransmitting()
    {
        $this->splits[$this->address] = [];
        $data = $this->fetchDataFromBufferWithHash();
        $metadata = json_decode($data, true);
        if (!isset($metadata)) {
            $this->logger->error('Не удалось извлечь метаданные из первого блока.', ['address' => $this->address]);
            return;
        }

        foreach (self::METADATA_KEYS_REQUIRED as $checkKey) {
            if (empty($metadata[$checkKey])) {
                $this->logger->error('Отсутствует необходимый ключ в метаданных.', ['keyAbsent' => $checkKey, 'metadata' => $metadata]);
            }
        }

        $this->logger->debug('Сохранены метаданные.', ['address' => $this->address, 'metadata' => $metadata]);
        $this->metadata[$this->address] = $metadata;
    }

    protected function fetchDataFromBufferWithHash()
    {
        $hash = substr($this->buffer, 0, 8);
        $data = substr($this->buffer, 8);
        if ($hash !== hash('crc32', $data)) {
            $this->logger->debug('Контрольная сумма блока не совпадает!', ['address' => $this->address]);
            $this->hasErrors[$this->address] = true;
            return false;
        }
        return $data;
    }

    protected function checkReceiveComplete()
    {
        if (!isset($this->metadata[$this->address])) {
            return false;
        }
        $complete = ord($this->buffer) === 0 && strlen($this->buffer) === 1;
        if ($complete) {
            $this->logger->debug('Получен код завершения передачи файла.', ['address' => $this->address]);
        }
        return $complete;
    }

    protected function completeReceive()
    {
        if ($this->hasErrors[$this->address]) {
            $this->logger->info('Отмена сохранения результата из-за ошибок.', ['address' => $this->address, 'metadata' => $this->metadata[$this->address]]);
            return;
        }

        if ($this->metadata[$this->address]['countSplits'] !== count($this->splits[$this->address])) {
            $this->logger->info('Количество принятых секций не совпадает.', ['address' => $this->address, 'countSplits' => count($this->splits[$this->address]), 'countExpected' => $this->metadata[$this->address]['countSplits']]);
            return;
        }

        $this->logger->info('Завершён приём файла.', ['address' => $this->address, 'metadata' => $this->metadata[$this->address]]);
        // ... здесь пропущены строки. Предполагается сохранение полученных данных: implode('', $this->splits[$this->address])
    }

    protected function resetFile()
    {
        unset($this->splits[$this->address]);
        unset($this->metadata[$this->address]);
        unset($this->hasErrors[$this->address]);
    }

    protected function addSection()
    {
        $data = $this->fetchDataFromBufferWithHash();
        if ($data) {
            $this->splits[$this->address][] = $data;
            $this->logger->debug('Добавлен блок данных.', ['address' => $this->address, 'iterator' => count($this->splits[$this->address])]);
        }
    }

}

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

P.P.S. Не судите очень строго. Это моя третья попытка вырваться из песочницы.

Источник: https://habr.com/ru/articles/758338/


Интересные статьи

Интересные статьи

Привет! Меня зовут Максим Бабенко, я руковожу отделом технологий распределённых вычислений в Яндексе. Сегодня мы выложили в опенсорс платформу YTsaurus — одну из основных инфраструктурных BigData-сист...
По-видимому, именно данный эффект исторически и привел к использованию термина "регрессия".Речь пойдет о небольшом поучительном факте в области линейной регрессии, который будет показан эксперименталь...
На Joomla CMS сделано очень много сайтов для образовательных учреждений самого разного уровня и сложности. На сайты образовательных учреждений распространяется (на момент написания статьи) Приказ Росо...
Всем привет. Мы продолжаем цикл публикаций о том, как наша BI-платформа «Форсайт» работает с данными. В этой статье мы бы хотели продолжить рассказ про виртуализацию данных. И рассказать о том, как с ...
Мир онлайн-покупок становится всё привычнее, а значит, и обезличенных данных про каждого пользователя всё больше. Билайн ТВ использует для онлайн-кинотеатра рекомендательную систему на основе данных: ...