Недавно я прочитал "Повторное изучение внешнего интерфейса". Первая глава предназначена для того, чтобы вы могли составить свою собственную карту системы знаний и добавить то, что вы узнаете позже, в соответствующие модули, что может не только углубить понимание исходных знаний, но и укрепить память.Это очень хороший план обучения.
Основные точки знаний этой статьи исходят из:
- «Жесткий бой Node.js: 115 основных навыков»
- i0natan/nodebestpractices
- Некоторые точки знаний для последующего изучения
инструкция
Лучший способ просмотреть уценку — использовать VSCode для непосредственного открытия схемы, чтобы весь контекст был ясен с первого взгляда, а последующие дополнительные точки знаний были быстро расположены в соответствующей позиции:
Этот файл уценки был удален на Github, и любые обновления будут загружаться прямо сюда:
GitHub.com/данная мощность в течение дней/nod…
В блоге тоже есть интересные вещи:
Установить
# 使用 nvm 安装
https://github.com/creationix/nvm#install-script # Git install
nvm install
nvm alias default
# 卸载 pkg 安装版
sudo rm -rf /usr/local/{bin/{node,npm},lib/node_modules/npm,lib/node,share/man/*/node.*}
глобальная переменная
require(id)
- Встроенные модули загружаются прямо из памяти
- Файловый модуль находит файл с помощью поиска файлов.
- Пакет находит входной файл через основное поле в package.json
module.exports
// 通过如下模块包装得到
(funciton (exports, require, module, __filename, __dirname) { // 包装头
}); // 包装尾
JSON-файл
- пройти через
fs.readFileSync()
нагрузка - пройти через
JSON.parse()
Разобрать
Загружать большие файлы
- После успешного выполнения запроса файл будет кэширован
- Интенсивное использование приведет к тому, что в памяти будет находиться много данных, что приведет к частотному разделению GC и утечкам памяти.
module.exports и экспорт
при исполнении
(funciton(exports, require, module, __filename, __dirname) { // 包装头
console.log('hello world!') // 原始文件
}); // 包装尾
exports
- экспорт является свойством модуля, по умолчанию это пустой объект
- Что требует, чтобы модуль действительно получал, так это свойство экспорта модуля.
- exports.xxxЭкспорт объекта с несколькими свойствами
- module.exports = xxx экспортирует объект
использовать
// module-2.js
exports.method = function() {
return 'Hello';
};
exports.method2 = function() {
return 'Hello again';
};
// module-1.js
const module2 = require('./module-2');
console.log(module2.method()); // Hello
console.log(module2.method2()); // Hello again
переменная пути
console.log('__dirname:', __dirname); // 文件夹
console.log('__filename:', __filename); // 文件
path.join(__dirname, 'views', 'view.html'); // 如果不希望自己手动处理 / 的问题,使用 path.join
console
Заполнитель | тип | пример |
---|---|---|
%s | String | console.log('%s', 'value') |
%d | Number | console.log('%d', 3.14) |
%j | JSON | console.log('%j', {name: 'Chenng'}) |
process
Посмотреть ПУТЬ
node
console.log(process.env.PATH.split(':').join('\n'));
установить ПУТЬ
process.env.PATH += ':/a_new_path_to_executables';
получение информации
// 获取平台信息
process.arch // x64
process.platform // darwin
// 获取内存使用情况
process.memoryUsage();
// 获取命令行参数
process.argv
nextTick
Метод process.nextTick позволяет поставить callback во главе следующей очереди опроса, а значит, его можно использовать для задержки выполнения, что оказывается более эффективным, чем setTimeout.
const EventEmitter = require('events').EventEmitter;
function complexOperations() {
const events = new EventEmitter();
process.nextTick(function () {
events.emit('success');
});
return events;
}
complexOperations().on('success', function () {
console.log('success!');
});
Buffer
Если формат кодирования не указан, операции с файлами и многие сетевые операции будут возвращать данные как тип буфера.
toString
По умолчаниюUTF-8
формат, также поддерживаетascii
,base64
Ждать.
data URI
// 生成 data URI
const fs = require('fs');
const mime = 'image/png';
const encoding = 'base64';
const base64Data = fs.readFileSync(`${__dirname}/monkey.png`).toString(encoding);
const uri = `data:${mime};${encoding},${base64Data}`;
console.log(uri);
// data URI 转文件
const fs = require('fs');
const uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...';
const base64Data = uri.split(',')[1];
const buf = Buffer(base64Data, 'base64');
fs.writeFileSync(`${__dirname}/secondmonkey.png`, buf);
events
const EventEmitter = require('events').EventEmitter;
const AudioDevice = {
play: function (track) {
console.log('play', track);
},
stop: function () {
console.log('stop');
},
};
class MusicPlayer extends EventEmitter {
constructor() {
super();
this.playing = false;
}
}
const musicPlayer = new MusicPlayer();
musicPlayer.on('play', function (track) {
this.playing = true;
AudioDevice.play(track);
});
musicPlayer.on('stop', function () {
this.playing = false;
AudioDevice.stop();
});
musicPlayer.emit('play', 'The Roots - The Fire');
setTimeout(function () {
musicPlayer.emit('stop');
}, 1000);
// 处理异常
// EventEmitter 实例发生错误会发出一个 error 事件
// 如果没有监听器,默认动作是打印一个堆栈并退出程序
musicPlayer.on('error', function (err) {
console.err('Error:', err);
});
util
promisify
const util = require('util');
const fs = require('fs');
const readAsync = util.promisify(fs.readFile);
async function init() {
try {
let data = await readAsync('./package.json');
data =JSON.parse(data);
console.log(data.name);
} catch (err) {
console.log(err);
}
}
поток
понимание потока
Потоки — это основанные на событиях API для управления и обработки данных.
- Потоки можно читать и записывать
- является экземпляром событийной реализации
Лучший способ понять потоки — представить, как данные будут обрабатываться без потоков:
-
fs.readFileSync
Прочитайте файл синхронно, программа заблокируется, и все данные будут прочитаны в память -
fs.readFile
Блокирует блокировку программы, но все равно считывает все данные файла в память - Я надеюсь прочитать большие файлы с меньшим объемом памяти, прочитать блок данных, пока память не будет обработана, а затем запросить дополнительные данные
тип потока
- Встроенный: многие основные модули реализуют потоковый интерфейс, например
fs.createReadStream
- HTTP: потоки для обработки веб-технологий
- Интерпретатор: сторонний модуль XML, интерпретатор JSON
- Браузеры: потоки узлов можно расширить для использования в браузерах.
- Аудио: звуковой модуль потокового интерфейса
- RPC (удаленный вызов): отправка потока по сети — эффективный способ связи между процессами.
- test: тестовая библиотека, использующая потоки
Использование встроенного потокового API
статический веб-сервер
Хотите отправить файл клиенту по сети эффективно и с поддержкой больших файлов.
не использовать поток
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
fs.readFile(`${__dirname}/index.html`, (err, data) => {
if (err) {
res.statusCode = 500;
res.end(String(err));
return;
}
res.end(data);
});
}).listen(8000);
использовать поток
const http = require('http');
const fs = require('fs');
http.createServer((req, res) => {
fs.createReadStream(`${__dirname}/index.html`).pipe(res);
}).listen(8000);
- Меньше кода, больше эффективности
- Предоставьте буфер для отправки клиенту
использовать поток + gzip
const http = require('http');
const fs = require('fs');
const zlib = require('zlib');
http.createServer((req, res) => {
res.writeHead(200, {
'content-encoding': 'gzip',
});
fs.createReadStream(`${__dirname}/index.html`)
.pipe(zlib.createGzip())
.pipe(res);
}).listen(8000);
Обработка ошибок для потоков
const fs = require('fs');
const stream = fs.createReadStream('not-found');
stream.on('error', (err) => {
console.trace();
console.error('Stack:', err.stack);
console.error('The error raised was:', err);
});
Используйте базовый класс потока
Читаемый поток — синтаксический анализатор строк JSON
Доступные для чтения потоки используются для предоставления гибкого API для источников ввода-вывода, а также могут использоваться в качестве синтаксических анализаторов:
- Унаследован от класса steam.Readable
- и реализовать
_read(size)
метод
json-lines.txt
{ "position": 0, "letter": "a" }
{ "position": 1, "letter": "b" }
{ "position": 2, "letter": "c" }
{ "position": 3, "letter": "d" }
{ "position": 4, "letter": "e" }
{ "position": 5, "letter": "f" }
{ "position": 6, "letter": "g" }
{ "position": 7, "letter": "h" }
{ "position": 8, "letter": "i" }
{ "position": 9, "letter": "j" }
JSONLineReader.js
const stream = require('stream');
const fs = require('fs');
const util = require('util');
class JSONLineReader extends stream.Readable {
constructor(source) {
super();
this._source = source;
this._foundLineEnd = false;
this._buffer = '';
source.on('readable', () => {
this.read();
});
}
// 所有定制 stream.Readable 类都需要实现 _read 方法
_read(size) {
let chunk;
let line;
let result;
if (this._buffer.length === 0) {
chunk = this._source.read();
this._buffer += chunk;
}
const lineIndex = this._buffer.indexOf('\n');
if (lineIndex !== -1) {
line = this._buffer.slice(0, lineIndex); // 从 buffer 的开始截取第一行来获取一些文本进行解析
if (line) {
result = JSON.parse(line);
this._buffer = this._buffer.slice(lineIndex + 1);
this.emit('object', result); // 当一个 JSON 记录解析出来的时候,触发一个 object 事件
this.push(util.inspect(result)); // 将解析好的 SJON 发回内部队列
} else {
this._buffer = this._buffer.slice(1);
}
}
}
}
const input = fs.createReadStream(`${__dirname}/json-lines.txt`, {
encoding: 'utf8',
});
const jsonLineReader = new JSONLineReader(input); // 创建一个 JSONLineReader 实例,传递一个文件流给它处理
jsonLineReader.on('object', (obj) => {
console.log('pos:', obj.position, '- letter:', obj.letter);
});
Доступный для записи поток - изменение цвета текста
Доступные для записи потоки можно использовать для вывода данных на низкоуровневый ввод-вывод:
- унаследовано от
stream.Writable
- реализовать
_write
метод отправки данных в базовые исходные данные
cat json-lines.txt | node stram_writable.js
stram_writable.js
const stream = require('stream');
class GreenStream extends stream.Writable {
constructor(options) {
super(options);
}
_write(chunk, encoding, cb) {
process.stdout.write(`\u001b[32m${chunk}\u001b[39m`);
cb();
}
}
process.stdin.pipe(new GreenStream());
Дуплексный поток — прием и преобразование данных
Дуплексные потоки позволяют отправлять и получать данные:
- унаследовано от
stream.Duplex
- выполнить
_read
и_write
метод
Преобразование потока — анализ данных
Используйте потоки для преобразования данных в другой формат и эффективного управления памятью:
- унаследовано от
stream.Transform
- выполнить
_transform
метод
тестовый поток
Протестируйте со встроенным модулем утверждений Node.
const assert = require('assert');
const fs = require('fs');
const CSVParser = require('./csvparser');
const parser = new CSVParser();
const actual = [];
fs.createReadStream(`${__dirname}/sample.csv`)
.pipe(parser);
process.on('exit', function () {
actual.push(parser.read());
actual.push(parser.read());
actual.push(parser.read());
const expected = [
{ name: 'Alex', location: 'UK', role: 'admin' },
{ name: 'Sam', location: 'France', role: 'user' },
{ name: 'John', location: 'Canada', role: 'user' },
];
assert.deepEqual(expected, actual);
});
Файловая система
взаимодействие модуля fs
- Файловый ввод/вывод POSIX
- файловый поток
- Массовый файловый ввод/вывод
- мониторинг файлов
файловая система POSIX
метод ФС | описывать |
---|---|
fs.truncate | Обрезать или расширить файл до указанной длины |
fs.ftruncate | То же, что и усечение, но в качестве аргумента принимает файловый дескриптор. |
fs.chown | Изменить владельца и группу файла |
fs.fchown | То же, что и chown, но в качестве аргумента принимает файловый дескриптор. |
fs.lchown | То же, что и chown, но не разрешает символические ссылки. |
fs.stat | Получить статус файла |
fs.lstat | То же, что и stat, но возвращает информацию о символической ссылке, а не о том, на что она указывает. |
fs.fstat | То же, что и stat, но в качестве аргумента принимает файловый дескриптор. |
fs.link | Создать жесткую ссылку |
fs.symlink | Создать мягкую ссылку |
fs.readlink | Чтение значения мягкой ссылки |
fs.realpath | Возвращает канонический абсолютный путь |
fs.unlink | Удалить файлы |
fs.rmdir | удалить каталог файлов |
fs.mkdir | Создать каталог файлов |
fs.readdir | Чтение содержимого каталога файлов |
fs.close | закрыть файловый дескриптор |
fs.open | Откройте или создайте файл для чтения или записи |
fs.utimes | Установить время чтения и изменения файла |
fs.futimes | То же, что и utimes, но в качестве аргумента принимает файловый дескриптор. |
fs.fsync | Синхронизировать данные файла с диска |
fs.write | записать данные в файл |
fs.read | читать данные из файла |
const fs = require('fs');
const assert = require('assert');
const fd = fs.openSync('./file.txt', 'w+');
const writeBuf = new Buffer('some data to write');
fs.writeSync(fd, writeBuf, 0, writeBuf.length, 0);
const readBuf = new Buffer(writeBuf.length);
fs.readSync(fd, readBuf, 0, writeBuf.length, 0);
assert.equal(writeBuf.toString(), readBuf.toString());
fs.closeSync(fd);
чтение и запись потоков
const fs = require('fs');
const readable = fs.createReadStream('./original.txt');
const writeable = fs.createWriteStream('./copy.txt');
readable.pipe(writeable);
мониторинг файлов
fs.watchFile
Сравниватьfs.watch
Неэффективно, но лучше использовать.
Синхронное чтение и требование
Метод синхронизации fs следует использовать при первой инициализации приложения.
const fs = require('fs');
const config = JSON.parse(fs.readFileSync('./config.json').toString());
init(config);
требовать:
const config = require('./config.json);
init(config);
- Модуль будет буферизован глобально, а также загружаются и модифицируются другие файлы, что повлияет на загруженный с этим файлом модуль во всей системе
- в состоянии пройти
Object.freeze
заморозить объект
описание файла
Описание файла управляется операционной системой как некоторое число или индекс, связанный с файлом, открытым в процессе. Операционная система используется для просмотра информации о файле путем присвоения уникального целого числа каждому открытому файлу.
Stream | описание файла | описывать |
---|---|---|
stdin | 0 | стандартный ввод |
stdout | 1 | стандартный вывод |
stderr | 2 | стандартная ошибка |
console.log('Log')
даprocess.stdout.write('log')
синтаксический сахар.
Описание файла — это число, возвращаемое вызовами методов open и openSync.
const fd = fs.openSync('myfile', 'a');
console.log(typeof fd === 'number'); // true
блокировка файла
Сотрудничайте с несколькими процессами для одновременного доступа к файлу, чтобы гарантировать целостность файла и невозможность потери данных:
- Обязательная блокировка (выполняется на уровне ядра)
- Консультативная блокировка (не обязательна, только если задействованный процесс подписан на тот же механизм блокировки)
-
node-fs-ext
пройти черезflock
заблокировать файл
-
- использовать файл блокировки
- Процесс A пытается создать файл блокировки и преуспевает
- Процесс A получил блокировку и может изменить общий ресурс
- Процесс B пытается создать файл блокировки, но терпит неудачу, не может изменить общий ресурс
Узел реализует файл блокировки
- Создать файл блокировки с эксклюзивным токеном
- Создайте файл блокировки с помощью mkdir
эксклюзивный тег
// 所有需要打开文件的方法,fs.writeFile、fs.createWriteStream、fs.open 都有一个 x 标记
// 这个文件应该已独占打开,若这个文件存在,文件不能被打开
fs.open('config.lock', 'wx', (err) => {
if (err) { return console.err(err); }
});
// 最好将当前进程号写进文件锁中
// 当有异常的时候就知道最后这个锁的进程
fs.writeFile(
'config.lock',
process.pid,
{ flogs: 'wx' },
(err) => {
if (err) { return console.error(err) };
},
);
блокировка файла mkdir
Есть проблема с эксклюзивным тегом, который может не распознаваться некоторыми системами.0_EXCL
отметка. Другим решением является замена файла блокировки каталогом, в котором PID может быть записан в файл.
fs.mkidr('config.lock', (err) => {
if (err) { return console.error(err); }
fs.writeFile(`/config.lock/${process.pid}`, (err) => {
if (err) { return console.error(err); }
});
});
реализация модуля блокировки
GitHub.com/year rating/lock FI...
const fs = require('fs');
const lockDir = 'config.lock';
let hasLock = false;
exports.lock = function (cb) { // 获取锁
if (hasLock) { return cb(); } // 已经获取了一个锁
fs.mkdir(lockDir, function (err) {
if (err) { return cb(err); } // 无法创建锁
fs.writeFile(lockDir + '/' + process.pid, function (err) { // 把 PID写入到目录中以便调试
if (err) { console.error(err); } // 无法写入 PID,继续运行
hasLock = true; // 锁创建了
return cb();
});
});
};
exports.unlock = function (cb) { // 解锁方法
if (!hasLock) { return cb(); } // 如果没有需要解开的锁
fs.unlink(lockDir + '/' + process.pid, function (err) {
if (err) { return cb(err); }
fs.rmdir(lockDir, function (err) {
if (err) return cb(err);
hasLock = false;
cb();
});
});
};
process.on('exit', function () {
if (hasLock) {
fs.unlinkSync(lockDir + '/' + process.pid); // 如果还有锁,在退出之前同步删除掉
fs.rmdirSync(lockDir);
console.log('removed lock');
}
});
рекурсивные операции с файлами
Онлайн-библиотека:mkdirp
Рекурсия: решение нашей проблемы требует сначала решения меньшей идентичной проблемы.
dir-a
├── dir-b
│ ├── dir-c
│ │ ├── dir-d
│ │ │ └── file-e.png
│ │ └── file-e.png
│ ├── file-c.js
│ └── file-d.txt
├── file-a.js
└── file-b.txt
Найти модули:find /asset/dir-a -name="file.*"
[
'dir-a/dir-b/dir-c/dir-d/file-e.png',
'dir-a/dir-b/dir-c/file-e.png',
'dir-a/dir-b/file-c.js',
'dir-a/dir-b/file-d.txt',
'dir-a/file-a.js',
'dir-a/file-b.txt',
]
const fs = require('fs');
const join = require('path').join;
// 同步查找
exports.findSync = function (nameRe, startPath) {
const results = [];
function finder(path) {
const files = fs.readdirSync(path);
for (let i = 0; i < files.length; i++) {
const fpath = join(path, files[i]);
const stats = fs.statSync(fpath);
if (stats.isDirectory()) { finder(fpath); }
if (stats.isFile() && nameRe.test(files[i])) {
results.push(fpath);
}
}
}
finder(startPath);
return results;
};
// 异步查找
exports.find = function (nameRe, startPath, cb) { // cb 可以传入 console.log,灵活
const results = [];
let asyncOps = 0; // 2
function finder(path) {
asyncOps++;
fs.readdir(path, function (er, files) {
if (er) { return cb(er); }
files.forEach(function (file) {
const fpath = join(path, file);
asyncOps++;
fs.stat(fpath, function (er, stats) {
if (er) { return cb(er); }
if (stats.isDirectory()) finder(fpath);
if (stats.isFile() && nameRe.test(file)) {
results.push(fpath);
}
asyncOps--;
if (asyncOps == 0) {
cb(null, results);
}
});
});
asyncOps--;
if (asyncOps == 0) {
cb(null, results);
}
});
}
finder(startPath);
};
console.log(exports.findSync(/file.*/, `${__dirname}/dir-a`));
console.log(exports.find(/file.*/, `${__dirname}/dir-a`, console.log));
Мониторинг файлов и папок
Хотите прослушать файл или каталог и выполнить действие при изменении файла.
const fs = require('fs');
fs.watch('./watchdir', console.log); // 稳定且快
fs.watchFile('./watchdir', console.log); // 跨平台
Чтение файлового потока построчно
const fs = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream('/etc/hosts'),
crlfDelay: Infinity
});
rl.on('line', (line) => {
console.log(`cc ${line}`);
const extract = line.match(/(\d+\.\d+\.\d+\.\d+) (.*)/);
});
Интернет
Получить локальный IP
function get_local_ip() {
const interfaces = require('os').networkInterfaces();
let IPAdress = '';
for (const devName in interfaces) {
const iface = interfaces[devName];
for (let i = 0; i < iface.length; i++) {
const alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
IPAdress = alias.address;
}
}
}
return IPAdress;
}
TCP-клиент
Использование NodeJSnet
Модули создают TCP-соединения и сервисы.
Запустите и проверьте TCP
const assert = require('assert');
const net = require('net');
let clients = 0;
let expectedAssertions = 2;
const server = net.createServer(function (client) {
clients++;
const clientId = clients;
console.log('Client connected:', clientId);
client.on('end', function () {
console.log('Client disconnected:', clientId);
});
client.write('Welcome client: ' + clientId);
client.pipe(client);
});
server.listen(8000, function () {
console.log('Server started on port 8000');
runTest(1, function () {
runTest(2, function () {
console.log('Tests finished');
assert.equal(0, expectedAssertions);
server.close();
});
});
});
function runTest(expectedId, done) {
const client = net.connect(8000);
client.on('data', function (data) {
const expected = 'Welcome client: ' + expectedId;
assert.equal(data.toString(), expected);
expectedAssertions--;
client.end();
});
client.on('end', done);
}
UDP-клиент
использоватьdgram
Модуль создает дейтаграммуsocket
, затем используйтеsocket.send
отправить данные.
служба доставки файлов
const dgram = require('dgram');
const fs = require('fs');
const port = 41230;
const defaultSize = 16;
function Client(remoteIP) {
const inStream = fs.createReadStream(__filename); // 从当前文件创建可读流
const socket = dgram.createSocket('udp4'); // 创建新的数据流 socket 作为客户端
inStream.on('readable', function () {
sendData(); // 当可读流准备好,开始发送数据到服务器
});
function sendData() {
const message = inStream.read(defaultSize); // 读取数据块
if (!message) {
return socket.unref(); // 客户端完成任务后,使用 unref 安全关闭它
}
// 发送数据到服务器
socket.send(message, 0, message.length, port, remoteIP, function () {
sendData();
}
);
}
}
function Server() {
const socket = dgram.createSocket('udp4'); // 创建一个 socket 提供服务
socket.on('message', function (msg) {
process.stdout.write(msg.toString());
});
socket.on('listening', function () {
console.log('Server ready:', socket.address());
});
socket.bind(port);
}
if (process.argv[2] === 'client') { // 根据命令行选项确定运行客户端还是服务端
new Client(process.argv[3]);
} else {
new Server();
}
HTTP-клиент
использоватьhttp.createServer
иhttp.createClient
Запустите службу HTTP.
Запустите и протестируйте HTTP
const assert = require('assert');
const http = require('http');
const server = http.createServer(function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' }); // 写入基于文本的响应头
res.write('Hello, world.'); // 发送消息回客户端
res.end();
});
server.listen(8000, function() {
console.log('Listening on port 8000');
});
const req = http.request({ port: 8000}, function(res) { // 创建请求
console.log('HTTP headers:', res.headers);
res.on('data', function(data) { // 给 data 事件创建监听,确保和期望值一致
console.log('Body:', data.toString());
assert.equal('Hello, world.', data.toString());
assert.equal(200, res.statusCode);
server.unref();
console.log('测试完成');
});
});
req.end();
перенаправить
Стандарт HTTP определяет коды состояния, которые определяют, когда происходит перенаправление, а также указывает, что клиенты должны проверять наличие бесконечных циклов.
- 300: Множественный выбор
- 301: навсегда переехать в новое место.
- 302: Обнаружен прыжок с перенаправлением
- 303: см. другую информацию
- 304: без изменений
- 305: Использовать прокси
- 307: Временное перенаправление
const http = require('http');
const https = require('https');
const url = require('url'); // 有很多接续 URLs 的方法
// 构造函数被用来创建一个对象来构成请求对象的声明周期
function Request() {
this.maxRedirects = 10;
this.redirects = 0;
}
Request.prototype.get = function(href, callback) {
const uri = url.parse(href); // 解析 URLs 成为 Node http 模块使用的格式,确定是否使用 HTTPS
const options = { host: uri.host, path: uri.path };
const httpGet = uri.protocol === 'http:' ? http.get : https.get;
console.log('GET:', href);
function processResponse(response) {
if (response.statusCode >= 300 && response.statusCode < 400) { // 检查状态码是否在 HTTP 重定向范围
if (this.redirects >= this.maxRedirects) {
this.error = new Error('Too many redirects for: ' + href);
} else {
this.redirects++; // 重定向计数自增
href = url.resolve(options.host, response.headers.location); // 使用 url.resolve 确保相对路径的 URLs 转换为绝对路径 URLs
return this.get(href, callback);
}
}
response.url = href;
response.redirects = this.redirects;
console.log('Redirected:', href);
function end() {
console.log('Connection ended');
callback(this.error, response);
}
response.on('data', function(data) {
console.log('Got data, length:', data.length);
});
response.on('end', end.bind(this)); // 绑定回调到 Request 实例,确保能拿到实例属性
}
httpGet(options, processResponse.bind(this))
.on('error', function(err) {
callback(err);
});
};
const request = new Request();
request.get('http://google.com/', function(err, res) {
if (err) {
console.error(err);
} else {
console.log(`
Fetched URL: ${res.url} with ${res.redirects} redirects
`);
process.exit();
}
});
HTTP-прокси
- Интернет-провайдеры используют прозрачные прокси, чтобы сделать сети более эффективными
- Уменьшите пропускную способность с помощью кэширующих прокси-серверов
- DevOps веб-приложений используют их для повышения производительности приложений.
const http = require('http');
const url = require('url');
http.createServer(function(req, res) {
console.log('start request:', req.url);
const options = url.parse(req.url);
console.log(options);
options.headers = req.headers;
const proxyRequest = http.request(options, function(proxyResponse) { // 创建请求来复制原始的请求
proxyResponse.on('data', function(chunk) { // 监听数据,返回给浏览器
console.log('proxyResponse length:', chunk.length);
res.write(chunk, 'binary');
});
proxyResponse.on('end', function() { // 追踪代理请求完成
console.log('proxied request ended');
res.end();
});
res.writeHead(proxyResponse.statusCode, proxyResponse.headers); // 发送头部信息给服务器
});
req.on('data', function(chunk) { // 捕获从浏览器发送到服务器的数据
console.log('in request length:', chunk.length);
proxyRequest.write(chunk, 'binary');
});
req.on('end', function() { // 追踪原始的请求什么时候结束
console.log('original request ended');
proxyRequest.end();
});
}).listen(8888); // 监听来自本地浏览器的连接
Инкапсулировать запрос-обещание
const https = require('https');
const promisify = require('util').promisify;
https.get[promisify.custom] = function getAsync(options) {
return new Promise((resolve, reject) => {
https.get(options, (response) => {
response.end = new Promise((resolve) => response.on('end', resolve));
resolve(response);
}).on('error', reject);
});
};
const rp = promisify(https.get);
(async () => {
const res = await rp('https://jsonmock.hackerrank.com/api/movies/search/?Title=Spiderman&page=1');
let body = '';
res.on('data', (chunk) => body += chunk);
await res.end;
console.log(body);
})();
DNS-запрос
использоватьdns
Модуль создает DNS-запросы.
- А:
dns.resolve
, запись A хранит IP-адрес - ТЕКСТ:
dns.resulveTxt
, текстовое значение можно использовать для создания других служб поверх DNS. - СРВ:
dns.resolveSrv
, служебная запись определяет данные о местоположении службы, обычно содержащие имя хоста и номер порта. - НС:
dns.resolveNs
, указав сервер доменных имен - CNAME:
dns.resolveCname
, связанная запись имени домена, установленная на имя домена вместо IP-адреса
const dns = require('dns');
dns.resolve('www.chenng.cn', function (err, addresses) {
if (err) {
console.error(err);
}
console.log('Addresses:', addresses);
});
шифрование и дешифрование криптобиблиотеки
const crypto = require('crypto')
function aesEncrypt(data, key = 'key') {
const cipher = crypto.createCipher('aes192', key)
let crypted = cipher.update(data, 'utf8', 'hex')
crypted += cipher.final('hex')
return crypted
}
function aesDecrypt(encrypted, key = 'key') {
const decipher = crypto.createDecipher('aes192', key)
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
Способ создания HTTP-запроса
- Стандартная библиотека HTTP
- Нет необходимости устанавливать внешние зависимости
- Вам нужно принимать данные блоками и слушать конечное событие самостоятельно
- HTTP и HTTPS — это два модуля, которые необходимо использовать отдельно.
- Запросить библиотеку
- легко использовать
- Имеет обещанную версию
request-promise
- Axios
- Может использоваться как в браузере, так и в NodeJS.
- Несколько одновременных запросов могут быть сделаны с помощью axios.all
- SuperAgent
- Может быть прикован
- node-fetch
- Перенесено из браузера
дочерний процесс
Выполнение внешних приложений
Базовые концепты
- 4 асинхронных метода: exec, execFile, fork, spawn
- Node
- fork: используется, когда вы хотите запустить процесс Node как независимый процесс, да, обработка вычислений и файловый дескриптор отделены от основного процесса Node.
- не-узел
- spawn: используется, когда будет много дочерних процессов ввода-вывода, и процесс будет иметь много вывода
- execFile: используется только при выполнении внешней программы, скорость выполнения высокая, а обработка пользовательского ввода относительно безопасна.
- exec: используется, когда вы хотите напрямую получить доступ к команде оболочки потока, обязательно обратите внимание на пользовательский ввод
- Node
- 3 метода синхронизации: execSync, execFileSync, spawnSync
- Дочерний процесс, созданный через API, не имеет необходимой связи с родительским процессом.
execFile
- Выходной результат будет кэширован, а окончательный результат или информация об исключении будут возвращены через обратный вызов.
const cp = require('child_process');
cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => {
if (err) { console.error(err); }
console.log('stdout: ', stdout);
console.log('stderr: ', stderr);
});
spawn
- Потоки могут использовать внешние приложения с большим объемом выходных данных для экономии памяти.
- Использование потоков для повышения эффективности ответа на данные
- Метод spawn возвращает потоковый интерфейс для ввода-вывода.
одиночная задача
const cp = require('child_process');
const child = cp.spawn('echo', ['hello', 'world']);
child.on('error', console.error);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
Многозадачность в тандеме
const cp = require('child_process');
const path = require('path');
const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]);
const sort = cp.spawn('sort');
const uniq = cp.spawn('uniq');
cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);
exec
- только одна строковая команда
- то же, что оболочка
const cp = require('child_process');
cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => {
console.log(stdout);
});
fork
- Метод fork создаст канал IPC, и различные процессы Node будут отправлять сообщения.
- Один дочерний процесс потребляет время запуска 30 мс и 10 МБ памяти.
- подпроцесс:
process.on('message')
,process.send()
- родительский процесс:
child.on('message')
,child.send()
общение отца и сына
// parent.js
const cp = require('child_process');
const child = cp.fork('./child', { silent: true });
child.send('monkeys');
child.on('message', function (message) {
console.log('got message from child', message, typeof message);
})
child.stdout.pipe(process.stdout);
setTimeout(function () {
child.disconnect();
}, 3000);
// child.js
process.on('message', function (message) {
console.log('got one', message);
process.send('no pizza');
process.send(1);
process.send({ my: 'object' });
process.send(false);
process.send(null);
});
console.log(process);
Общие навыки
Убить все дочерние процессы при выходе
- Сохраняйте ссылку на объект ChildProcess, возвращаемый spawn, и уничтожайте его при выходе из основного процесса.
const spawn = require('child_process').spawn;
const children = [];
process.on('exit', function () {
console.log('killing', children.length, 'child processes');
children.forEach(function (child) {
child.kill();
});
});
children.push(spawn('/bin/sleep', ['10']));
children.push(spawn('/bin/sleep', ['10']));
children.push(spawn('/bin/sleep', ['10']));
setTimeout(function () { process.exit(0); }, 3000);
Понимание кластера
- Решить проблему, из-за которой один процесс NodeJS не может в полной мере использовать многоядерный процессор.
- Приложение можно сделать более надежным с помощью режима мастер-кластера.
- Нижний слой Cluster — это модуль child_process, который помимо отправки обычных сообщений также может отправлять нижележащие объекты.
TCP
,UDP
Ждать - Основной процесс TCP отправляется дочернему процессу, дочерний процесс может перестроить TCP-соединение в соответствии с сообщением, а кластер может определить количество дочерних процессов, которые разветвляют соответствующие аппаратные ресурсы.
Многопоточность узла
проблема с одним потоком
- недозагрузка процессора
- Неперехваченное исключение может привести к завершению работы всей программы.
Поток узла
- Процесс узла занимает 7 потоков
- Ядром Node является движок v8. После запуска Node будет создан экземпляр v8. Этот экземпляр является многопоточным.
- Основной поток: компилировать и выполнять код
- Компилировать/оптимизировать поток: код можно оптимизировать во время выполнения основного потока.
- Поток профилировщика: запишите время выполнения профилированного кода, чтобы дать Crankshaft основу для оптимизации выполнения кода.
- Несколько потоков сборки мусора
- Выполнение JavaScript является однопоточным, но среда размещения JavaScript, будь то Node или браузер, многопоточная.
Асинхронный ввод-вывод
- В Node есть некоторые операции ввода-вывода (DNS, FS) и некоторые вычисления с интенсивным использованием ЦП (Zlib, Crypto), которые включают пул потоков Node.
- Размер пула потоков по умолчанию равен 4, вы можете вручную изменить размер пула потоков по умолчанию.
process.env.UV_THREADPOOL_SIZE = 64
кластер
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
}).listen(8000);
console.log(`工作进程 ${process.pid} 已启动`);
}
- Всего есть 9 процессов, один из которых является основным процессом, количество процессоров x количество ядер процессора = 2 x 4 = 8 дочерних процессов
- Вне зависимости от child_process или cluster, это не многопоточная модель, а многопроцессная модель
- Для решения однопоточных задач обычно используются многопроцессорные методы для имитации многопоточности.
Настоящая многопоточность узла
- В выпуске Node 10.5.0 представлен экспериментальный модуль worker_threads, который предоставляет Node настоящие возможности многопоточности.
- В модуле worker_thread есть 4 объекта и 2 класса.
- isMainThread: Является ли это основным потоком, судят по threadId === 0 в исходном коде.
- MessagePort: используется для связи между потоками, унаследованными от EventEmitter.
- MessageChannel: экземпляр канала для создания асинхронной двусторонней связи.
- threadId: идентификатор потока.
- Рабочий: используется для создания дочерних потоков в основном потоке. Первый параметр — это имя файла, которое представляет запись выполнения дочернего потока.
- parentPort: объект типа MessagePort, представляющий родительский процесс в рабочем потоке, null в основном потоке.
- workerData: используется для передачи данных (копии данных) в основном процессе дочернему процессу.
const {
isMainThread,
parentPort,
workerData,
threadId,
MessageChannel,
MessagePort,
Worker
} = require('worker_threads');
function mainThread() {
for (let i = 0; i < 5; i++) {
const worker = new Worker(__filename, { workerData: i });
worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });
worker.on('message', msg => {
console.log(`main: receive ${msg}`);
worker.postMessage(msg + 1);
});
}
}
function workerThread() {
console.log(`worker: workerDate ${workerData}`);
parentPort.on('message', msg => {
console.log(`worker: receive ${msg}`);
}),
parentPort.postMessage(workerData);
}
if (isMainThread) {
mainThread();
} else {
workerThread();
}
нить связи
const assert = require('assert');
const {
Worker,
MessageChannel,
MessagePort,
isMainThread,
parentPort
} = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
const subChannel = new MessageChannel();
worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
subChannel.port2.on('message', (value) => {
console.log('received:', value);
});
} else {
parentPort.once('message', (value) => {
assert(value.hereIsYourPort instanceof MessagePort);
value.hereIsYourPort.postMessage('the worker is sending this');
value.hereIsYourPort.close();
});
}
Многопроцессорность против многопоточности
Процесс — это наименьшая единица распределения ресурсов, а поток — наименьшая единица планирования ЦП.
управление проектом
Сборка компонентов
Конфигурация с несколькими средами
- Файл конфигурации JSON
- переменная среды Используйте стороннее управление модулями (nconf)
управление зависимостями
- зависимости: зависимости, необходимые для нормальной работы модуля
- devDependencies: зависимости, необходимые для разработки
- optionDependencies: необязательные зависимости, улучшенные до некоторой степени.
- peerDependencies: зависимости времени выполнения, ограниченная версия
Обработка исключений
Обработка неперехваченных исключений
- Если разработчик не забудет добавить оператор .catch, ошибки, возникающие в этих местах, не будут обрабатываться обработчиком событий uncaughtException, а затем исчезнут.
- Приложения Node не будут аварийно завершать работу, но могут вызвать утечку памяти
process.on('uncaughtException', (error) => {
// 我刚收到一个从未被处理的错误
// 现在处理它,并决定是否需要重启应用
errorManagement.handler.handleError(error);
if (!errorManagement.handler.isTrustedError(error)) {
process.exit(1);
}
});
process.on('unhandledRejection', (reason, p) => {
// 我刚刚捕获了一个未处理的promise rejection,
// 因为我们已经有了对于未处理错误的后备的处理机制(见下面)
// 直接抛出,让它来处理
throw reason;
});
Управление исключениями по домену
- Создайте экземпляр с помощью метода create модуля домена.
- Ошибка и любая другая ошибка будут обрабатываться одним и тем же обработчиком ошибок.
- Любой код, вызывающий ошибку в этом обратном вызове, будет переопределен доменом.
- Позволяет нашему коду запускаться в песочнице и может использовать объект res для обратной связи с пользователем.
const domain = require('domain');
const audioDomain = domain.create();
audioDomain.on('error', function(err) {
console.log('audioDomain error:', err);
});
audioDomain.run(function() {
const musicPlayer = new MusicPlayer();
musicPlayer.play();
});
Параметры аутентификации Джой
const memberSchema = Joi.object().keys({
password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
birthyear: Joi.number().integer().min(1900).max(2013),
email: Joi.string().email(),
});
function addNewMember(newMember) {
//assertions come first
Joi.assert(newMember, memberSchema); //throws if validation fails
//other logic here
}
Мониторинг системы Kibana
Онлайн-практика
Ведение журнала с Уинстоном
var winston = require('winston');
var moment = require('moment');
const logger = new (winston.Logger)({
transports: [
new (winston.transports.Console)({
timestamp: function() {
return moment().format('YYYY-MM-DD HH:mm:ss')
},
formatter: function(params) {
let time = params.timestamp() // 时间
let message = params.message // 手动信息
let meta = params.meta && Object.keys(params.meta).length ? '\n\t'+ JSON.stringify(params.meta) : ''
return `${time} ${message}`
},
}),
new (winston.transports.File)({
filename: `${__dirname}/../winston/winston.log`,
json: false,
timestamp: function() {
return moment().format('YYYY-MM-DD HH:mm:ss')
},
formatter: function(params) {
let time = params.timestamp() // 时间
let message = params.message // 手动信息
let meta = params.meta && Object.keys(params.meta).length ? '\n\t'+ JSON.stringify(params.meta) : ''
return `${time} ${message}`
}
})
]
})
module.exports = logger
// logger.error('error')
// logger.warm('warm')
// logger.info('info')
Делегировать обратный прокси
Node плохо справляется с ресурсоемкими задачами, такими как gzip, завершение SSL и т. д. Вместо этого лучше использовать настоящую службу промежуточного программного обеспечения, такую как Nginx. В противном случае бедный однопоточный узел, к сожалению, будет занят обработкой сетевых задач, а не ядра приложения, и производительность соответственно снизится.
Хотя express.js обрабатывает статические файлы через некоторое промежуточное ПО для подключения, вам не следует его использовать. Nginx лучше обрабатывает статические файлы и предотвращает засорение процесса нашего узла запросами динамического содержимого.
# 配置 gzip 压缩
gzip on;
gzip_comp_level 6;
gzip_vary on;
# 配置 upstream
upstream myApplication {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
keepalive 64;
}
#定义 web server
server {
# configure server with ssl and error pages
listen 80;
listen 443 ssl;
ssl_certificate /some/location/sillyfacesociety.com.bundle.crt;
error_page 502 /errors/502.html;
# handling static content
location ~ ^/(images/|img/|javascript/|js/|css/|stylesheets/|flash/|media/|static/|robots.txt|humans.txt|favicon.ico) {
root /usr/local/silly_face_society/node/public;
access_log off;
expires max;
}
Обнаружение уязвимых зависимостей
Конфигурация HTTP-кластера PM2
Конфигурация рабочего потока
-
pm2 start app.js -i 4
,-i 4
Запустите приложение в cluster_mode с 4 рабочими потоками, если настроено0
, PM2 будет генерировать соответствующие рабочие потоки в зависимости от количества ядер ЦП. - Рабочий поток зависает PM2 немедленно перезапустит его
-
pm2 scale <app name> <n>
Масштабировать кластер
PM2 запускается автоматически
-
pm2 save
Сохраните текущее запущенное приложение -
pm2 startup
запускать
исполнительская практика
Избегайте лодаша
- Использование библиотеки методов, такой как lodash, приводит к ненужным зависимостям и снижению производительности.
- С введением нового движка V8 и нового стандарта ES собственные методы были улучшены и теперь работают на 50 % лучше, чем библиотека методов.
Используйте плагин ESLint для обнаружения:
{
"extends": [
"plugin:you-dont-need-lodash-underscore/compatible"
]
}
benchmark
const _ = require('lodash'),
__ = require('underscore'),
Suite = require('benchmark').Suite,
opts = require('./utils');
//cf. https://github.com/Berkmann18/NativeVsUtils/blob/master/utils.js
const concatSuite = new Suite('concat', opts);
const array = [0, 1, 2];
concatSuite.add('lodash', () => _.concat(array, 3, 4, 5))
.add('underscore', () => __.concat(array, 3, 4, 5))
.add('native', () => array.concat(3, 4, 5))
.run({ 'async': true });
Профилирование с проф.
- Используйте инструмент тикового процессора для обработки анализа
node --prof profile-test.js
npm install tick -g
node-tick-processor
Использование моментальных снимков кучи дампа головы
- Модуль загрузки кода для создания файла моментального снимка
- Профили Chrome загружают файл моментального снимка
yarn add heapdump -D
const heapdump = require('heapdump');
const string = '1 string to rule them all';
const leakyArr = [];
let count = 2;
setInterval(function () {
leakyArr.push(string.replace(/1/g, count++));
}, 0);
setInterval(function () {
if (heapdump.writeSnapshot()) console.log('wrote snapshot');
}, 20000);
Контрольный список безопасности приложений
шлем устанавливает заголовок ответа безопасности
Определить конфигурацию заголовка:Security Headers.
Приложения должны использовать безопасные заголовки, чтобы предотвратить использование злоумышленниками распространенных методов атаки, таких как межсайтовый скриптинг (XSS), подделка межсайтовых запросов (CSRF). можно использовать модулиhelmetПростота настройки.
- структура
- X-Frame-Options: тот же источник. Обеспечивает защиту от кликджекинга, фреймы могут быть только одного происхождения.
- коробка передач
- Строгая транспортная безопасность: max-age=31536000; includeSubDomains. Force HTTPS, который уменьшает количество ошибок в веб-приложениях с помощью файлов cookie и внешних ссылок, утечек данных сеанса и предотвращения атак «человек посередине».
- содержание
- X-Content-Type-Options: nosniff. Предотвращает прослушивание ответов от объявленных типов контента, снижая риск загрузки пользователями вредоносного контента.
- Тип содержимого: текст/html; кодировка = utf-8. Дайте браузеру указание интерпретировать страницу как определенный тип контента, а не полагаться на то, что браузер делает предположения.
- XSS
- X-XSS-защита: 1, режим = блокировка. Фильтры межсайтового скриптинга (XSS), встроенные в последние веб-браузеры, включены.
- скачать
- X-Параметры загрузки: noopen.
- тайник
- Кэш-контроль: без кеша. Данные, возвращаемые веб-приложением, могут кэшироваться браузером пользователя, а также промежуточными прокси-серверами. Директива предписывает им не сохранять содержимое страницы, чтобы другие могли получить доступ к конфиденциальному содержимому из этих кешей.
- Прагма: без кеша. То же
- Истекает: -1. Данные, возвращаемые в веб-ответе, могут кэшироваться браузером пользователя, а также промежуточными прокси-серверами. Эта директива предотвращает это, устанавливая время истечения в значение.
- Контроль доступа
- Access-Control-Allow-Origin: не *. «Access-Control-Allow-Origin: *» по умолчанию отключен в современных браузерах.
- X-Permitted-Cross-Domain-Policies: только мастер. Указывает, что только указанный файл считается действительным в этом домене.
- Политика безопасности контента
- Content-Security-Policy: Политики безопасности контента должны быть тщательно настроены и точно определены.
- информация о сервере
- Сервер: Не отображается.
Использование плагина security-linter
Использование плагина проверки безопасностиeslint-plugin-securityилиtslint-config-security.
koa-ratelimit ограничение одновременных запросов
DOS-атаки очень популярны, и с ними относительно легко бороться. Используйте внешнюю службу, такую как облачная балансировка нагрузки, облачный брандмауэр, nginx или (для небольших, не столь важных приложений) промежуточное ПО, ограничивающее скорость (например,koa-ratelimit) для реализации ограничения скорости.
Размещение конфиденциальной информации в виде обычного текста
Конфиденциальная информация, хранящаяся в системе управления версиями, должна быть зашифрована и управляться (переменные ключи, время истечения срока действия, аудит и т. д.). Используйте обработчики предварительной фиксации/передачи, чтобы предотвратить случайную фиксацию конфиденциальной информации.
Библиотека ORM/ODM предотвращает уязвимости внедрения запросов
Чтобы предотвратить внедрение SQL/NoSQL и другие злонамеренные атаки, всегда используйте ORM/ODM или библиотеку базы данных для экранирования данных или поддержки именованных или индексированных параметризованных запросов, а также тщательно проверяйте ожидаемый тип пользовательского ввода. Не используйте просто строки шаблона JavaScript или конкатенацию строк для вставки значений в операторы запроса, так как это откроет приложение для широкого спектра уязвимостей.
Библиотеки:
- TypeORM
- sequelize
- mongoose
- Knex
- Objection.js
- waterline
Используйте Bcrypt вместо Crypto
Пароли или секреты (ключи API) должны использовать безопасную функцию хэш + соль (bcrypt) для хранения, что должно быть предпочтительным для его реализации JavaScript из соображений производительности и безопасности.
// 使用10个哈希回合异步生成安全密码
bcrypt.hash('myPassword', 10, function(err, hash) {
// 在用户记录中存储安全哈希
});
// 将提供的密码输入与已保存的哈希进行比较
bcrypt.compare('somePassword', hash, function(err, match) {
if(match) {
// 密码匹配
} else {
// 密码不匹配
}
});
Избегайте вывода HTML, JS и CSS
Ненадежные данные, отправленные в браузер, могут выполняться, а не отображаться, что часто называют атакой межсайтового скриптинга (XSS). Эту проблему можно смягчить, используя специализированные библиотеки для явной маркировки данных как обычного текстового содержимого, которое не следует выполнять (например, кодирование, экранирование).
Проверять входящие схемы JSON
Проверьте полезные данные тела входящего запроса и убедитесь, что они соответствуют ожидаемым требованиям, если нет, быстро сообщите об ошибке. Чтобы избежать громоздкого кодирования проверки на каждом маршруте, вы можете использовать упрощенную схему проверки на основе JSON, такую какjsonschemaилиjoi
JWT с поддержкой черного списка
При использовании веб-токенов JSON (например, черезPassport.js), по умолчанию нет механизма отзыва доступа для выпущенных токенов. После обнаружения какой-либо вредоносной активности пользователя невозможно предотвратить их доступ к системе, пока они владеют действительным токеном. Эта проблема смягчается путем внедрения черного списка ненадежных токенов и их проверки при каждом запросе.
const jwt = require('express-jwt');
const blacklist = require('express-jwt-blacklist');
app.use(jwt({
secret: 'my-secret',
isRevoked: blacklist.isRevoked
}));
app.get('/logout', function (req, res) {
blacklist.revoke(req.user)
res.sendStatus(200);
});
Ограничение разрешенных запросов на вход для каждого пользователя
Класс промежуточного программного обеспечения, защищающий от взлома методом полного перебора, такой как экспресс-перебор, следует использовать в экспресс-приложениях для предотвращения атак методом полного перебора/словаря; такие атаки в основном используются для некоторых чувствительных маршрутов, таких как/admin
или/login
, на основе определенных атрибутов запроса, таких как имя пользователя, или других идентификаторов, таких как параметры тела и т. д. В противном случае злоумышленник может предпринимать неограниченное количество попыток подбора пароля для получения доступа к привилегированным учетным записям в приложении.
const ExpressBrute = require('express-brute');
const RedisStore = require('express-brute-redis');
const redisStore = new RedisStore({
host: '127.0.0.1',
port: 6379
});
// Start slowing requests after 5 failed
// attempts to login for the same user
const loginBruteforce = new ExpressBrute(redisStore, {
freeRetries: 5,
minWait: 5 * 60 * 1000, // 5 minutes
maxWait: 60 * 60 * 1000, // 1 hour
failCallback: failCallback,
handleStoreError: handleStoreErrorCallback
});
app.post('/login',
loginBruteforce.getMiddleware({
key: function (req, res, next) {
// prevent too many attempts for the same username
next(req.body.username);
}
}), // error 403 if we hit this route too often
function (req, res, next) {
if (User.isValidLogin(req.body.username, req.body.password)) {
// reset the failure counter for valid login
req.brute.reset(function () {
res.redirect('/'); // logged in
});
} else {
// handle invalid user
}
}
);
Запустите Node.js с пользователем без полномочий root
Node.js запускается от имени пользователя root с неограниченными привилегиями, что является распространенным сценарием. Например, в контейнере Docker это поведение по умолчанию. Рекомендуется создать пользователя без полномочий root и сохранить его в образ Docker (пример приведен ниже) или запустить процесс от имени этого пользователя, вызвав контейнер с «-u username». В противном случае злоумышленник, запускающий скрипт на сервере, получает неограниченные права на локальной машине (например, менять iptable, перенаправлять трафик на свой сервер)
FROM node:latest
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
USER node
CMD ["node", "server.js"]
Ограничьте размер полезной нагрузки с помощью обратного прокси-сервера или промежуточного программного обеспечения.
Чем больше полезная нагрузка в теле запроса, тем сложнее одному потоку Node.js с ней справиться. Это возможность для злоумышленника поставить сервер на колени без флуда запросов (DOS/DDOS-атака). Ограничьте размер тела входящих запросов на границе (например, брандмауэр, ELB) или настройтеexpress body parser
Принятие только небольших нагрузок смягчает эту проблему. В противном случае вашему приложению придется обрабатывать большие запросы и оно не сможет выполнять другую важную работу, которую оно должно выполнять, что приведет к снижению производительности и уязвимости к DOS-атакам.
выражать:
const express = require('express');
const app = express();
// body-parser defaults to a body size limit of 300kb
app.use(express.json({ limit: '300kb' }));
// Request with json body
app.post('/json', (req, res) => {
// Check if request payload content-type matches json
// because body-parser does not check for content types
if (!req.is('json')) {
return res.sendStatus(415); // Unsupported media type if request doesn't have JSON body
}
res.send('Hooray, it worked!');
});
app.listen(3000, () => console.log('Example app listening on port 3000!'));
нгинкс:
http {
...
# Limit the body size for ALL incoming requests to 1 MB
client_max_body_size 1m;
}
server {
...
# Limit the body size for incoming requests to this specific server block to 1 MB
client_max_body_size 1m;
}
location /upload {
...
# Limit the body size for incoming requests to this route to 1 MB
client_max_body_size 1m;
}
Запретить RegEx перегружать NodeJS
Пользовательский ввод, который соответствует тексту, требует много циклов процессора для обработки. Обработка регулярных выражений в некоторой степени неэффективна, например, один запрос на проверку 10 слов может заблокировать весь цикл обработки событий на срок до 6 секунд. По этой причине предпочтение отдается сторонним пакетам проверки подлинности, таким какvalidator.js, вместо использования регулярного выражения или используйтеsafe-regexдля обнаружения проблемных регулярных выражений.
const saferegex = require('safe-regex');
const emailRegex = /^([a-zA-Z0-9])(([\-.]|[_]+)?([a-zA-Z0-9]+))*(@){1}[a-z0-9]+[.]{1}(([a-z]{2,3})|([a-z]{2,3}[.]{1}[a-z]{2,3}))$/;
// should output false because the emailRegex is vulnerable to redos attacks
console.log(saferegex(emailRegex));
// instead of the regex pattern, use validator:
const validator = require('validator');
console.log(validator.isEmail('liran.tal@gmail.com'));
Запуск небезопасного кода в песочнице
Когда задача выполняет внешний код, указанный во время выполнения (например, подключаемый модуль), используйте любую песочницу для выполнения основного кода, дружественного к среде, и изолируйте основной код от подключаемого модуля. Этого можно достичь с помощью выделенного процесса (например, cluster.fork() ), бессерверной среды или выделенного пакета npm, который действует как песочница.
- Выделенный подпроцесс — это обеспечивает быструю изоляцию информации, но требует ограничения подпроцесса, ограничения времени его выполнения и восстановления после ошибок.
- Облачная бессерверная среда отвечает всем требованиям песочницы, но динамическое развертывание и вызов методов Faas не являются частью этого раздела.
- Некоторые библиотеки npm, такие какsandboxиvm2Позволяет изолированное выполнение кода с помощью одной строки кода. Хотя последний вариант выигрывает в простоте, он предлагает ограниченную защиту.
const Sandbox = require("sandbox");
const s = new Sandbox();
s.run( "lol)hai", function( output ) {
console.log(output);
//output='Synatx error'
});
// Example 4 - Restricted code
s.run( "process.platform", function( output ) {
console.log(output);
//output=Null
})
// Example 5 - Infinite loop
s.run( "while (true) {}", function( output ) {
console.log(output);
//output='Timeout'
})
Скрыть сведения об ошибке для клиентов
По умолчанию встроенный экспресс-обработчик ошибок скрывает сведения об ошибках. Однако, по всей вероятности, вы реализуете свою собственную логику обработки ошибок с помощью специального объекта ошибки (многие считают это лучшей практикой). Если вы сделаете это, не возвращайте клиенту весь объект Error, который может содержать некоторые конфиденциальные сведения о приложении. В противном случае конфиденциальные сведения о приложении, такие как пути к файлам сервера, используемые сторонние модули и другие внутренние рабочие процессы приложения, которые могут быть использованы злоумышленниками, могут быть утеряны из информации, найденной трассировкой стека.
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
Для npm или Yarn настройте 2FA
Любой шаг в цепочке разработки должен быть защищен с помощью MFA (многофакторной аутентификации), npm/Yarn — хорошая возможность для злоумышленников, которые могут получить пароль какого-либо разработчика. Используя учетные данные разработчика, злоумышленники могут внедрить вредоносный код в библиотеки, широко установленные в проектах и службах. Это может быть даже опубликовано в Интернете. Если в npm включена двухфакторная аутентификация, у злоумышленников мало шансов изменить код вашего пакета.
настройки промежуточного программного обеспечения сеанса
Каждая веб-инфраструктура и технология имеют свои известные недостатки, и злоумышленникам было бы очень полезно узнать, какую веб-инфраструктуру мы используем. Используя настройки промежуточного программного обеспечения сеанса по умолчанию, вы можете использовать что-то вродеX-Powered-Byheader
способ подвергнуть ваше приложение атакам перехвата модулей и фреймворков. Постарайтесь скрыть все, что идентифицирует и раскрывает технический стек (например, Nonde.js, экспресс). В противном случае файлы cookie могут быть отправлены через небезопасное соединение, а злоумышленники могут использовать идентификаторы сеансов для идентификации базовой структуры веб-приложения, а также уязвимостей, связанных с конкретными модулями.
// using the express session middleware
app.use(session({
secret: 'youruniquesecret', // secret string used in the signing of the session ID that is stored in the cookie
name: 'youruniquename', // set a unique name to remove the default connect.sid
cookie: {
httpOnly: true, // minimize risk of XSS attacks by restricting the client from reading the cookie
secure: true, // only send cookie over https
maxAge: 60000*60*24 // set cookie expiry length in ms
}
}));
csurf предотвращает CSRF
Уровень маршрутизации:
var cookieParser = require('cookie-parser');
var csrf = require('csurf');
var bodyParser = require('body-parser');
var express = require('express');
// 设置路由中间件
var csrfProtection = csrf({ cookie: true });
var parseForm = bodyParser.urlencoded({ extended: false });
var app = express();
// 我们需要这个,因为在 csrfProtection 中 “cookie” 是正确的
app.use(cookieParser());
app.get('/form', csrfProtection, function(req, res) {
// 将 CSRFToken 传递给视图
res.render('send', { csrfToken: req.csrfToken() });
});
app.post('/process', parseForm, csrfProtection, function(req, res) {
res.send('data is being processed');
});
Слой отображения:
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
Favorite color: <input type="text" name="favoriteColor">
<button type="submit">Submit</button>
</form>
Комплексное приложение
смотреть сервис
const fs = require('fs');
const exec = require('child_process').exec;
function watch() {
const child = exec('node server.js');
const watcher = fs.watch(__dirname + '/server.js', function () {
console.log('File changed, reloading.');
child.kill();
watcher.close();
watch();
});
}
watch();
RESTful веб-приложение
-
REST означает передачу репрезентативного состояния
-
Создавайте семантические RESTful API с правильными HTTP-методами, URL-адресами и заголовками.
-
ПОЛУЧИТЬ /gages: получить
-
ПОСТ/страницы: создать
-
ПОЛУЧИТЬ /pages/10: получить страницы10
-
ПАТЧ /pages/10: обновить страницы10
-
PUT /pages/10: заменить страницы10
-
УДАЛИТЬ /pages/10: удалить страницы10
let app;
const express = require('express');
const routes = require('./routes');
module.exports = app = express();
app.use(express.json()); // 使用 JSON body 解析
app.use(express.methodOverride()); // 允许一个查询参数来制定额外的 HTTP 方法
// 资源使用的路由
app.get('/pages', routes.pages.index);
app.get('/pages/:id', routes.pages.show);
app.post('/pages', routes.pages.create);
app.patch('/pages/:id', routes.pages.patch);
app.put('/pages/:id', routes.pages.update);
app.del('/pages/:id', routes.pages.remove);
промежуточное ПО
const express = require('express');
const app = express();
const Schema = require('validate');
const xml2json = require('xml2json');
const util = require('util');
const Page = new Schema();
Page.path('title').type('string').required(); // 数据校验确保页面有标题
function ValidatorError(errors) { // 从错误对象继承,校验出现的错误在错误中间件处理
this.statusCode = 400;
this.message = errors.join(', ');
}
util.inherits(ValidatorError, Error);
function xmlMiddleware(req, res, next) { // 处理 xml 的中间件
if (!req.is('xml')) return next();
let body = '';
req.on('data', function (str) { // 从客户端读到数据时触发
body += str;
});
req.on('end', function () {
req.body = xml2json.toJson(body.toString(), {
object: true,
sanitize: false,
});
next();
});
}
function checkValidXml(req, res, next) { // 数据校验中间件
const page = Page.validate(req.body.page);
if (page.errors.length) {
next(new ValidatorError(page.errors)); // 传递错误给 next 阻止路由继续运行
} else {
next();
}
}
function errorHandler(err, req, res, next) { // 错误处理中间件
console.error('errorHandler', err);
res.send(err.statusCode || 500, err.message);
}
app.use(xmlMiddleware); // 应用 XML 中间件到所有的请求中
app.post('/pages', checkValidXml, function (req, res) { // 特定的请求校验 xml
console.log('Valid page:', req.body.page);
res.send(req.body);
});
app.use(errorHandler); // 添加错误处理中间件
app.listen(3000);
Организация приложений по событиям
// 监听用户注册成功消息,绑定邮件程序
const express = require('express');
const app = express();
const emails = require('./emails');
const routes = require('./routes');
app.use(express.json());
app.post('/users', routes.users.create); // 设置路由创建用户
app.on('user:created', emails.welcome); // 监听创建成功事件,绑定 email 代码
module.exports = app;
// 用户注册成功发起事件
const User = require('./../models/user');
module.exports.create = function (req, res, next) {
const user = new User(req.body);
user.save(function (err) {
if (err) return next(err);
res.app.emit('user:created', user); // 当用户成功注册时触发创建用户事件
res.send('User created');
});
};
WebSocket и сеанс
const express = require('express');
const WebSocketServer = require('ws').Server;
const parseCookie = express.cookieParser('some secret'); // 加载解析 cookie 中间件,设置密码
const MemoryStore = express.session.MemoryStore; // 加载要使用的会话存储
const store = new MemoryStore();
const app = express();
const server = app.listen(process.env.PORT || 3000);
app.use(parseCookie);
app.use(express.session({ store: store, secret: 'some secret' })); // 告知 Express 使用会话存储和设置密码(使用 session 中间件)
app.use(express.static(__dirname + '/public'));
app.get('/random', function (req, res) { // 测试测试用的会话值
req.session.random = Math.random().toString();
res.send(200);
});
// 设置 WebSocket 服务器,将其传递给 Express 服务器
// 需要传递已有的 Express 服务(listen 的返回对象)
const webSocketServer = new WebSocketServer({ server: server });
// 在连接事件给客户端创建 WebSocket
webSocketServer.on('connection', function (ws) {
let session;
ws.on('message', function (data, flags) {
const message = JSON.parse(data);
// 客户端发送的 JSON,需要一些代码来解析 JSON 字符串确定是否可用
if (message.type === 'getSession') {
parseCookie(ws.upgradeReq, null, function (err) {
// 从 HTTP 的更新请求中获取 WebSocket 的会话 ID
// 一旦 WebSockets 服务器有一个连接,session ID 可以用=从初始化请求中的 cookies 中获取
const sid = ws.upgradeReq.signedCookies['connect.sid'];
// 从存储中获取用户的会话信息
// 只需要在初始化的请求中传递一个引用给解析 cookie 的中间件
// 然后 session 可以使用 session 存储的 get 方法加载
store.get(sid, function (err, loadedSession) {
if (err) console.error(err);
session = loadedSession;
ws.send('session.random: ' + session.random, {
mask: false,
}); // session 加载后会把一个包含了 session 值的消息发回给客户端
});
});
} else {
ws.send('Unknown command');
}
});
});
<!DOCTYPE html>
<html>
<head>
<script>
const host = window.document.location.host.replace(/:.*/, '');
const ws = new WebSocket('ws://' + host + ':3000');
setInterval(function () {
ws.send('{ "type": "getSession" }'); // 定期向服务器发送消息
}, 1000);
ws.onmessage = function (event) {
document.getElementById('message').innerHTML = event.data;
};
</script>
</head>
<body>
<h1>WebSocket sessions</h1>
<div id='message'></div><br>
</body>
</html>
Промежуточное ПО Express4
package | описывать |
---|---|
body-parser | Анализ данных тела запроса в формате URL и JSON POST |
compression | Сжать ответ сервера |
connect-timeout | запрос разрешенного тайм-аута |
cookie-parser | Разобрать файлы cookie из заголовков HTTP и поместить результат в req.cookies. |
cookie-session | Используйте файлы cookie для поддержки простых сеансов |
csurf | Добавьте токен в сеанс для защиты от CSRF-атак. |
errorhandler | Обработка ошибок по умолчанию, используемая в Connect |
express-session | Простая обработка сеанса, используйте расширение store для записи информации о сеансе в базу данных или файл. |
method-override | Сопоставьте новые глаголы HTTP с _method в переменных запроса |
morgan | форматирование журнала |
response-time | Отслеживайте время отклика |
serve-favicon | отправить значок |
serve-index | список каталогов |
whost | Разрешить маршрутам соответствовать субдоменам |
JWT
JSON Web Token (сокращенно JWT) в настоящее время является самым популярным решением для междоменной аутентификации.
Междоменная аутентификация
Общий процесс
- Пользователь отправляет имя пользователя и пароль на сервер
- После аутентификации сервера в текущем сеансе (сеансе) сохраняются соответствующие данные, такие как роль пользователя, время входа в систему и т. д.
- Сервер возвращает пользователю session_id и записывает куки пользователя.
- Каждый последующий запрос пользователя будет передавать session_id обратно на сервер через файл cookie.
- Сервер получает session_id, находит данные, сохраненные в предыдущий период, и, таким образом, знает личность пользователя.
совместное использование сеанса
В кластере серверов требуется совместное использование данных сеанса, и каждый сервер может читать сеанс:
- Одним из решений является сохранение данных сеанса, запись их в базу данных или другой уровень сохранения. После того, как различные службы получают запросы, все они запрашивают данные из уровня сохраняемости. Преимущество этой схемы в том, что структура понятна, а недостаток в том, что объем инженерных работ относительно велик. Кроме того, в случае сбоя уровня сохраняемости будет единая точка отказа.
- Другое решение заключается в том, что сервер просто не сохраняет данные сессии, все данные хранятся на стороне клиента, а каждый запрос отправляется обратно на сервер. JWT является представителем такой схемы.
JWT
принцип
- После аутентификации сервера создается объект JSON, который отправляется обратно пользователю.
- Когда пользователь связывается с сервером, этот объект JSON должен быть отправлен обратно, и сервер использует этот объект только для идентификации пользователя.
- Для предотвращения фальсификации будет добавлена подпись
структура данных
Заголовок.Полезная нагрузка.Подпись:
- Заголовок: JSON, преобразованный в строку с использованием URL-адреса Base64.
- Полезная нагрузка: JSON, преобразованный в строку с использованием URL-адреса Base64.
- Подпись: подпись первых двух частей
Header
{
"alg": "HS256", // 签名的算法
"typ": "JWT" // token 的类型
}
Payload
{
// 7 个官方字段
"iss": "签发人",
"exp": "过期时间",
"sub": "主题",
"aud": "受众",
"nbf": "生效时间",
"iat": "签发时间",
"jti": "编号",
// 定义私有字段
"name": "Chenng"
}
Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret) # secret 秘钥只有服务器知道
Как пользоваться
- Клиент получает JWT, возвращенный сервером, который может быть сохранен в файле cookie или в локальном хранилище.
- Он автоматически отправляется в файле cookie, но он не может быть междоменным, поэтому лучше указать его в поле «Авторизация» заголовка HTTP-запроса.
Функции
- JWT можно использовать не только для аутентификации, но и для обмена информацией. Эффективное использование JWT может сократить количество запросов сервера к базе данных.
- Самый большой недостаток JWT заключается в том, что, поскольку сервер не сохраняет состояние сеанса, невозможно отозвать токен во время использования или изменить разрешения токена. То есть после выдачи JWT он останется действительным до истечения срока его действия, если только сервер не развернет дополнительную логику.
- Сам JWT содержит информацию об аутентификации, и после утечки любой может получить все разрешения токена. Чтобы уменьшить кражу, срок действия JWT должен быть относительно коротким. Для некоторых более важных разрешений пользователь должен пройти повторную аутентификацию при использовании
koa
основной объект
- Получение HTTP-ответа на синтаксический анализ
- контекст выполнения промежуточного ПО
- Все процессы в Koa являются промежуточным программным обеспечением.
Исходный состав
- application
- context
- request
- response
Использование промежуточного программного обеспечения
const Koa = require('koa');
const app = new Koa();
const mid1 = async (ctx, next) => {
ctx.body = 'Hi';
await next(); // next 执行下一个中间件
ctx.body += ' there';
};
const mid2 = async (ctx, next) => {
ctx.type = 'text/html; chartset=utf-8';
await next();
};
const mid3 = async (ctx, next) => {
ctx.body += ' chenng';
await next();
};
app.use(mid1);
app.use(mid2);
app.use(mid3);
app.listen(2333);
// Hi chenng there
Вернуться к собственности
router
.get('/api/dynamic_image/codewars', async (ctx, next) => {
const res = await axios.get('https://www.codewars.com/users/ringcrl');
const [, kyu, score] = res.data
.match(/<div class="stat"><b>Rank:<\/b>(.+?)<\/div><div class="stat"><b>Honor:<\/b>(.+?)<\/div>/);
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="20">
<rect x="0" y="0" width="80" height="20" fill="#fff" stroke-width="2" stroke="#cccccc"></rect>
<rect x="0" y="0" width="50" height="20" fill="#5b5b5b"></rect>
<text x="5" y="15" class="small" fill="#fff" style="font-size: 14px;">${kyu}</text>
<rect x="50" y="0" width="30" height="20" fill="#3275b0"></rect>
<text x="53" y="15" class="small" fill="#fff" style="font-size: 14px">${score}</text>
</svg>
`;
ctx.set('Content-Type', 'image/svg+xml');
ctx.body = Buffer.from(svg);
await next();
});
Дизайн веб-API
необходимость
- легко использовать
- легко изменить
- хорошая прочность
- Не боится выступать на публике
Важные рекомендации
- Дизайн легко запомнить, а функция понятна с первого взгляда.
- Используйте соответствующий метод HTTP
- Выберите правильное английское слово, обратите внимание на формы единственного и множественного числа слова
- Аутентификация с помощью OAuth 2.0
Веб-сайт общих ресурсов API ProgrammableWeb (www.programmableweb.com) Существуют различные опубликованные документы Web API, взгляните
шифрование с открытым ключом дешифрование с закрытым ключом
Сгенерировать открытый ключ и закрытый ключ
利用 openssl 生成公钥私钥
生成公钥:openssl genrsa -out rsa_private_key.pem 1024
生成私钥:openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
использование криптографии
const crypto = require('crypto');
const fs = require('fs');
const publicKey = fs.readFileSync(`${__dirname}/rsa_public_key.pem`).toString('ascii');
const privateKey = fs.readFileSync(`${__dirname}/rsa_private_key.pem`).toString('ascii');
console.log(publicKey);
console.log(privateKey);
const data = 'Chenng';
console.log('content: ', data);
//公钥加密
const encodeData = crypto.publicEncrypt(
publicKey,
Buffer.from(data),
).toString('base64');
console.log('encode: ', encodeData);
//私钥解密
const decodeData = crypto.privateDecrypt(
privateKey,
Buffer.from(encodeData, 'base64'),
);
console.log('decode: ', decodeData.toString());
Интерфейс кэша Redis
- Некоторые данные, которые не обновляются в реальном времени, кэшируются с помощью Redis.
- Используйте node-schedule для вызова интерфейса каждую ночь
повторное использование
const redis = require('redis');
const redisClient = redis.createClient();
const getAsync = promisify(redisClient.get).bind(redisClient);
let codewarsRes = JSON.parse(await getAsync('codewarsRes'));
if (!codewarsRes) {
const res = await axios.get('https://www.codewars.com/users/ringcrl');
codewarsRes = res.data;
redisClient.set('codewarsRes', JSON.stringify(codewarsRes), 'EX', 86000);
}
использование узла-расписания
const schedule = require('node-schedule');
const axios = require('axios');
schedule.scheduleJob('* 23 59 * *', function () {
axios.get('https://static.chenng.cn/api/dynamic_image/leetcode_problems');
axios.get('https://static.chenng.cn/api/dynamic_image/leetcode');
axios.get('https://static.chenng.cn/api/dynamic_image/codewars');
});