Часто сервера на Node.JS используются как сервисы-агрегаторы, получающие динамические данные с других HTTP-источников и формирующие на основе этих данных агрегированный ответ.
Для обработки полученных данных удобно использовать внешние процессы, обрабатывающие исходный набор файлов (например, утилиты ImageMagick или ffmpeg).
Рассмотрим это на примере HTTP-сервера, выполняющего роль backend для сервера nginx, и формирующего CSS-спрайты для набора изображений.
Объекты «HTTP-клиент» в Node.JS работают каждый с одним TCP-соединением, выполняя запросы по очереди, поэтому нам нужно организовать пул клиентов (компромисс между созданием соединений на каждый чих и использованием одного соединения), если мы хотим работать действительно быстро (параллельно).
Самый примитивный пул мы сделаем из предположения, что все исходные запросы мы посылаем на адрес example.com:80.
При желании можно изменить архитектуру пула, разрешив соединения к нескольким хостам, а также ограничив сверху размер пула (при этом раскидывая запросы по наименее загруженным соединениям). Оставлю это в качестве домашнего задания.
Нам нужна асинхронная функция для выполнения HTTP-запросов и сохранения содержимого в файл. Особенность её в том, что выполняется сразу два потока асинхронных операций — чтение исходного HTTP-потока, и запись в файл. Причём успешно закрыть файл и вызвать функцию обратного вызова мы можем только по завершению всех операций записи, которые могут выполняться не обязательно последовательно.
Вот пример реализации:
Для того, чтобы не генерировать спрайты на каждый запрос, мы будем сохранять выходные спрайты, удаляя самые старые из их, например, по крону. В случае, если файл уже существует, nginx отдаст его по правилу try_files. В противном случае запрос будет перенаправлен на наш backend, который создаст нужный файл, и с помощью X-Accel-Redirect попросит nginx отдать файл с внутренней локации, которая ведёт на то же физическое пространство.
При этом конфигурация nginx будет выглядеть где-то так:
Данный пример не претендует на совершенство, с его помощью хорошо отдавать большие файлы, в том числе и по частям, с кэшированием.
Если же файлы маленькие, и нам желательно лучше контролировать перегенерацию спрай��ов с отсутствующими картинками, то правильнее кэшировать на стороне nginx с правилом вида proxy_no_cache $http_pragma.
Здесь фрагмент HTTP-сервера, отвечающего за получение набора файлов, формирование спрайта и отдачу результата для nginx.
Контролировать внешние процессы с помощью Node.JS легко и удобно. Для удобства отладки будем копировать вывод, генерируемый внешним процессом, в нашу консоль. Для формирования спрайта выберем пакет GraphicsMagick (форк ImageMagick, обладающий стабильным API и хорошей производительностью).
Для формирования имени файла лучше использовать Process.pid и счётчик запросов (например, как path.join('/tmp', ['source-file', Process.pid, requestCounter].join('-')). При этом функция обработки запросов должна получать счётчик запросов как аргумент, так как обработка следующего за��роса может начаться раньше, чем выполнятся все шаги выполнения текущего запроса.
Пусть все наши временные файлы имеют имя source-pid… или sprite-pid-…:
Пусть мы хотим получить спрайт по фотоальбому с какого-то момента времени (timespec).
Собственно, теперь у нас есть готовое приложение, формирующее спрайт, как агрегированный результат по набору запросов к другим сайтам.
Осталось добавить конкретики (алгоритмы получения ссылок на исходные изображения, формирования плэйсхолдеров, если размеры постоянно меняются), и этим можно пользоваться.
Собственно, одно из моих мини-приложений и выполняет роль динамического генератора спрайтов.
Для обработки полученных данных удобно использовать внешние процессы, обрабатывающие исходный набор файлов (например, утилиты ImageMagick или ffmpeg).
Рассмотрим это на примере HTTP-сервера, выполняющего роль backend для сервера nginx, и формирующего CSS-спрайты для набора изображений.
Асинхронные чтение/запись
Пул клиентских соединений
Объекты «HTTP-клиент» в Node.JS работают каждый с одним TCP-соединением, выполняя запросы по очереди, поэтому нам нужно организовать пул клиентов (компромисс между созданием соединений на каждый чих и использованием одного соединения), если мы хотим работать действительно быстро (параллельно).
Самый примитивный пул мы сделаем из предположения, что все исходные запросы мы посылаем на адрес example.com:80.
var ClientPool = function()
{
this.poolSize = 0;
this.freeClients = [];
};
ClientPool.prototype.needClient = function()
{
this.freeClients.push(this.newClient());
this.poolSize++;
};
ClientPool.prototype.newClient = function()
{
return http.createClient(80, 'example.com');
};
ClientPool.prototype.request = function(method, url, headers)
{
if (this.freeClients.length == 0)
{
this.needClient();
}
var client = this.freeClients.pop();
var req = client.request(method, url, headers);
return req;
};
ClientPool.prototype.returnToPool = function(client)
{
this.freeClients.push(client);
};
var clientPool = new ClientPool();
* This source code was highlighted with Source Code Highlighter.При желании можно изменить архитектуру пула, разрешив соединения к нескольким хостам, а также ограничив сверху размер пула (при этом раскидывая запросы по наименее загруженным соединениям). Оставлю это в качестве домашнего задания.
Получение и сохранение файла
Нам нужна асинхронная функция для выполнения HTTP-запросов и сохранения содержимого в файл. Особенность её в том, что выполняется сразу два потока асинхронных операций — чтение исходного HTTP-потока, и запись в файл. Причём успешно закрыть файл и вызвать функцию обратного вызова мы можем только по завершению всех операций записи, которые могут выполняться не обязательно последовательно.
Вот пример реализации:
var getFile = function(url, path, callback)
{
fs.open(path, 'w', 0600, function(err, fd)
{
if (err)
{
callback(err);
return;
}
var request = clientPool.request('GET', url, { 'Host': 'example.com' });
request.on('response', function(sourceResponse)
{
var statusCode = parseInt(sourceResponse.statusCode);
if (statusCode < 200 || statusCode > 299)
{
sourceResponse.on('end', function()
{
clientPool.returnToPool(sourceResponse.client);
});
callback('Bad status code');
return;
}
var writeErr = null;
var writesPending = 0;
var sourceEnded = false;
var checkPendingCallback = function()
{
if (!sourceEnded || writesPending > 0)
{
return;
}
fs.close(fd, function(err)
{
err = err ? err : writeErr;
if (err)
{
removeFile(path);
callback(err);
return;
}
// No errors and all written
callback(null);
});
};
var position = 0;
sourceResponse.on('data', function(chunk)
{
writesPending++;
fs.write(fd, chunk, 0, chunk.length, position, function(err, written)
{
writesPending--;
if (err)
{
writeErr = err;
}
checkPendingCallback();
});
position += chunk.length;
});
sourceResponse.on('end', function()
{
sourceEnded = true;
checkPendingCallback();
clientPool.returnToPool(sourceResponse.client);
});
});
request.end();
});
};
* This source code was highlighted with Source Code Highlighter.Механизм взаимодействия nginx и нашего сервера
Для того, чтобы не генерировать спрайты на каждый запрос, мы будем сохранять выходные спрайты, удаляя самые старые из их, например, по крону. В случае, если файл уже существует, nginx отдаст его по правилу try_files. В противном случае запрос будет перенаправлен на наш backend, который создаст нужный файл, и с помощью X-Accel-Redirect попросит nginx отдать файл с внутренней локации, которая ведёт на то же физическое пространство.
При этом конфигурация nginx будет выглядеть где-то так:
upstream sprite_gen {
server 127.0.0.1:14239;
}
location /out_folder/ {
alias /var/sprite-gen/out_folder/;
internal;
}
location / {
alias /var/sprite-gen/out_folder/;
try_files $uri @transcoder;
}
location @transcoder {
proxy_pass http://sprite_gen;
}
Данный пример не претендует на совершенство, с его помощью хорошо отдавать большие файлы, в том числе и по частям, с кэшированием.
Если же файлы маленькие, и нам желательно лучше контролировать перегенерацию спрай��ов с отсутствующими картинками, то правильнее кэшировать на стороне nginx с правилом вида proxy_no_cache $http_pragma.
Получаем несколько файлов
Здесь фрагмент HTTP-сервера, отвечающего за получение набора файлов, формирование спрайта и отдачу результата для nginx.
var outPath = ''; // Куда кладём результирующий спрайт
var imageUrls = []; // Здесь список путей для исходных изображений.
var images = []; // Здесь список путей для исходных изображений.
var waitCounter = images.length;
var needCache = true; // если хоть одно изображение отсутствует, и заменено плейсхолдером, то выключаем кэширование
var handlePart = function(url, pth)
{
getFile(url, pth, function(err)
{
waitCounter--;
if (err)
{
removeFile(pth);
var pth = placeholder_path;
needCache = false;
}
if (waitCounter == 0)
{
makeSprite(images, outPath, function(err)
{
if (err)
{
response.writeHead(500, {
'Content-Type': 'text/plain',
});
response.end('Trouble');
return;
}
var headers = {
'Content-Type': 'image/png',
'X-Accel-Redirect': outUrl
};
if (needCache)
{
headers['Cache-Control'] = 'max-age:315360000, public';
headers['Expires'] = 'Thu, 31 Dec 2037 23:55:55 GMT';
}
else
{
headers['Cache-Control'] = 'no-cache, no-store';
headers['Pragma'] = 'no-cache';
}
response.writeHead(200, headers);
response.end();
});
}
});
};
for (var i = 0; i < imageUrls.length)
{
handlePart(imageUrls[i], images[i]);
}
* This source code was highlighted with Source Code Highlighter.Формируем выходной файл посредством внешнего процесса
Контролировать внешние процессы с помощью Node.JS легко и удобно. Для удобства отладки будем копировать вывод, генерируемый внешним процессом, в нашу консоль. Для формирования спрайта выберем пакет GraphicsMagick (форк ImageMagick, обладающий стабильным API и хорошей производительностью).
var spriteScript = '/usr/bin/gm';
var placeholder = path.join(__dirname, 'placeholder.jpg');
var getParams = function(count)
{
return ('montage +frame +shadow +label -background #000000 -tile ' + count + 'x1 -geometry +0+0').split(' ');
};
var removeFile = function(path)
{
fs.unlink(path, function(err)
{
if (err)
{
console.log('Cannot remove ' + path);
}
});
};
var cleanup = function(inPaths, placeholder)
{
for (var i = 0; i < inPaths.length; i++)
{
if (inPaths[i] == placeholder)
{
continue;
}
removeFile(inPaths[i]);
}
};
var makeSprite = function(inPaths, outPath, placeholder, callback)
{
var para = getParams(inPaths.length).concat(inPaths, outPath);
console.log(['run', spriteScript, para.join(' ')].join(' '));
var spriter = child_process.spawn(spriteScript, para);
spriter.stderr.addListener('data', function(data)
{
console.log(data);
});
spriter.stdout.addListener('data', function(data)
{
console.log(data);
});
spriter.addListener('exit', function(code, signal)
{
if (signal != null)
{
callback('Internal Server Error - Interrupted by signal' + signal.toString());
return;
}
if (code != 0)
{
callback('Internal Server Error - Code is ' + code.toString());
return;
}
cleanup(inPaths, placeholder);
callback(null);
});
};
* This source code was highlighted with Source Code Highlighter.Небольшие нюансы
Формируем имя для временного файла
Для формирования имени файла лучше использовать Process.pid и счётчик запросов (например, как path.join('/tmp', ['source-file', Process.pid, requestCounter].join('-')). При этом функция обработки запросов должна получать счётчик запросов как аргумент, так как обработка следующего за��роса может начаться раньше, чем выполнятся все шаги выполнения текущего запроса.
Чистим временные данные от прошлых процессов
Пусть все наши временные файлы имеют имя source-pid… или sprite-pid-…:
var fileExpr = /^(?:source|sprite)\-(\d+)\b/;
var storagePath = '/tmp/';
var cleanupOldFiles = function()
{
fs.readdir(storagePath, function(err, files)
{
if (err)
{
console.log('Cannot read ' + storagePath + ' directory.';
return;
}
for (var i = 0; i < files.length; i++)
{
var fn = files[i];
m = fileExpr.exec(fn);
if (!m)
{
continue;
}
var pid = parseInt(m[1]);
if (pid == process.pid)
{
continue;
}
removeFile(path.join(storagePath, fn));
}
});
};
* This source code was highlighted with Source Code Highlighter.Скелет обработки запроса
Пусть мы хотим получить спрайт по фотоальбому с какого-то момента времени (timespec).
#!/usr/bin/env node
var child_process = require('child_process');
var http = require('http');
var path = require('path');
var fs = require('fs');
var routeExpr = /^\/?(\w)\/([^\/]+)\/(\d+)\/(\d+)x(\d+)\.png$/;
var fileCounter = 0;
http.createServer(function(request, response)
{
if (request.method != 'GET')
{
response.writeHead(405, {'Content-Type': 'text/plain'});
response.end('Method Not Allowed');
return;
}
var m = routeExpr.exec(request.url);
if (!m)
{
response.writeHead(400, {'Content-Type': 'text/plain'});
response.end('Bad Request');
return;
}
var mode = m[1];
var chapter = m[2];
var timespec = parseInt(m[3]);
var width = parseInt(m[4]);
var height = parseInt(m[5]);
fileCounter++;
var moments = [timespec];
addWantedMoments(moments, mode)
var runner = function(moments, fileCounter, width, height)
{
var waitCounter = moments.length;
var outPath = path.join(storagePath, ['sprite', process.pid, fileCounter].join('-') + '.png');
var needCache = true;
for (var i = 0; i < moments.length; i++)
{
handlePart(i, placeholder);
}
};
request.connection.setTimeout(0);
runner([].concat(moments), fileCounter, width, height);
}).listen(8080, '127.0.0.1');
console.log('Server running at 127.0.0.1:8080');
cleanupOldFiles();
* This source code was highlighted with Source Code Highlighter.Собственно, теперь у нас есть готовое приложение, формирующее спрайт, как агрегированный результат по набору запросов к другим сайтам.
Осталось добавить конкретики (алгоритмы получения ссылок на исходные изображения, формирования плэйсхолдеров, если размеры постоянно меняются), и этим можно пользоваться.
Собственно, одно из моих мини-приложений и выполняет роль динамического генератора спрайтов.
- Node.JS — формируем результирующий документ, используя другие HTTP-источники
- Node.JS — Основы асинхронного программирования, часть 1
