Доброго времени суток, коллеги. В этой статье я опишу опыт создание многопоточного загрузчика файлов (с ограниченной нагрузкой на сервер) на JS (jQuery).
Совсем недавно у меня появилась задача (опытом решения которой я и хочу с вами поделится): сделать, в админке, возможность выбирать и загружать более одного файла за один раз. Задание вроде тривиальное и не сложное, но в итоге мое решение показалось мне довольно таки интересным, так как не было найдено аналогов.
Так как задача была простенькая, и, естественно, решений уже написано предостаточно, я обратился за помощью к гуглу. Но, поиск положительных результатов не дал – большинство предлагаемых решений используют Flash (что из-за некоторых специфик не позволяло использовать такие решения мне) или же написанные библиотеки на JS были сильно громадными и что самое прискорбное – нерабочими. Пришлось собирать велосипед.
Задача была срочной, поэтому нужно было срочное решение. Не долго думаю я прикрутил к полю инпут атрибут multiple (доступен с HTML5).
Далее следовали маленькие изменения обработчика на получение не одного файла, а массива файлов – и задача решена! (наивности (неопытности) моей не было придела).
Как многие со смехом уже додумали, что на первую, нормальную, партию файлов nginx ответил пятьсот третей. Надо было думать дальше.
Так как прошлое решение было сделано красиво и удобно для админов, было решено отталкиваться от него. Нужно было решить проблему ошибки №503, которую возвращал nginx из-за длительной обработки файлов.
Полминуты на обдумывание и появляется новое решение: будем отправлять ajax-ом не сразу все файлы, а по одному.
Решение, примерно, имело следующий вид:
Все просто: перебираем массив файлов (который находится в нужном инпуте), создаем экземпляр класса для работы с файлами (об этом дальше) и ajax-ом отправляем запрос на сервер. Стоит обратить внимание на параметр "async: false" — тут мы задаем синхронное выполнение ajax-запроса, так как асинхронное создаст нам множество запросов на сервер, чем мы с легкостью его сами и положим.
Решение работает, ошибки нет, но вот одна проблема – работает то оно медленно. И тут мне пришла в голову идея еще одного решения поставленной задачи.
Для ускорения загрузки файлов на сервер можно увеличить количество запросов, в которых будут передаваться файлы. Такое решение упереться в решение двух проблем:
1).Большим количеством запросов я быстро полажу к чертям свой сервер.
2) Большой объем одновременно загружаемых файлов забьет нам весь канал.
Судя по проблемам наш загрузчик должен считать количество запущенных запросов и объем передаваемых данных и при некоторых условиях ожидать окончания одних запросов, для начала других.
Приступим к коду:
Легенда:
dataArr – массив с данными для отправки.
fStek – сюда записываем идентификаторы таймаутов, для дальнейшей остановки рекурсии и очистки памяти от незавершенных функций.
vStek – количество вызванных потоков.
deley – задержка рекурсии функции, проверяющей потоки и объемы.
debug – режим дэбага. Нужно для отладки, но в этом примере все её признаки я удалил.
maxFilesSize – максимальная сума объемов загружаемых файлов
maxThreads – максимальное количество потоков.
Полную ясность в переменных (особенно fStek и deley) внесет вторая рассматриваемая функция FilesUploader.controller(). А пока что перейдем к инициализации класса:
Именно на эту функцию вешается обработка события клика кнопки в форме. Работа функции просто: пробегаем по файлам (jQuery.each), занесенным в инпут, и добавляем (FilesUploader.dataArr.push(f)) запись о каждом в массив. Далее вызываем контроллер, который есть важнейшим и сложнейшим звеном системы:
В первой строчке функции мы асинхронно вызываем (через некоторый период времени) эту же функцию (т.е. создаем рекурсию), и заносим идентификатор вызванной функции в переменную, для возможности прервать её выполнение.
Далее идет условие проверки потоков.
После получение файл из массива (FilesUploader.dataArr.pop()) проверяем его на наличие.
1. Если файла нет — тогда «убиваем» вызванные функции, по их идентификатору (clearTimeout(FilesUploader.fStek.pop()));
2. Если файл существует, делаем проверку на объем загружаемых файлов, и если он превышен – возвращаем файл обратно в стек и выходим из функции, иначе, если не превышен: отнимаем объем, увеличиваем счетчик запушенных потоков и вызываем следующую функцию (FilesUploader.worker(item)).
Для отправки файла на сервер средствами ajax, нужно поместить в него данные о файле (file.append()) в экземпляр класса FormData.
Далее уже вызываем функцию $.ajax, которая передаст наш файл загрузчику на сервер. По завершению каждого запроса (функция complete()) нужно увеличить допустимый объем и уменьшить количество исполняемых потоков (что и делается в строках “FilesUploader.maxFilesSize+=this.fileData.size” и “FilesUploader.vStek—“).
И последний штрих – функция вывода в консоль и закрывающая скобка:
Вот и все – класс для многопоточной загрузки файлов на сервер готов. Далее уже следует выставить, в зависимости от конфигурации сервера, допустимое количество потоков и объем одновременно загружаемых файлов – и можно работать.
Совсем недавно у меня появилась задача (опытом решения которой я и хочу с вами поделится): сделать, в админке, возможность выбирать и загружать более одного файла за один раз. Задание вроде тривиальное и не сложное, но в итоге мое решение показалось мне довольно таки интересным, так как не было найдено аналогов.
Решение №1
Так как задача была простенькая, и, естественно, решений уже написано предостаточно, я обратился за помощью к гуглу. Но, поиск положительных результатов не дал – большинство предлагаемых решений используют Flash (что из-за некоторых специфик не позволяло использовать такие решения мне) или же написанные библиотеки на JS были сильно громадными и что самое прискорбное – нерабочими. Пришлось собирать велосипед.
Решение №2
Задача была срочной, поэтому нужно было срочное решение. Не долго думаю я прикрутил к полю инпут атрибут multiple (доступен с HTML5).
<form id='FilesupLoadForm'>
<input type='file' id=’fileinput’ name='files' multiple="multiple" >
<input type="submit" value='upload'>
</form>
Далее следовали маленькие изменения обработчика на получение не одного файла, а массива файлов – и задача решена! (наивности (неопытности) моей не было придела).
Как многие со смехом уже додумали, что на первую, нормальную, партию файлов nginx ответил пятьсот третей. Надо было думать дальше.
Решение №3
Так как прошлое решение было сделано красиво и удобно для админов, было решено отталкиваться от него. Нужно было решить проблему ошибки №503, которую возвращал nginx из-за длительной обработки файлов.
Полминуты на обдумывание и появляется новое решение: будем отправлять ajax-ом не сразу все файлы, а по одному.
Решение, примерно, имело следующий вид:
jQuery.each($('#fileinput')[0].files, function(i, f) {
var file = new FormData();
file.append('file', f);
$.ajax({
url: 'uploader.php',
data: file,
async : false,
contentType: false,
processData: false,
dataType: "JSON",
type: 'POST',
beforeSend: function() {},
complete: function(event, request, settings) {},
success: function(data){ }
});
});
Все просто: перебираем массив файлов (который находится в нужном инпуте), создаем экземпляр класса для работы с файлами (об этом дальше) и ajax-ом отправляем запрос на сервер. Стоит обратить внимание на параметр "async: false" — тут мы задаем синхронное выполнение ajax-запроса, так как асинхронное создаст нам множество запросов на сервер, чем мы с легкостью его сами и положим.
Решение работает, ошибки нет, но вот одна проблема – работает то оно медленно. И тут мне пришла в голову идея еще одного решения поставленной задачи.
Решение №4
Для ускорения загрузки файлов на сервер можно увеличить количество запросов, в которых будут передаваться файлы. Такое решение упереться в решение двух проблем:
1).Большим количеством запросов я быстро полажу к чертям свой сервер.
2) Большой объем одновременно загружаемых файлов забьет нам весь канал.
Судя по проблемам наш загрузчик должен считать количество запущенных запросов и объем передаваемых данных и при некоторых условиях ожидать окончания одних запросов, для начала других.
Приступим к коду:
FilesUploader = {
dataArr : new Array(),
fStek : new Array(),
vStek : 0,
deley : 100,
debug : true,
maxFilesSize : 1024*1024*10,
maxThreads : 10,
Легенда:
dataArr – массив с данными для отправки.
fStek – сюда записываем идентификаторы таймаутов, для дальнейшей остановки рекурсии и очистки памяти от незавершенных функций.
vStek – количество вызванных потоков.
deley – задержка рекурсии функции, проверяющей потоки и объемы.
debug – режим дэбага. Нужно для отладки, но в этом примере все её признаки я удалил.
maxFilesSize – максимальная сума объемов загружаемых файлов
maxThreads – максимальное количество потоков.
Полную ясность в переменных (особенно fStek и deley) внесет вторая рассматриваемая функция FilesUploader.controller(). А пока что перейдем к инициализации класса:
run : function() {
jQuery.each($('#fileinput')[0].files, function(i, f) {
FilesUploader.dataArr.push(f);
});
FilesUploader.controller();
},
Именно на эту функцию вешается обработка события клика кнопки в форме. Работа функции просто: пробегаем по файлам (jQuery.each), занесенным в инпут, и добавляем (FilesUploader.dataArr.push(f)) запись о каждом в массив. Далее вызываем контроллер, который есть важнейшим и сложнейшим звеном системы:
controller : function() {
FilesUploader.fStek.push(setTimeout(FilesUploader.controller, FilesUploader.deley));
if(FilesUploader.vStek>=this.maxThreads) { return; }
item = FilesUploader.dataArr.pop();
if(item) {
if(FilesUploader.maxFilesSize-item.size < 0) {
FilesUploader.dataArr.push(item);
return;
}
FilesUploader.maxFilesSize-=item.size;
FilesUploader.vStek++;
FilesUploader.worker(item);
} else clearTimeout(FilesUploader.fStek.pop());
},
В первой строчке функции мы асинхронно вызываем (через некоторый период времени) эту же функцию (т.е. создаем рекурсию), и заносим идентификатор вызванной функции в переменную, для возможности прервать её выполнение.
Далее идет условие проверки потоков.
После получение файл из массива (FilesUploader.dataArr.pop()) проверяем его на наличие.
1. Если файла нет — тогда «убиваем» вызванные функции, по их идентификатору (clearTimeout(FilesUploader.fStek.pop()));
2. Если файл существует, делаем проверку на объем загружаемых файлов, и если он превышен – возвращаем файл обратно в стек и выходим из функции, иначе, если не превышен: отнимаем объем, увеличиваем счетчик запушенных потоков и вызываем следующую функцию (FilesUploader.worker(item)).
worker : function(item) {
var file = new FormData();
file.append('file', item);
$.ajax({
url: 'uploader.php',
data: file,
contentType: false,
processData: false,
dataType: "JSON",
type: 'POST',
beforeSend: function() {},
complete: function(event, request, settings) {
FilesUploader.maxFilesSize+=this.fileData.size;
FilesUploader.vStek--;
},
success: function(data){ }
});
},
Для отправки файла на сервер средствами ajax, нужно поместить в него данные о файле (file.append()) в экземпляр класса FormData.
Далее уже вызываем функцию $.ajax, которая передаст наш файл загрузчику на сервер. По завершению каждого запроса (функция complete()) нужно увеличить допустимый объем и уменьшить количество исполняемых потоков (что и делается в строках “FilesUploader.maxFilesSize+=this.fileData.size” и “FilesUploader.vStek—“).
И последний штрих – функция вывода в консоль и закрывающая скобка:
out : function(message) {
if(console.log && this.debug) console.log(message);
}
}
Вот и все – класс для многопоточной загрузки файлов на сервер готов. Далее уже следует выставить, в зависимости от конфигурации сервера, допустимое количество потоков и объем одновременно загружаемых файлов – и можно работать.