Как стать автором
Обновить

Использование apr_socket_sendfile() из сервлетов под Tomcat

Время на прочтение4 мин
Количество просмотров2.2K
В этом топике расскажу о маленьком, но эффективном способе передачи файлов пользователю из сервлета по HTTP протоколу. Используется:
  • Apache Tomcat
  • Apache Portable Runtime Library
  • Apache Tomcat Native Library
  • Ваш сервлет, которому нужно отдавать файлы пользователю

Конечно, отдавать файлы сервлетом не очень хорошо с точки зрения производительности. Во-первых, отдавать статичный контент лучше всего вообще без всяких скриптов. Но иногда без этого не обойтись. Во-вторых, отдача данных сводится, чаще всего, к чему-то подобному:
  1.     long writed = 0;
  2.     byte[] buffer = new byte[BUFFER_LENGTH];
  3.     int readed = in.read(buffer, 0, BUFFER_LENGTH);
  4.     while (readed != -1) {
  5.       out.write(buffer, 0, readed);
  6.       writed += readed;
  7.       readed = in.read(buffer, 0, BUFFER_LENGTH);
  8.     }
* This source code was highlighted with Source Code Highlighter.

После прочтения книжек по NIO и воспользовавшись микроскопом можно это переделать в чуть более эффективное средство:
  1.   public static long transfer(File file, OutputStream out) throws IOException {
  2.     return transfer(file, 0, file.length(), out);
  3.   }
  4.  
  5.   public static long transfer(File file, long position, long count,
  6.       OutputStream out) throws IOException {
  7.     FileChannel in = new FileInputStream(file).getChannel();
  8.     try {
  9.       long writed = in.transferTo(position, count, Channels
  10.           .newChannel(out));
  11.       return writed;
  12.     } finally {
  13.       in.close();
  14.     }
  15.   }
* This source code was highlighted with Source Code Highlighter.

Однако те, кто внимательно изучал stack trace'ы своего сервера, знает, что OutputStream у Tomcat не поддерживает передачу с помощью каналов, и всё сводится к первому примеру, но уже в недрах JVM.

Очевидные недостатки данного подхода:
  • Производительность. Код, написанный на Java в данном случае будет, очевидно, медленее, чем native-код, если бы он умел копировать напрямую из файла в OutputStream
  • Использование памяти. Разработчикам трудно удержаться и не обернуть каждый из стримов в пару-другую Buffered(Input|Output)Stream. Получается, что каждый кусок файла поочерёдно побывает в трёх-четырёх местах нашего ОЗУ (вспомните ещё дисковый кеш операционной системы и, скорее всего, некоторый кеш TCP/IP)
  • Код активно использует ресурсы процессора по копированию кусочков данных туда-сюда

Однако Apache Tomcat позволяет сервлетам использовать (через удобный интерфейс) функцию apr_socket_sendfile из библиотеки Apache Portable Runtime. Эта функция принимает на вход указатель на сокет, на файл, а также параменты старта и длины передаваемых данных (передавать можно не только файл целиком). Доступ к данной функциональности делается через использование атрибутов запроса (HttpServletRequest). Проверить наличие данной функциональности:
  1.   private static final String TOMCAT_SENDFILE_SUPPORT = "org.apache.tomcat.sendfile.support";
  2.  
  3.   final boolean sendFileSupport = Boolean.TRUE.equals(request
  4.         .getAttribute(TOMCAT_SENDFILE_SUPPORT));
* This source code was highlighted with Source Code Highlighter.


Теперь, если:
  1. sendFileSupport == true
  2. Файл не будет удалён сразу после выполнения кода
  3. Размер файла меньше 2 Гб

То можно вместо самостоятельной передачи файла поручить это Apache Tomcat:
  1. private static final String TOMCAT_SENDFILE_FILENAME = "org.apache.tomcat.sendfile.filename";
  2. private static final String TOMCAT_SENDFILE_START = "org.apache.tomcat.sendfile.start";
  3. private static final String TOMCAT_SENDFILE_END = "org.apache.tomcat.sendfile.end";
  4.  
  5. // using Apache APR and/or NIO to transfer file
  6. response.setBufferSize(1 << 18);
  7. request.setAttribute(TOMCAT_SENDFILE_FILENAME, file.getCanonicalPath());
  8. request.setAttribute(TOMCAT_SENDFILE_START, Long.valueOf(0));
  9. request.setAttribute(TOMCAT_SENDFILE_END, Long.valueOf(fileLength));
* This source code was highlighted with Source Code Highlighter.

Второе ограничение связано с тем, что процесс передачи файла будет начат уже после того, как мы закончим работу в сервлете. Третье — с чем не ясно, но, возможно, с тем, что у меня 32-битная JVM и 32-битная Gentoo на тестовой машине (не захотел Tomcat отдавать файл больше 2 Гб сам).

В результате:
  • Количество рабочих Java-потоков сервера снизилось в два-три раза, так как файлы теперь передаются в отдельных native-тредах
  • Загрузка процессора уменьшилась, так как APR использует функции операционной системы, чтобы оптимизировать передачу файлов
  • В «куче» остаётся меньше «мусора», что улучшает работу Garbage Collector'а

Разумеется, для production системы нужно не только уметь отдавать файл целиком, но и по частям, а также учитывать возможность того, что файл уже есть у пользователя (обрабатывать NotModifiedSince).

Для дальнейшего изучения
Теги:
Хабы:
Всего голосов 31: ↑23 и ↓8+15
Комментарии15

Публикации

Истории

Работа

Java разработчик
332 вакансии

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань