21 марта 2021
Контрольная сумма от набора байт позволяет убедиться в том, что данные на клиенте, полученные от сервера, являются корректными. Для этого вместе с файлом сервер может предоставлять контрольную сумму для проверки на клиентской стороне. Существует несколько алгоритмов вычисления контрольной суммы, рассмотрим самые популярные: md5, sha-256, sha-512 и crc-32.
В пакете java.security есть такой класс как MessageDigest. Он позволяет получить одну из встроенных реализаций алгоритма вычисления контрольных сумм. Поэтому сначала реализуем метод, который абстрагирован от конкретного алгоритма и работает с любым MessageDigest одинаково.
Метод checksumForDigest() получает два параметра: полное имя файла и объект MessageDigest. Затем используем конструкцию try-with-resources, в котором последовательно создаём три потока, «оборачивая» один в другой. По выходу из блока try эти потоки будут закрыты автоматически.
Сначала создаётся поток FileInputStream, связанный непосредственно с целевым файлом, для которого считаем контрольную сумму.
Затем этот поток оборачиваем в BufferedInputStream, чтобы добавить буфер потока и значительно ускорить работу с файловой системой. Оптимизация работает за счёт того, что мы не грузим массив байт из файла целиком в память, а читаем его небольшими порциями. Таким образом мы можем читать сколь угодно большой файл, не боясь исчерпать всю оперативную память.
Наконец, этот второй буферизованный поток оборачиваем в DigestInputStream, который и реализует механизм подсчёта контрольной сумму в соответствии с заданным в MessageDigest алгоритмом.
Вы можете убедиться, что если убрать BufferedInputStream из этой цепочки, то при работе с большими файлами задержка окажется довольно заметной.
Кстати, такая «матрёшка» из потоков является типичным примером паттерна «Декоратор», который позволяет наращивать функциональность объекта, не меняя исходный интерфейс.
Затем в цикле читаем файл. Тело цикла при этом должно быть пустым, т.к. каких-то специальных действий на каждой итерации нам делать не нужно – всё необходимое делает DigestInputStream.
После завершения чтения файла получаем новый объект MessageDigest с помощью метода getMessageDigest(). Чтобы получить шестнадцатеричную строку хеша, выполним следующее преобразование:
Теперь осталось передать абсолютный путь до целевого файла и требуемую реализацию алгоритма md-5.
Объект messageDigest получаем с помощью метода getInstance(). Теперь запустим наш пример и в консоли увидим строку в шестнадцатеричном формате. Для проверки в linux-системах можно использовать консольную утилиту md5sum. В результате в консоли увидим нечто подобное:
Семейство алгоритмов SHA-2 (Secure Hash Algorithm Version 2 – безопасный алгоритм хеширования, версия 2) включает в себя наиболее популярные sha-256 и sha-512. Мы также можем вычислять их с помощью метода, рассмотренного выше.
Как видите, метод main() почти не изменился. Мы просто поменяли параметр getInstance() на «SHA-256». Вычисление SHA-512 происходит аналогично.
Для проверки полученных значений в консоли можем воспользоваться утилитами sha256sum и sha512sum соответственно:
Как видите, хэш SHA-512 длиннее, а потому безопаснее и рекомендуется использовать именно его.
Теперь перейдём к хэшу crc-32 (Cyclic redundancy check). Метод его вычисления ещё проще:
Метод на вход получает абсолютное имя файла и объект с интерфейсом Checksum из пакета java.util.zip. Java предлагает несколько реализаций Checksum, одной из которых является класс CRC32.
Здесь мы также используем конструкцию try-with-resources из трёх потоков, как и в предыдущем примере. FileInputStream связан с целевым файлом, BufferedInputStream обеспечивает буферизацию для ускорения обработки больших файлов, а CheckedInputStream как раз позволяет вычислять контрольную сумму с помощью объекта Checksum.
Тут у нас также используется пустой цикл, который считывает файл до конца. После завершения чтения файла мы получаем контрольную сумму в виде целого числа с помощью метода getChecksum().getValue(). Для того, чтобы привести это число в шестнадцатеричный формат, воспользуемся методом Long.toHexString().
Наш метод main() для вычисления контрольной суммы crc-32 будет выглядеть так:
Второй реализацией интерфейса Checksum является класс CRC32C – одна из разновидностей алгоритма CRC.
Теперь запустим наш пример и получим контрольную сумму в шестнадцатеричном формате. Для её проверки воспользуемся консольной утилитой crc32:
Как видите, в данном случае код довольно компактный.
Мы увидели, что «из коробки» Java предоставляет реализации всех популярных алгоритмов для вычисления контрольных сумм, причём от нас не требуется разбираться в их внутреннем устройстве – достаточно лишь использовать соответствующий поток. Также для ускорения обработки больших файлов нужно не забывать про буферизацию.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.