Постановка задачи


Периодически у меня возникает задача поделиться файлами по локальной сети, например, с коллегой по проекту.


Решений для этого может быть очень много — Samba / FTP / scp. Можно просто залить файл в общедоступное публичное место типа Google Drive, приложить к задаче в Jira, или даже отправить письмом.


Но все это в той или иной степени негибко, где-то требует предварительной настройки и имеет свои ограничения (например, максимальный размер вложения).


А хочется чего-то более легковесного и гибкого.


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


Скажем, часто вышеозначенную задачу я решал используя системный питон следующим однострочником


$ python3 -mhttp.server
Serving HTTP on 0.0.0.0 port 8000 ...

Эта команда стартует веб-сервер в текущей папке и позволяет через веб-интерфейс получить список файлов и скачать их. Больше подобных штук можно отсыпать тут.


Неудобств тут несколько. Теперь чтоб передать ссылку на скачивание коллеге вам надо знать свой IP адрес в сети.


Для этого удобно использовать команду


$ ifconfig -a 

И потом из полученного списка сетевых интерфейсов выбрать подходящий и вручную скомпоновать ссылку вида http://IP:8000, которую и отправить.


Второе неудобство: этот сервер однопоточен. Это значит что пока один ваш коллега качает файл, второй даже не сможет загрузить список файлов.


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


$ mkdir tmp1
$ cp file.zip tmp1
$ cd tmp1
$ python3 -mhttp.server

Четвертое неудобство — нет простого способа скачать все содержимое папки.


Для передачи содержимого папки обычно применяют приём называемый tar pipe.


Делают примерно так:


$ ssh user@host 'cd /path/to/source && tar cf - .' | cd /path/to/destination && tar xvf -

Если вдруг непонятно, поясню как это работает. Первая часть команды tar cf - . созраёт архив содержимого текущей папки и пишет в стандартный вывод. Дальше этот вывод через pipe передается по защищенному ssh каналу на вход похожей команды tar xvf - которая делает обратную процедуру, т.е. читает стандартный вход и разархивирует в текущую папку. Фактически происходит передача архива файлов, но без создания промежуточного файла!


Очевидно и неудобство такого подхода. Нужен ssh доступ с одной машины на другую, а это в общем случае почти никогда не выполняется.


А можно ли достичь все вышеперечисленное, но без этих описанных проблем?


Итак, пришло время формализовать, что будем строить:


  1. Программу, которую легко установить (статический бинарник)
  2. Которая позволит передавать как файл так и папку со всем содержимым
  3. С опциональным сжатием
  4. Которая позволит принимающей стороне скачать файл(ы) используя лишь стандартные *nix инструменты (wget/curl/tar)
  5. Программа будет после запуска сразу выдавать точные команды для скачивания

Решение


На конференции JEEConf, которую я посетил не так давно, тема Graal поднималась неоднократно. Тема далеко не новая, но для меня это явилось спусковым крючком чтоб наконец пощупать этого зверя собственноручно.


Для тех кто еще не в теме (неужели еще есть такие? oO) напомню, что GraalVM это такая прокачанная JVM от Oracle с дополнительными возможностями, самые заметные из которых:


  1. Полиглотная JVM — возможность бесшовного совместного запуска Java, Javascript, Python, Ruby, R, и т.д. кода
  2. Поддержка AOT-компиляции — компиляция Java прямо в нативный бинарник
  3. Менее заметная, но очень крутая фишка — C2 компилятор переписан с C++ на Java с целью более удобной его дальнейшей разработки. Это уже дало заметные плоды. Этот компилятор производит гораздо больше оптимизаций на стадии преобразования байткода Java в нативный код. Например, он способен более эффективно устранять аллокации. В Twitter смогли понизить потребление CPU на 11% просто включив эту настройку, что в их масштабах дало заметную экономию ресурсов (и денег).

Освежить представление о Graal можно, например, в этой хабра-статье.


Писать будем на Java, поэтому для нас самой релевантой возможностью будет AOT-компиляция.


Собственно, результат разработки представлен в этом Github репозитории.


Пример использования для передачи одного файла:


$ serv '/path/to/report.pdf' 
To download the file please use one of the commands below: 

curl http://192.168.0.179:17777/dl > 'report.pdf'
wget -O- http://192.168.0.179:17777/dl > 'report.pdf'
curl http://192.168.0.179:17777/dl?z --compressed > 'report.pdf'
wget -O- http://192.168.0.179:17777/dl?z | gunzip > 'report.pdf'

Пример использования при передаче содержимого папки (все файлы включая вложенные!):


$ serv '/path/to/folder' 
To download the files please use one of the commands below. 
NB! All files will be placed into current folder!

curl http://192.168.0.179:17777/dl | tar -xvf -
wget -O- http://192.168.0.179:17777/dl | tar -xvf -
curl http://192.168.0.179:17777/dl?z | tar -xzvf -
wget -O- http://192.168.0.179:17777/dl?z | tar -xzvf -

Да, так просто!


Обратите внимание — программа сама определяет правильный IP адрес на котором будут доступны файлы для скачивания.


Наблюдения / Размышления


Понятно, что одной из целей при создании программы была её компактность. И вот какого результата удалось достичь:


$ du -hs `which serv`
2.4M    /usr/local/bin/serv 

Невероятно, но вся JVM вместе с кодом приложения уместилась в жалкие несколько мегабайт! Конечно, все несколько не так, но об этом далее.


На самом деле компилятор Graal выдает бинарник размером несколько более 7 мегабайт. Я же решил дополнительно сжать его UPX-ом.


Это оказалось удачной идеей, посколько время запуска возрасло при этом очень несущественно:


Несжатый вариант:


$ time ./build/com.cmlteam.serv.serv -v
0.1

real    0m0.001s
user    0m0.001s
sys     0m0.000s

Сжатый:


$ time ./build/serv -v
0.1

real    0m0.021s
user    0m0.021s
sys     0m0.000s

Для сравнения, время запуска "традиционным способом":


$ time java -cp "/home/xonix/proj/serv/target/classes:/home/xonix/.m2/repository/commons-cli/commons-cli/1.4/commons-cli-1.4.jar:/home/xonix/.m2/repository/org/apache/commons/commons-compress/1.18/commons-compress-1.18.jar" com.cmlteam.serv.Serv -v
0.1

real    0m0.040s
user    0m0.030s
sys     0m0.019s

Как видим, в два раза медленнее UPX-варианта.


Вообще, малое время старта — одна из сильнейших сторон GraalVM. Этим, а также низким потреблением памяти обусловлен существенный энтузиазм вокруг использования этой технологии для микросервисов и serverless.


Я старался делать логику программы максимально минималистичной и использовать минимум библиотек. В принципе, такой подход вообще оправдан, а в данном случае у меня были опасения, что добавление сторонних maven зависимостей существенно "утяжелит" результирующий файл программы.


Например, поэтому я не использовал стороннюю зависимость для веб-сервера на Java (а таковых множество на любой вкус и цвет), а воспользовался встроенной в JDK реализацией веб-сервера из пакета com.sun.net.httpserver.*. Вообще-то, использование пакета com.sun.* считается моветоном, но я посчитал это допустимым в данном случае, поскольку я компилирую в нативный код, и, значит, вопрос о совместимости между JVM не стоит.


Однако мои опасения оказались вполне напрасными. В программе я для удобства использовал две зависимости


  1. commons-cli — для парсинга аргументов командной строки
  2. commons-compress — для генерации tar-архива папки и опционального gzip-сжатия

При этом размер файла возрос весьма незначительно. Рискну предположить, что компилятор Graal весьма умен чтоб не помещать в исполняемый файл все подключаемые jar-ники, а лишь тот код из них который реально используется кодом приложения.


Компиляция в нативный код на Graal выполняется утилитой native-image. Стоит упомянуть что процесс этот ресурсоёмкий. Скажем, на моей не очень медленной конфигурации с CPU Intel 7700K на борту этот процесс занимает 19 секунд. По-этому рекомендую при разработке запускать программу как обычно (через java), а бинарник собирать на конечном этапе.


Выводы


Эксперимент, как мне кажется, оказался весьма удачен. При разработке, используя инструментарий Graal, я не столкнулся с какими-то непреодолимыми или даже существенными проблемами. Все работало предсказуемо и стабильно. Хотя почти наверняка все будет не так гладко если вы попытаетесь собрать таким образом что-то более сложное, например, приложение на Spring Boot. Тем не менее уже представлен ряд платформ в которых заявлена нативная поддержка Graal. Среди них Micronaut, Microprofile, Quarkus.


Что касается дальнейшего развития проекта — уже готов список улучшений, запланированных для версии 0.2. Также в данный момент реализована сборка финального бинарника только под Linux x64. Надеюсь что это упущение будет исправлено в будущем, тем более что компилятор native-image из Graal поддерживает MacOS и Windows. К сожалению, он не поддерживает пока кросс-компиляцию, что могло бы существенно облегчить дело.


Надеюсь, что представленная утилита будет полезна хотя-бы кому-то из уважаемого хабра-сообщества. Буду рад вдвойне если найдутся желающие законтрибутить в проект.

Комментарии (3)


  1. Uhehesh
    13.05.2019 14:48

    Для передачи содержимого папки обычно применяют приём называемый tar pipe.
    Почему не scp?


    1. xonix Автор
      13.05.2019 14:51

      Тоже вариант, да. Правда, scp копирует по одному файлу а tar pipe все шлет одним потоком, который еще можно опционально сжать.


  1. Googolplex
    13.05.2019 23:55

    Еще rsync есть, тоже позволяет слать директории по сети, и быстро. Но он принципиально не отличается от scp/ssh, т.к. он тоже будет работать через ssh в данном случае.


    Вообще-то, использование пакета com.sun.* считается моветоном

    Это, кстати, не совсем так. В com.sun.*, в отличие от sun.*, нет ничего зазорного, т.к. com.sun.* это вполне себе публичное и задокументированное API; это такая же библиотека, как и все остальное что вы можете притащить из репозиториев, просто распространяется вместе с Oracle JVM (не знаю точно, есть ли она в OpenJDK, но скорее всего да).