Comments 97
Э… и с каких пор понятие "архитектура" стало означать "структура директорий"?
если бы вы по возможности указали мне на ошибки
Сразу бросается в глаза использование regexp в парсе пути...
Так же радует, как вы определяете mime… Попробуйте нормальные инструменты для этого дела, только сначала нужно проверить, чтобы этот файл вообще существовал.
Ну и конечно же, стоит заметить, что для настоящих сайтов лучше использовать проверенные библиотеки, типа express. Потому что нужно поддерживать разные кодировки, выставлять заголовок content-length и много чего еще.
Для обучающих целей эта статья полезна, но до полноценного сервера нужно сделать много чего еще.
выставлять заголовок content-length и много чего еще.
Не обязательно, так как с HTTP/1.1 поддерживается 'Transfer-Encoding': 'chunked', который node.js выставляет автоматически.
А если будет еще nginx с включенным gzip, то Content-Length заранее и вовсе нет смысла высчитывать, так как nginx всё равно удалит его и поставит 'Transfer-Encoding': 'chunked'.
но до полноценного сервера нужно сделать много чего еще
По сути полноценный сервер это вот:
const http = require('http')
http.createServer((req, res) => {
let html = 'hello<br>world'
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
})
res.end(html)
}).listen(8080)
express и остальные это просто удобства, они принципиально ничего особенного не делают. Особенно если взять сторонний роутер и сторонний шаблонизатор. Даже миддлвары можно прикрутить из экспресса.
Поэтому в общем-то у автора уже полноценный сервер с нужными ему удобствами, и даже тот участок, где require в try-catch по сути не проблема, так как уже успешные require nodejs закэширует, и производительность не упадет.
В целом согласен с вашим комментарием, для быстрого/надежного развертывания экспресс или коа лучше подходят, это да.
Просто хорошо, что появляются статьи, которые расширяют понимание ноды дальше экспресса.
Помнится написал свою реализацию мидлверов, чтобы не грузить в проект express, ибо там было тупое api. Так что да, по сути модуль http — вот, что нам нужно.
А на проду ставить чистый nodejs без какого-нибудь nginx — просто глупо.
А неуспешные require нода тоже закеширует? Если нет, будет хохма — сервер, который отдает динамический контент быстрее чем статику...
Писал когда-то сервер для подобных вещей, пришлось явно ставить длину в хедере
Если речь про старую версию ios, где было HTTP/1.0, то только так.
Но в любом случае раздавать бинарники через ноду смысла нет, а если вы раздаете через nginx, то он сам для статики посчитает и выставит нужный размер content-length.
Ну, допустим, с content-length разобрались. Но остается еще немало других вопросов.
1) Что будет, если клиент пришлет POST-запрос с многогигабайтным body? Сервер загнется в попытках сохранить его в памяти.
2) Запрос может оборваться на середине. Надо обрабатывать эту ситуацию, чтобы избежать утечки открытых соединений.
Этот список можно продолжать долго. Свой "сервер без фреймворков" стоит писать лишь для того, чтобы наступить на эти грабли самому. Ну и ставить такой сервер наружу в интернет тоже опасно. Ботов-дудосеров, которые ходят по сети и автоматически ищут уязвимости, в интернете хватает.
2. node сам после истечения keep-alive прибьет этот запрос.
Полноценный сервер на ноде это те 5 строчек кода.
А перед сервером, на который предполагается что будет кто-то заходить извне, как ни крути лучше поставить nginx, который и body запрос ограничит в размере, и от примитивного ддоса защитит и т.д.
По сути ведь что? Не настроенный экспресс точно так же будет доставлять проблемы, да и пытаться все уровни защиты дублировать на ноде — это оверхед, всё равно nginx лучше с этим справится.
Ставить экспресс или что-то такое, это не то же самое что залить 2 файла на сервер и запустить их.
Да ничего особенного не будет. Будет какой-нибудь эксепшен вида «RangeError: Invalid string length»
А вот и нет. Приведенный в статье код
req.on('data', (data) => {
jsonString += data;
});
req.on('end', () => {
//...
});
Будет записывать все в jsonString, пока не кончится память.
/nodejs/server.js:11
jsonString += data;
^
RangeError: Invalid string length
Но то что ни в одном из этих случаев сам сервер не «загнется», с этим я согласна
Если бы закончилась память — было бы написано что закончилась память. А тут совсем другая ошибка.
Если бы закончилась память — было бы написано что закончилась память. А тут совсем другая ошибка.
Я и не говорила, что память закончилась, я сказала, что может до 2гб дело и не дошло.
А то, что память таким образом кончается намного быстрее — это особенность строк в js.
Всё верно, а старые строки будут по мере надобности подхватываться сборщиком мусора, так как на них больше нет ссылок.
Нет, не верно. Ссылка на старые строки остается, в этом и проблема
То, что выделяется новый участок памяти под результат конкатенации, это логично, но почему старый участок не должен освобождаться?
Дело в том, что строки не только иммутабельны, но еще и pooled. Поэтому при создании новой строки, старая остается в пуле, и остается там до тех пор пока вручную не нормализовать строку или не удалить весь объект строки
То есть сборщиком мусора сама она не очиститься
И на сколько я помню конкатенация работает быстрее чем Array#join.
UPD: пруф
В этом примере размер массива всего лишь 13 символов. Естественно конкатенация будет быстрее. Речь про огромные массивы строк. Там arr.join будет быстрее, меньше израсходует памяти и вообще будет работать, в отличии от строк, которые могут свалится с такой ошибкой как выше.
При чем я бы вообще рекомендовала вместо arr.push использовать Map.set — будет в 2 раза быстрее чем []
Интересная информация, спасибо за ответ. Никогда во внутренности реализаций js не погружался, но видимо стоит.
const teststring = "" //здесь было 512 utf-8 символов.
let string = "";
let multiplier = "2";
setInterval(() => {
if(string.length < 1024*1024*2) {
string += teststring;
console.log('1'); // для отслеживания
}
}, 10)
Следил встроенным в хром диспетчером задач за потреблением памяти и потреблением памяти javascript. Скрипт сделал всё верно, досчитал до 4096. За это время общая потребляемая память выросла, но даже не на 2 мегабайта. Было видно, как сборщик памяти раз в несколько секунд очищал память на 100-200кб.
Нет, любые строки по умолчанию pooled не являются, в пул попадают только строковые литералы. Результат конкатенации строковым литералом не является.
И на старую строку ссылку никто не удерживает, с этой стороны проблемы нет.
Вот производительность конкатенации в большом цикле и правда хромает, это общая особенность java, c#, javascript и еще кучи других языков.
Я вот прямо сейчас написал ради интереса такую штуку:
Скрипт сделал всё верно
И на старую строку ссылку никто не удерживает, с этой стороны проблемы нет.
Строки в js при конкатенации делают 2 вещи:
1. Создается новая строка
2. Новая строка складывается в пул
Если в коде встречается, что новая строка, которую прибавляют к старой уже есть в пуле, то новая строка не создается, просто используется та что уже в пуле. Поэтому если складывать одни и те же строки, то размер, можно сказать, увеличиваться не будет
Но если новая строка всегда новая и в пуле ее нет, начинается ад
Вот правильный пример:
'use strict';
function getRandomStr() {
let testString = ''
for (let i = 0; i < 512; i++) {
testString += '' + Math.random() * 100 | 0
}
return testString
}
let string = ""
setInterval(() => {
if (string.length < 1024 * 1024 * 2) {
string += getRandomStr()
console.log(string.length, process.memoryUsage())
}
}, 10)
В начале:
974 { rss: 20697088,
heapTotal: 5685248,
heapUsed: 3913832,
external: 9284 }
Спустя 25 секунд:
2097369 { rss: 104075264,
heapTotal: 87474176,
heapUsed: 65276264,
external: 9284 }
Память и не думает очищаться. Но помимо памяти есть еще и накладные расходы на всё это, которые в случае с 2гб файлом переходят разумный предел и выбрасывается исключение до расхода всей доступной памяти
'use strict';
const iter = 100000
function getRandomStr() {
let testString = []
for (let i = 0; i < 512; i++) {
testString.push(String.fromCharCode(Math.random() * 255 | 0))
}
return testString.join('')
}
let string = []
for (let i = 0; i < iter; i++) {
string.push(getRandomStr())
}
console.log((process.memoryUsage().rss / 1024 / 1024 | 0) + 'MB')
Результат:
>node sdfs.js
104MB
И результат довольно быстрый. А вариант с конкатенацией строк
'use strict';
const iter = 100000
function getRandomStr() {
let testString = ''
for (let i = 0; i < 512; i++) {
testString += String.fromCharCode(Math.random() * 255 | 0)
}
return testString
}
let string = ''
for (let i = 0; i < iter; i++) {
string += getRandomStr()
}
console.log((process.memoryUsage().rss / 1024 / 1024 | 0) + 'MB')
Работает вечность и всё равно в конце выдает:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
Спасибо, о конкатенации строк и памяти вообще нигде ничего нет.
Еще как есть. Вот например от одного из разработчиков ноды:
https://habrahabr.ru/post/283090/#comment_9641904
Обратите внимание: суммарный объем сгенерированных вами же промежуточных строк — гигабайт для внешнего цикла и еще сто метров во внутреннем. Ой, то есть два гигабайта и еще 200 метров, там же 16ти битная кодировка если я правильно помню.
Тот факт, что итоговое потребление памяти — всего 65 мегабайт, как раз и говорит о том, что строки ни в каком пуле не удерживаются.
Результат для arr.push:
>node sdfs.js
104MB
Работает очень быстро
Результат для конкатенации строк работает вечность и всё равно в конце выдает:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
Какое это имеет отношение к пулу строк?
Какое это имеет отношение к пулу строк?
Тот факт, что итоговое потребление памяти — всего 65 мегабайт, как раз и говорит о том, что строки ни в каком пуле не удерживаются.
Вернемся к тому первому примеру:
Длина получившейся строки — 2097369. Каждый символ допустим занимает 16 бит или 2 байта, то есть размер такой строки должен быть 4мб.
А реальный размер занимаемой памяти — 79мб, которая не очищается а только продолжает расти
Это всего лишь 4мб текста, в примере выше передается что-то около 3гб данных. Тут не память раньше кончится, а проблема с накладными расходами на поддержание таких строк начнется, что и произошло
Не знаю как вы умудрились прийти к выводу, что раз памяти в 10 раз (а чем дальше, тем больше) больше расходуется чем надо, то значит всё в порядке со строками
Вот еще пример:
'use strict';
function getRandomStr() {
let testString = ''
for (let i = 0; i < 512; i++) {
testString += '' + Math.random() * 100 | 0
}
return testString
}
const iter = 1000
let string = ''
let i = 0
const intr = setInterval(() => {
i++
string += getRandomStr()
if (i > iter) {
stopIntr()
}
}, 10)
function stopIntr() {
clearInterval(intr)
console.log('Занимает памяти: ' + (process.memoryUsage().rss / 1024 | 0) + 'KB')
console.log('Хотя должно занимать всего лишь: ' + (Buffer.from(string).length / 1024 | 0) + 'KB')
}
>node sdfs.js
Занимает памяти: 77268KB
Хотя должно занимать всего лишь: 951KB
Тоесть в 77 раз больше памяти расходует, чем требуется всего лишь на 1000 итераций. В примере с arr.push было 100.000 итераций, а памяти расходовалось всего «104MB»
Их можно бесконечно приводить, суть не изменится, строки иммутабельны и пулед, из-за этого идет большой оверхед по памяти и накладным расходам, которые не заметны если строки маленькие или оптимизатор смог их как-то оптимизировать, но становится очень заметны когда размеры переваливают за разумный предел.
И чтобы этого избежать можно использовать arr.push(newData) и arr.join('')
Это и был мой изначальный совет (слово в слово)
Если вы до сих пор хотите гнуть линию, что никаких удержаний нет, в пул не попадают — то я пожалуй пас.
Для того, чтобы опровергнуть гипотезу, достаточно одного контрпримера. Так что даже не буду смотреть очередной, а вернусь к исходному.
В процессе работы исходного примера совершается около 2 * 1024 итераций, каждая из которых добавляет в среднем 1024 символа к строке.
Суммарная длина промежуточных строк — это 1024 (2048 2047) / 2, то есть 2 миллиарда символов или 4 гигабайта.
Потребление памяти — 79 Мб, то есть в 50 раз меньше.
Следовательно, 98% использованной промежуточной памяти было успешно освобождено сборщиком мусора, что противоречит гипотезе о навечном застревании строк в строковом пуле.
Растет же потребляемая память в данном случае, скорее всего, не из-за утечек, а потому что нода таким образом пытается уменьшить число вызовов сборщика мусора.
Заметьте: нода ела всего лишь 800 мб! Если бы все строки хранились в пуле — зависимость потребляемой памяти от размера файла была бы квадратичной, а тут разница всего в 5-6 раз.
Время же выполнения тут ни при чем, факт что конкатенация строк в цикле работает медленно — общеизвестен и не имеет никакого отношения к пулу строк.
Растет же потребляемая память в данном случае, скорее всего, не из-за утечек,
Абы, да кабы. У вас догадки, а я опираюсь на слова одного из разработчиков ноды и собственные эксперименты.
К тому же это не утечки, а стандартная работа со строками
Я спорю с утверждением что любые строки интернируются в пул строк и становятся недоступны сборщику мусора, потому что это бред.Не было такого утверждения
что противоречит гипотезе о навечном застревании строк в строковом пуле.
Для того, чтобы опровергнуть гипотезу, достаточно одного контрпримера. Так что даже не буду смотреть очередной, а вернусь к исходному.
Ну если бы мы были в мире, где оптимизатор был совсем туп, то да. Что-то он умеет оптимизировать, и это заметно
И кто сказал про навечное застревание в пуле? Те строки, что сгенерированы в getRandomStr освобождаются, так как они не используются больше нигде, только их финальная копия сохранена в пуле
Когда строки свободны, а когда нет — это чуть поработав с собственным шаблонизатором вы быстро научитесь чувствовать, но это оверхед
Вот чтобы не гадать, и предлагают использовать arr.push (а лучше Map/Set), так как он даже с одинаковым расходом памяти работает быстрее на больших строках
И кто сказал про навечное застревание в пуле?
Вы:
Дело в том, что строки не только иммутабельны, но еще и pooled. Поэтому при создании новой строки, старая остается в пуле, и остается там до тех пор пока вручную не нормализовать строку или не удалить весь объект строки
Поскольку в приведенном вами коде нет никаких операций нормализации строки или явного удаления объекта — строки в пуле должны оставаться все время работы программы. Чего не наблюдается.
Ну если бы мы были в мире, где оптимизатор был совсем туп, то да. Что-то он умеет оптимизировать, и это заметно
Причем тут оптимизатор? Строки успешно собирает самый обычный сборщик мусора, как и любые другие объекты.
Вы:
И где же?
Поскольку в приведенном вами коде нет никаких операций нормализации строки или явного удаления объекта — строки в пуле должны оставаться все время работы программы. Чего не наблюдается.
Вот я так и думала, что мы будем не по существу, а придираться к словам.
Ну хорошо, вот вам 3 условие, когда строки освобождаются: Они больше нигде не используются, их пул больше не нужен, они стали частью другого пула. Оптимизатор сам умеет понимать когда это произошло, и делает это успешно
Причем тут оптимизатор? Строки успешно собирает самый обычный сборщик мусора, как и любые другие объекты.
Оптимизатор тут при том, что он понимает, когда можно срезать углы, и превращает функцию getRandomStr(), которая прогоняется не один раз, в нечто более быстро и оптимальное
Ситуацию "они больше нигде не используются" отслеживает не оптимизатор, а сборщик мусора.
Вашему терпению можно позавидовать
Еще ошибки, видимые сходу:
- Страница 404 отдается с кодом 200. Это так и задумано?
- Вместо хардкода в prePath стоило бы использовать __dirname или require.resolve
__dirname я почему-то пропустил мимо своего взгляда, большое спасибо за совет.
del
Я бы еще несколько сомнительных моментов добавил.
Если __dirname
по некоторым причинам не подходит, то различные базовые константы по хорошему надо хотя бы выносить в конфигурационный файл, или в опции к запуску, или еще куда. Так же увидел что пути через конкатенацию формируются, для этого есть path.join
, а иначе можно напороться на проблемы при запуске сервера под Windows, например.
Дублирующийся код при формирование Content-Type
и не правильные регулярки, не говоря уже про сам подход определения типа файлов.
/.mp3/.test('test.mp3') // true
/.mp3/.test('test.mp3.gz') // true
/.mp3/.test('testmp3') // true
/.mp3/.test('testmp3test') // true
/.mp3/.test('test.MP3') // false
Промисы в одном месте, обратные вызовы в другом в другом выглядят не хорошо. Достаточно просто работа с потоками или методы fs
оборачиваются в промисы, но для подобного примера может это и лишний код.
Про регулярные выражения — да, надо исправить, и чтобы она смотрела в конце страницы. Благодарю.
По поводу промисов и коллбеков — на мой взгляд, нет никаких проблем, если коллбек находится там же, где и то, что его вызывает, но в более сложных штуках с большим количеством действий можно было бы и переделать.
Большое спасибо за развёрнутый комментарий
Интересно очень.
Погуглите PillarJS и понятие BYO-фреймворк
BYO — Build you own, собери себе сам
https://pillarjs.github.io/
Берешь нужные компоненты и строишь.
Express, например, использует path-to-regexp и router из PillarJS (ветка 5.0 Express)
> Свой веб-сервер
> ни единого фреймворка
> const http = require('http');
> let server = new http.Server(
Что, правда?
Ваша статья — дерьмо. Hello world работающий по протоколу HTTP.
По поводу статьи — я не ставил себе целью написать апач. Это небольшая работа, и действительно своего рода «hello-world», который я бы с радостью прочёл, только взявшись за nodejs.
Класс. Старые добрые локальные инклюды — теперь и в node.js.
А идея отдавать статику нодой и отказаться от апача просто прекрасна.
Из комментариев, данных автором, и кода, который он написал в статье, могу резюмировать следующее: человек только только нашёл что такое nodejs, по некоторым соображениям даже только только притронулся к js, поэтому не стоит рассматривать статью как какой-то призыв к действию. Вообще не стоит рассматривать статью как какой-то ценный кусок ума. Просто hello world на публику.
Конечно лаканичнее, но мы же тут про ноду статью читаем. Как поднять вэб-сервер на стандартной библиотеке, уникальная в своём роде статья. А Вы тут со своим Go. Автор чётко выразился, что в своё время он много чего бы отдал за такую статью. Речь идет о годах 4 назад, за это время ничего подобного так и не появилось.
'use strict';
const http = require('http')
const routing = require('./routing')
http.Server(function (req, res) {
let jsonString = ''
res.setHeader('Content-Type', 'application/json; charset=utf-8')
req.on('data', (data) => {
jsonString += data
})
req.on('end', () => {
routing(req, res, jsonString)
})
}).listen(8000)
routing/index.js
'use strict';
const url = require('url')
const fs = require('fs')
const path = require('path')
function show404(req, res) {
const nopath = path.join(__dirname, 'nopage', 'index.html')
fs.readFile(nopath, (err, html) => {
if (!err) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('' + html)
}
else {
const text = "Something went wrong. Please contact webmaster";
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
res.end(text)
}
})
}
function loadStaticFile(pathname, req, res) {
const staticPath = path.join(__dirname, pathname)
if (pathname === '/favicon.ico') {
}
else if (/[.]mp3$/gi.test(pathname)) {
res.writeHead(200, { 'Content-Type': 'audio/mpeg' })
}
else if (/[.]css$/gi.test(pathname)) {
res.writeHead(200, { 'Content-Type': 'text/css' })
}
else if (/[.]js$/gi.test(pathname)) {
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' })
}
fs.createReadStream(staticPath).pipe(res)
}
function router(req, res, postData) {
const urlParsed = url.parse(req.url, true)
const pathname = urlParsed.pathname
if (/[.]/.test(pathname)) {
loadStaticFile(pathname, req, res)
return
}
let filepath = path.join(__dirname, 'dynamic', pathname, 'index.js')
fs.access(filepath, err => {
if (!err) {
const routeDestination = require(filepath)
routeDestination.promise(res, req, postData)
.then(result => {
res.writeHead(200)
res.end('' + result)
})
.catch(err => {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(`${err.name}: ${err.message}`)
})
}
else {
filepath = path.join(__dirname, 'static', pathname, 'index.html')
fs.readFile(filepath, 'utf-8', (err, html) => {
if (err) {
show404(req, res)
}
else {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('' + html)
}
})
}
})
}
module.exports = router
Думаю, от require избавится не получится, всё таки у запрашиваемых модулей могут быть зависимости. Только если все модули переписать соответствующим образом.
А вместо postData можно сделать:
req.on('end', () => {
req.body = jsonString
routing(req, res)
})
Чтобы не пробрасывать постоянно лишнюю переменную
А вот тут вы немного не правы:
Пара слов об API
Способ организации скриптов — личное дело каждого.
Представьте, что ваш проект разросся, у него появилась команда и огромная кодовая база, но — «способ организации скриптов — личное дело каждого»… В итоге ориентироваться в таком коде с личными делами — совершенно невозможно, невозможно и написать гайд по коду для вольных программистов гитхаба.
То есть на самом деле способ организации (скриптов или чего-то другого) — одна из первых вещей, о которых надо думать.
Хорошего дня!
express -someFlag -anotherFlag projectFolder
Автору можно было бы создать аналогичную структуру. Тогда она была бы унифицирована со стандартным решением, привычнее бы воспринималась и в случае необходимости упростила бы переход на фреймворк. Заодно можно было бы использовать package.json и запускать сервер стандартным
npm start
Свой веб-сервер на NodeJS, и ни единого фреймворка. Часть 1