В этом топике расскажу о маленьком, но эффективном способе передачи файлов пользователю из сервлета по HTTP протоколу. Используется:
Конечно, отдавать файлы сервлетом не очень хорошо с точки зрения производительности. Во-первых, отдавать статичный контент лучше всего вообще без всяких скриптов. Но иногда без этого не обойтись. Во-вторых, отдача данных сводится, чаще всего, к чему-то подобному:
После прочтения книжек по NIO и воспользовавшись микроскопом можно это переделать в чуть более эффективное средство:
Однако те, кто внимательно изучал stack trace'ы своего сервера, знает, что OutputStream у Tomcat не поддерживает передачу с помощью каналов, и всё сводится к первому примеру, но уже в недрах JVM.
Очевидные недостатки данного подхода:
Однако Apache Tomcat позволяет сервлетам использовать (через удобный интерфейс) функцию apr_socket_sendfile из библиотеки Apache Portable Runtime. Эта функция принимает на вход указатель на сокет, на файл, а также параменты старта и длины передаваемых данных (передавать можно не только файл целиком). Доступ к данной функциональности делается через использование атрибутов запроса (HttpServletRequest). Проверить наличие данной функциональности:
Теперь, если:
То можно вместо самостоятельной передачи файла поручить это Apache Tomcat:
Второе ограничение связано с тем, что процесс передачи файла будет начат уже после того, как мы закончим работу в сервлете. Третье — с чем не ясно, но, возможно, с тем, что у меня 32-битная JVM и 32-битная Gentoo на тестовой машине (не захотел Tomcat отдавать файл больше 2 Гб сам).
В результате:
Разумеется, для production системы нужно не только уметь отдавать файл целиком, но и по частям, а также учитывать возможность того, что файл уже есть у пользователя (обрабатывать NotModifiedSince).
Для дальнейшего изучения
- Apache Tomcat
- Apache Portable Runtime Library
- Apache Tomcat Native Library
- Ваш сервлет, которому нужно отдавать файлы пользователю
Конечно, отдавать файлы сервлетом не очень хорошо с точки зрения производительности. Во-первых, отдавать статичный контент лучше всего вообще без всяких скриптов. Но иногда без этого не обойтись. Во-вторых, отдача данных сводится, чаще всего, к чему-то подобному:
- long writed = 0;
- byte[] buffer = new byte[BUFFER_LENGTH];
- int readed = in.read(buffer, 0, BUFFER_LENGTH);
- while (readed != -1) {
- out.write(buffer, 0, readed);
- writed += readed;
- readed = in.read(buffer, 0, BUFFER_LENGTH);
- }
* This source code was highlighted with Source Code Highlighter.
После прочтения книжек по NIO и воспользовавшись микроскопом можно это переделать в чуть более эффективное средство:
- public static long transfer(File file, OutputStream out) throws IOException {
- return transfer(file, 0, file.length(), out);
- }
-
- public static long transfer(File file, long position, long count,
- OutputStream out) throws IOException {
- FileChannel in = new FileInputStream(file).getChannel();
- try {
- long writed = in.transferTo(position, count, Channels
- .newChannel(out));
- return writed;
- } finally {
- in.close();
- }
- }
* 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). Проверить наличие данной функциональности:
- private static final String TOMCAT_SENDFILE_SUPPORT = "org.apache.tomcat.sendfile.support";
-
- final boolean sendFileSupport = Boolean.TRUE.equals(request
- .getAttribute(TOMCAT_SENDFILE_SUPPORT));
* This source code was highlighted with Source Code Highlighter.
Теперь, если:
- sendFileSupport == true
- Файл не будет удалён сразу после выполнения кода
- Размер файла меньше 2 Гб
То можно вместо самостоятельной передачи файла поручить это Apache Tomcat:
- private static final String TOMCAT_SENDFILE_FILENAME = "org.apache.tomcat.sendfile.filename";
- private static final String TOMCAT_SENDFILE_START = "org.apache.tomcat.sendfile.start";
- private static final String TOMCAT_SENDFILE_END = "org.apache.tomcat.sendfile.end";
-
- // using Apache APR and/or NIO to transfer file
- response.setBufferSize(1 << 18);
- request.setAttribute(TOMCAT_SENDFILE_FILENAME, file.getCanonicalPath());
- request.setAttribute(TOMCAT_SENDFILE_START, Long.valueOf(0));
- 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).
Для дальнейшего изучения
- Apache Portable Runtime and Tomcat — об этой и других возможностях
- Apache Portable Runtime
- FileFieldBehaviour — класс в Arp.Site, отвечающий за обработку запросов к файлам, в том числе с поддержкой докачки и NotModifiedSince