Node.js — это встроенный браузер Chrome.V8引擎
в среде выполнения JavaScript,
использовать单线程
,事件驱动
,非阻塞I/O
способ достижения большого количества одновременных запросов,libuv
Предоставляет возможность асинхронного программирования.
Архитектурная композиция
Из этого рисунка видно, что базовая структура Node.js состоит изСтандартная библиотека Node.js,Node bindings,базовая библиотекаСостоит из трех частей.
Стандартная библиотека Node.js
Этот слой написан Javascript, то есть API, который мы можем напрямую вызывать во время использования, что можно увидеть в каталоге lib в исходном коде, напримерhttp、fs、events
и другие часто используемые основные модули
Node bindings
Этот уровень можно понимать как связь между библиотеками javascript и C/C++.桥
,
Через этот мост базовая библиотека C/C++ подвергается воздействию среды javascript, в то время какjs传入V8
, после разбора и передачиlibuv
положить начало非阻塞I/O
, и ждать事件循环
планирование;
базовая библиотека
Этот слой в основном состоит из следующих четырех частей:
- V8: виртуальная машина Javascript, запущенная Google, предоставляет среду для запуска Javascript вне браузера;
- libuv: предоставляет Node.js кроссплатформенность, пул потоков, пул событий, асинхронный ввод-вывод и другие возможности, что является основной причиной эффективности Nodejs;
- C-ares: обеспечивает возможность асинхронной обработки DNS;
- http_parser, OpenSSL, zlib и т. д.: предоставление возможностей, включая синтаксический анализ http, SSL, сжатие данных и т. д.;
Кстати, взгляните на архитектурную схему libuv, вы можете увидеть Nodejs.网络I/O
,文件I/O
,DNS操作
, и некоторый пользовательский код работают в libuv.
один поток
Мы знаем, что обычно существует две схемы планирования задач: одна单线程串行执行
, порядок выполнения соответствует порядку кодирования, самая большая проблема заключается в том, что многоядерный ЦП не может быть полностью использован.Когда параллелизм чрезвычайно велик, теоретическая вычислительная мощность одноядерного ЦП составляет 100%;
Другой多线程并行处理
, преимущество состоит в том, что можно эффективно использовать многоядерный ЦП, но недостатком является то, что создание и переключение потоков является дорогостоящим, а также связано с такими проблемами, как блокировки и синхронизация состояний.ЦП часто ожидает завершения ввода-вывода, и производительность процессора тратится впустую.
Обычно создание потока для клиентского соединения потребляет 2 МБ памяти, поэтому теоретически сервер с 8G может поддерживать до 4000 параллелизма в приложении Java. Node.js использует только один поток. Когда есть запрос на подключение клиента, он запускает внутренние события. Благодаря неблокирующему вводу-выводу механизм, управляемый событиями, делает его параллельным. Теоретически сервер с памятью 8G может поддерживать соединения от 30 000 до 40 000 пользователей одновременно.
Node.js использует однопоточное решение, которое позволяет избежать сложных проблем, таких как блокировки и синхронизация состояний, а также улучшает использование ЦП. Помимо того, что Node.js эффективен из-за своего единственного потока, он также должен взаимодействовать с неблокирующим вводом-выводом, описанным ниже.
Неблокирующий ввод-вывод
концепция
В первую очередь должно быть понятно, что для сетевого IO будут задействованы два системных объекта:
- Процесс или поток, вызывающий этот ввод-вывод
- Ядро системы (kernel)
И когда происходит операция чтения, она проходит две фазы:
- дождитесь готовности данных
- Копирование данных из ядра в пользовательский процесс Помните, что эти два момента важны, потому что разница между моделью ввода-вывода заключается в том, что разные ситуации выполняются на двух этапах.
Далее разъясните эти понятия:
- Блокировка ввода-вывода: после того, как операция ввода-вывода инициирована, процесс будет заблокирован, и никакие другие операции не будут выполняться до тех пор, пока не будет получен ответ или тайм-аут;
- Неблокирующий ввод-вывод: инициируйте операцию ввода-вывода и немедленно возвращайтесь, не дожидаясь ответа или тайм-аута, позволяя процессу продолжать выполнять другие операции, но постоянно проверяя, готовы ли данные, путем опроса
- Мультиплексный ввод-вывод: он далее делится на выборку, пул и epool. Самым большим преимуществом является то, что один процесс может одновременно обрабатывать ввод-вывод нескольких сетевых подключений.
Основной принцип
select/poll
Эта функция будет постоянно опрашивать все сокеты, за которые она отвечает, и уведомлять пользовательский процесс, когда в определенный сокет поступают данные. а такжеepool
Благодаря механизму уведомления обратного вызова он снижает нагрузку на память и не снижает эффективность из-за большого параллелизма.Это наиболее эффективный механизм событий ввода-вывода в Linux. - Синхронный ввод-вывод: после того, как операция ввода-вывода инициирована, процесс будет заблокирован до тех пор, пока не будет получен ответ или не истечет время ожидания. первые три
阻塞I/O,非阻塞I/O,多路复用I/O
Все они являются синхронным вводом-выводом. Обратите внимание, что неблокирующий ввод-вывод по-прежнему блокируется, когда данные копируются из ядра в пользовательский процесс, поэтому это все еще синхронный ввод-вывод. - Асинхронный ввод-вывод: возврат непосредственно для продолжения выполнения следующего оператора Когда операция ввода-вывода завершена или данные возвращены, процесс, выполняющий операцию ввода-вывода, уведомляется в виде события.
Суммировать:
Разница между блокирующим вводом-выводом и неблокирующим вводом-выводом заключается в том, ждать или возвращаться до завершения операции ввода-вывода или возврата данных (что можно понимать какждать или ждать)
Разница между синхронным вводом-выводом и асинхронным вводом-выводом заключается в следующем: будет ли процесс заблокирован (или другими словами) до завершения операции ввода-вывода или возврата данныхАктивно запрашивать или пассивно ждать уведомления)
концепт дизайна
Поскольку в Node.js принят неблокирующий механизм ввода-вывода, после выполнения кода для чтения данных код, стоящий за ним, будет выполнен немедленно, а код обработки возвращенного результата чтения данных будет помещен в функция обратного вызова, тем самым повышается эффективность выполнения программы. Когда определенный ввод-вывод завершен, поток, выполняющий операцию ввода-вывода, будет уведомлен в виде события, и поток выполнит функцию обратного вызова этого события.
цикл событий
Основной процесс
- Каждый процесс Node.js имеет только один основной поток, выполняющий программный код, формируя
执行栈
(стек контекста выполнения); - Помимо основного потока,
事件队列
(очередь событий), когда пользователь网络请求或者其它的异步操作
Когда он прибудет, он будет поставлен в очередь в очереди событий первым, и он не будет выполняться немедленно, и код не будет заблокирован, а продолжит опускаться до тех пор, пока не будет выполнен код основного потока; - После выполнения кода основного потока, а затем передать
事件循环机制
(Event Loop), проверить, есть ли в очереди событие для обработки, взять первое событие из головы очереди и线程池
Выделите поток для обработки этого события, затем второй, затем третий, пока не будут выполнены все события в очереди. Когда событие выполняется, основной поток будет уведомлен, основной поток выполнит обратный вызов, и поток будет возвращен в пул потоков. Этот процесс называется事件循环
(Цикл событий); - Повторите третий шаг выше;
6 этапов
Уведомление:
- Каждый блок называется этапом обработки цикла событий.
- каждый этап имеет
FIFO(先进先出)执行回调函数的队列
, обычно, когда цикл событий входит в заданный этап, он выполняет все операции, характерные для этого этапа, а затем выполняет события обратного вызова очереди этапов до тех пор, пока очередь не будет исчерпана или не превысит максимальный предел выполнения, а затем цикл событий будет перемещен на следующий этап; - Новые события обработки на этапе опроса могут быть добавлены в очередь ядра, то есть, когда события опроса обрабатываются, добавляются новые события опроса, поэтому длительные события обратного вызова приведут к тому, что время выполнения этапа опроса превысит пороговое значение таймера;
- Когда все этапы выполняются последовательно, говорят, что цикл событий завершил тик.
Обзор фазы:
-
таймеры (таймер) фаза:воплощать в жизнь
setTimeout
а такжеsetInterval
Обратный звонок по расписанию. - фаза ожидающих обратных вызовов (ожидание обратных вызовов): используется для выполнения предыдущего раунда цикла событий, который был отложен до этого раунда.
I/O回调函数
. - фаза бездействия, подготовки (бездействия, подготовки): может использоваться только внутренне.
-
этап опроса (опроса): Самый ответственный этап, выполнение
I/O事件
Обратный вызов, нода заблокируется на этом этапе при правильных условиях. -
проверить (проверить) этап:воплощать в жизнь
setImmediate
Перезвоните. - callbacks закрыть (close callback) этап: выполнить обратный вызов события close, например, внезапно закрылся сокет (socket) или дескриптор (handle);
цикл событийPending
,Idle/Prepare
а такжеClose
Этапы выделены серым цветом, потому что это этапы, которые Node использует внутри.
Код, написанный разработчиками Node.js, представлен только в виде микрозадач в主线
,计时器(Timers)
сцена,轮询(Poll)
сцена и查询(Check)
проходить поэтапно.
- Чтобы обрабатывать события асинхронного ввода-вывода как можно быстрее, цикл обработки событий тикает
总有一种维持 poll 状态的倾向
; - Как долго должна поддерживаться (блокироваться) текущая фаза опроса, определяется
后续 tick 各个阶段是否存在不为空的回调函数队列
а также最近的计时器时间节点
Принимать решение. Если все очереди пусты и таймеров нет, цикл обработки событий无限制地维持在 poll 阶段
; Чтобы понять, что как только функция обратного вызова ввода-вывода добавлена в очередь опроса, она может быть выполнена немедленно; - Все функции обратного вызова в очереди функций обратного вызова фазы проверки взяты из функции setImmediate фазы опроса.
Этап опроса выполняет две основные функции.:
- Когда таймеры таймеров истекают, выполните обратный вызов таймера (setTimeout и setInterval)
- Выполнение обратного вызова ввода/вывода в очереди опроса
- Если цикл обработки событий переходит в фазу опроса, а код не устанавливает таймер, может произойти следующее:
- Если очередь опроса не пуста, цикл обработки событий будет выполнять обратный вызов в очереди синхронно до тех пор, пока очередь не станет пустой или выполняемый обратный вызов не достигнет системного предела.
- Если очередь опроса пуста, может произойти следующее:
- Если код использует setImmediate() для установки обратного вызова, цикл обработки событий завершит фазу опроса и перейдет к фазе проверки, а также выполнит очередь фазы проверки.
- Если в коде не используется setImmediate(), цикл обработки событий на этом этапе будет заблокирован, ожидая, когда обратные вызовы присоединятся к очереди опроса, и немедленно выполнится, если поступит обратная связь. Как только очередь опроса опустеет, цикл событий проверит таймеры, и, если какие-либо таймеры истечет, цикл событий вернется к фазе таймеров и выполнит очередь таймеров.
process.nextTick
process.nextTick() выполняется не на каком-либо этапе цикла событий, а в各个阶段切换的中间执行
, то есть до перехода с одного этапа на следующий.
Здесь также необходимо упомянуть понятия макрозадачи и микрозадачи.Макрозадача относится к задачам, выполняемым на каждом этапе цикла событий, а микрозадача относится к задачам, выполняемым между каждым этапом.
То есть вышеперечисленные шесть этапов относятся к макрозадаче,process.nextTick() 属于 microtask
.
Реализация process.nextTick() не имеет ничего общего с микрозадачей v8, это что-то на уровне Node.js, надо сказать, что поведение process.nextTick() близко к поведению микрозадачи. Promise.then также является типом микрозадачи.
Вы можете «голодать» свой ввод-вывод с помощью рекурсивных вызовов process.nextTick(), предотвращая переход цикла событий к этапу опроса.
Обратные вызовы promise.then выполняются как микропроцессоры, как и process.nextTick .
Хотя, если оба находятся в одной очереди микрозадач, это будет首先执行 process.nextTick 的回调
.
приоритетprocess.nextTick > promise.then = queueMicrotask
анализ случая
Дело номер один
Давайте посмотрим на результаты выполнения этого фрагмента кода в разных средах:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Сначала в среде браузера вывод:
timer1
promise1
timer2
promise2
С помощью предыдущих знаний о цикле событий Javascript в браузерах не могу понять.
Затем мы вставляем егоNode.js v11.0.0
Следующая версия выполняется и получает результат:
timer1;
timer2;
promise1;
promise2;
но если вNode.js v11.0.0
Выполнение в вышеуказанной (включающей) версии приведет к:
timer1;
promise1;
timer2;
promise2;
Причина в том, что в узле v11 только все задачи в очереди фазы таймера будут выполнять очередь микрозадач, а браузер будет выполнять очередь микрозадач до тех пор, пока выполняется задача макроса. setTimeout, setInterval на этапе таймера и сразу на этапе проверки узла v11 изменены в узле v11 для выполнения очереди микрозадач, как только задача на этапе выполняется. Также для согласованности с браузерами.
Случай 2
-
setImmediate
Предназначен для выполнения скрипта после завершения текущей фазы опроса. -
setTimeout
Запланируйте запуск скрипта после прохождения минимального порога в миллисекундах.
не вI/O 回调(即主模块)
сценарий внутри, порядок выполнения двух таймеров не определен, поскольку он ограничен производительностью машины, например:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
Порядок вывода не определен.
Мы знаем, что функция обратного вызова setTimeout выполняется на этапе таймера, а функция обратного вызова setImmediate выполняется на этапе проверки.Начало цикла событий сначала проверит этап таймера, но это займет определенное количество времени. чтобы достичь стадии таймера перед запуском, поэтому есть две ситуации:
- Время подготовки перед таймером превышает 1 мс, и если выполняется цикл->время >= 1, выполняется функция обратного вызова стадии таймера (setTimeout).
- Если время подготовки перед таймером меньше 1 мс, сначала выполняется функция обратного вызова фазы проверки (setImmediate), а в следующий раз цикл событий выполняет функцию обратного вызова фазы таймера (setTimeout).
И если два вызова находятся в обратном вызове ввода-вывода, немедленное всегда выполняется первым.
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
проанализируйте, как показано ниже:
- После выполнения функции обратного вызова fs.readFile;
- Зарегистрируйте функцию обратного вызова setTimeout на этапе таймера;
- Зарегистрируйте функцию обратного вызова setImmediate на этапе проверки;
- Цикл событий выходит из этапа пула и продолжает выполняться на следующем этапе, который является этапом проверки, поэтому функция обратного вызова setImmediate выполняется первой. После того, как этот цикл событий завершится, войдите в следующий цикл событий и выполните функцию обратного вызова setTimeout;
Случай 3
setInterval(() => {
console.log('setInterval')
}, 100)
process.nextTick(function tick () {
process.nextTick(tick)
})
Результат: setInterval никогда не печатает.
process.nextTick будет бесконечно зацикливаться, блокируя цикл событий на этапе микрозадачи, так что функции обратного вызова других этапов макрозадач в цикле событий не будут выполняться. Решение обычно состоит в том, чтобы заменить process.nextTick на setImmediate следующим образом:
setInterval(() => {
console.log('setInterval')
}, 100)
setImmediate(function immediate () {
setImmediate(immediate)
})
Результат выполнения: печатать setInterval каждые 100 мс.
Выполнение process.nextTick в process.nextTick по-прежнему регистрирует функцию тика в конце текущей микрозадачи, поэтому микрозадача никогда не будет выполнена; Выполнение setImmediate внутри setImmediate зарегистрирует немедленную функцию в фазе проверки следующего цикла событий, а не в фазе проверки, выполняемой в данный момент, что дает возможность другим макрозадачам в цикле событий выполниться.
Случай 4
setImmediate(() => {
console.log('setImmediate1')
setImmediate(() => {
console.log('setImmediate2')
})
process.nextTick(() => {
console.log('nextTick')
})
})
setImmediate(() => {
console.log('setImmediate3')
})
Текущий результат ниже node v11:
setImmediate1
setImmediate3
nextTick
setImmediate2
На узле v11 и выше это:
setImmediate1
nextTick
setImmediate3
setImmediate2
Причина та же, что и в случае 1.
Случай 5
setImmediate(() => {
console.log(1)
setTimeout(() => {
console.log(2)
}, 100)
setImmediate(() => {
console.log(3)
})
process.nextTick(() => {
console.log(4)
})
})
process.nextTick(() => {
console.log(5)
setTimeout(() => {
console.log(6)
}, 100)
setImmediate(() => {
console.log(7)
})
process.nextTick(() => {
console.log(8)
})
})
console.log(9)
Текущий результат ниже node v11:
9
5
8
1
7
4
3
6
2
На узле v11 и выше это:
9
5
8
1
4
7
3
6
2
Пожалуйста, проанализируйте причины самостоятельно.