Вступление
Статьи, исследующие новые технологии.
В этом посте я хочу вернуться к основам и построить простой веб-сервер с нуля с Nodejs. При этом мы рассмотрим структуру HTTP-запросов и ответов и получите введение в Узел Поток
API Отказ
Во-первых, мы быстро рассмотрим Встроенный узел http
. модуль Отказ После этого мы изучим общую структуру HTTP-запроса и ответа. Затем, используя встроенный узел Чистая
Модуль, мы создадим низкоуровневый TCP-сервер и попытаться из этого сделать удобный веб-сервер.
Само собой разумеется, что весь код в этой статье нет бизнеса в любом производственном приложении. Это предусмотрено только для образовательной стоимости.
С этим с пути, давайте начнем!
Быстрый взгляд на встроенный модуль узла узла
Nodejs поставляется с простым HTTP-сервером, встроенным встроенным. Этот сервер позволяет слушать произвольный порт и предоставлять функцию обратного вызова, которая будет вызываться на каждом входящем запросе.
Обратный вызов получит два аргумента: A Запросить объект и а Объект ответа Отказ Объект запроса будет заполнен полезными свойствами о запросе, а объект ответа будет использоваться для отправки ответа на клиента.
Пример «Hello World», использующий узел http
. Сервер:
const http = require('http'); const server = http.createServer(); server.on('request', (req, res) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); res.setHeader('Content-Type','text/plain'); res.end('Hello World!'); }); server.listen(3000);
Это начнет прослушивание веб-сервера в порту 3000. Когда вступит запрос, сообщение будет зарегистрировано в консоли с текущей датой, за которой следуют метод запроса и URL.
Эти данные исходят от параметра req
, который является объектом запроса, созданного узлом. Этот объект запроса также имеет свойство под названием .socket
, который является нижним уровнем TCP сокет.
Позже мы будем создавать TCP-сервер и получите прямой доступ к этому сокету для каждой новой связи, а затем, используя его, чтобы сделать наш веб-сервер.
В коде выше, тот факт, что у нас есть доступ к хорошему объекту с req.method
и req.url
Значит, кто-то, кроме США, проходил беда разбираться в тексте запроса и добиться его в хороший объект.
В следующем разделе мы посмотрим на структуру HTTP-запроса, чтобы получить подсказки к тому, как разбирать его.
Рассекать http.
Вот образец HTTP-запроса:
POST /posts/42/comments HTTP/1.1\r\n Host: www.my-api.com\r\n Accept: application/json\r\n Authorization: Bearer N2E5NTU2MzQ5MGQ4N2UzNjIxOTY2ZDU1M2YwNjA3OGFjYjgyMjU4NQ\r\n Accept-Encoding: gzip, deflate, br\r\n Content-Type: application/json\r\n Content-Length: 28\r\n User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0)\r\n \r\n {"text":"this is a comment"}
Несколько наблюдений:
- Каждая строка ограничена
\ r \ n
Отказ - Первая строка называется «строкой запроса». Он состоит из трех токенов, отделяемых космическими пространствами:
- Метод запроса,
Пост
Отказ Стандартные методы определены в Раздел 9 Спецификация HTTP/1.1 Отказ - Запрос URI,
/посты/42/комментарии
- Версия протокола,
Http/1.1.
- Метод запроса,
- Каждая следующая строка называется заголовком запроса. Он состоит из поля и его ценности, разделенного
:
Отказ Стандартные заголовки определены в Раздел 14 спецификации HTTP/1.1 Отказ - Есть только линия с
\ r \ n
Отказ Эта линия указывает конец заголовка запроса. Что-нибудь после этого является телом запроса. - Для этого запроса тело является документом JSON. Это соответствует
Content-Type
заголовок Документ JSON длиной 28 байтов, который соответствуетДлина содержимого
заголовок
И это образец HTTP-ответа:
HTTP/1.1 200 OK\r\n Server: nginx/1.9.4\r\n Date: Fri, 20 Apr 2017 16:19:42 GMT\r\n Content-Type: application/json\r\n Content-Length: 141\r\n \r\n { "id": "8fh924b42o", "text": "this is a comment", "createdAt": "2017-04-20T16:19:42.840Z", "updatedAt": "2017-04-20T16:19:42.840Z" }
Опять же, несколько наблюдений:
- Как запрос, каждая строка ограничена
\ r \ n
Отказ - Первая строка называется «строкой состояния». Он состоит из:
- Версия HTTP,
Http/1.1.1
- HTTP-код состояния ,
200.
- А РАЗНОВА ФРАСТИКИ ,
ОК
- Версия HTTP,
- Каждая строка после этого является заголовка отклика, с той же структурой в качестве заголовков запроса.
- Есть только линия с
\ r \ n
Отказ Это указывает на конец заголовка ответа и начало тела ответа. - Тело – 141-байтовый документ JSON. Это соответствует значениям в заголовках ответа.
В следующем разделе мы создадим TCP-сервер и сможете соблюдать HTTP-запрос, подходящий, кусок кусочком. Мы увидим, как прочитать и анализировать запрос, затем отправить ответ с помощью потоков.
Получение и анализ HTTP-запроса
Nodejs предоставляет встроенный Чистая
Модуль для создания потокового TCP-сервера. «Потоковое» относится к тому, что сервер может отправлять и получать данные с течением времени, используя Узел Поток
API Отказ
Где . http
. Сервер испускал Запрос
Событие с объектами запроса и ответа, сервер TCP выделяет Соединение
событие с объектом сокета. Объекты сокета Дуплексные потоки Отказ Короче говоря, это означает, что они могут быть прочитаны и написаны.
Если мы сделаем HTTP-запрос на наш TCP-сервер, чтение от розетки даст нам текст запроса. Есть два способа читать из читаемого потока: подписывая его данные
событие, или призывая его .read ()
метод. Давайте посмотрим на первый:
const net = require('net'); const server = net.createServer(); server.on('connection', handleConnection); server.listen(3000); function handleConnection(socket) { socket.on('data', (chunk) => { console.log('Received chunk:\n', chunk.toString()); }); socket.write('HTTP/1.1 200 OK\r\nServer: my-web-server\r\nContent-Length: 0\r\n\r\n'); }
Поместите этот код в файл под названием server.js
и выполнить его из вашей командной строки с помощью Node Server.js
Отказ В другом терминале используйте Curl, чтобы сделать простой запрос на ваш сервер:
curl -v localhost:3000/some/url
Вы должны увидеть многословную вывод из CURL, показывающий вам заголовки запроса и ответа. В узле терминала вы должны увидеть что-то вроде этого:
Received chunk: GET /some/url HTTP/1.1 Host: localhost:3000 User-Agent: curl/7.54.0 Accept: */*
Давайте сделаем Пост
Запрос с некоторыми данными:
curl -v -XPOST -d'hello=world' localhost:3000/some/url
Это должно дать вам следующий вывод в вашем узле-терминале:
Received chunk: POST /some/url HTTP/1.1 Host: localhost:3000 User-Agent: curl/7.54.0 Accept: */* Content-Length: 11 Content-Type: application/x-www-form-urlencoded hello=world
Если вы должны были отправить более длинное тело запроса, вы получите его в нескольких чанках. Но Веб-сервер не всегда должен обрабатывать тело запроса Отказ
Часто часть заголовка запроса достаточно, чтобы начать обработку запроса. Поскольку мы пишем универсальный веб-сервер, мы должны найти способ остановить получение данных запроса из сокета, как только мы достигнем строки \ r \ n \ r \ n
на входе.
Это легче сделать, используя .read ()
Способ розетки, поскольку мы можем контролировать, когда прекратить вытягивать данные из потока. Вот какой-то код для этого:
const net = require('net'); const server = net.createServer(); server.on('connection', handleConnection); server.listen(3000); function handleConnection(socket) { // Subscribe to the readable event once so we can start calling .read() socket.once('readable', function() { // Set up a buffer to hold the incoming data let reqBuffer = new Buffer(''); // Set up a temporary buffer to read in chunks let buf; let reqHeader; while(true) { // Read data from the socket buf = socket.read(); // Stop if there's no more data if (buf === null) break; // Concatenate existing request buffer with new data reqBuffer = Buffer.concat([reqBuffer, buf]); // Check if we've reached \r\n\r\n, indicating end of header let marker = reqBuffer.indexOf('\r\n\r\n') if (marker !== -1) { // If we reached \r\n\r\n, there could be data after it. Take note. let remaining = reqBuffer.slice(marker + 4); // The header is everything we read, up to and not including \r\n\r\n reqHeader = reqBuffer.slice(0, marker).toString(); // This pushes the extra data we read back to the socket's readable stream socket.unshift(remaining); break; } } console.log(`Request header:\n${reqHeader}`); // At this point, we've stopped reading from the socket and have the header as a string // If we wanted to read the whole request body, we would do this: reqBuffer = new Buffer(''); while((buf = socket.read()) !== null) { reqBuffer = Buffer.concat([reqBuffer, buf]); } let reqBody = reqBuffer.toString(); console.log(`Request body:\n${reqBody}`); // Send a generic response socket.end('HTTP/1.1 200 OK\r\nServer: my-custom-server\r\nContent-Length: 0\r\n\r\n'); }); }
Код немного дольше, потому что нам нужна какая-то логика, чтобы решить, когда прекратить чтение от потока. Это позволяет нам отделить заголовок из тела и позволить разработчику, который будет использовать наш веб-сервер, определить, что делать с телом, если что-то.
Ключевая часть здесь – Socket.unshift
Линия, которая «кладет» любые дополнительные данные, которые мы прочитаем в читаемый поток. Это позволит нам пройти этот сокет к нашему пользователю в случае, если они должны прочитать от него.
И вот полный код нашего основного веб-сервера, вкладывая все, что мы видели до сих пор. Наш сервер открывает функцию Createwebserver (RequestHandler)
Отказ Эта функция принимает обработчик формы (REQ, RES) => пустота
, как базовый веб-сервер узла. Комментарии в коде объясняют, что делает каждый шаг.
const net = require('net'); function createWebServer(requestHandler) { const server = net.createServer(); server.on('connection', handleConnection); function handleConnection(socket) { // Subscribe to the readable event once so we can start calling .read() socket.once('readable', function() { // Set up a buffer to hold the incoming data let reqBuffer = new Buffer(''); // Set up a temporary buffer to read in chunks let buf; let reqHeader; while(true) { // Read data from the socket buf = socket.read(); // Stop if there's no more data if (buf === null) break; // Concatenate existing request buffer with new data reqBuffer = Buffer.concat([reqBuffer, buf]); // Check if we've reached \r\n\r\n, indicating end of header let marker = reqBuffer.indexOf('\r\n\r\n') if (marker !== -1) { // If we reached \r\n\r\n, there could be data after it. Take note. let remaining = reqBuffer.slice(marker + 4); // The header is everything we read, up to and not including \r\n\r\n reqHeader = reqBuffer.slice(0, marker).toString(); // This pushes the extra data we read back to the socket's readable stream socket.unshift(remaining); break; } } /* Request-related business */ // Start parsing the header const reqHeaders = reqHeader.split('\r\n'); // First line is special const reqLine = reqHeaders.shift().split(' '); // Further lines are one header per line, build an object out of it. const headers = reqHeaders.reduce((acc, currentHeader) => { const [key, value] = currentHeader.split(':'); return { ...acc, [key.trim().toLowerCase()]: value.trim() }; }, {}); // This object will be sent to the handleRequest callback. const request = { method: reqLine[0], url: reqLine[1], httpVersion: reqLine[2].split('/')[1], headers, // The user of this web server can directly read from the socket to get the request body socket }; /* Response-related business */ // Initial values let status = 200, statusText = 'OK', headersSent = false, isChunked = false; const responseHeaders = { server: 'my-custom-server' }; function setHeader(key, value) { responseHeaders[key.toLowerCase()] = value; } function sendHeaders() { // Only do this once :) if (!headersSent) { headersSent = true; // Add the date header setHeader('date', new Date().toGMTString()); // Send the status line socket.write(`HTTP/1.1 ${status} ${statusText}\r\n`); // Send each following header Object.keys(responseHeaders).forEach(headerKey => { socket.write(`${headerKey}: ${responseHeaders[headerKey]}\r\n`); }); // Add the final \r\n that delimits the response headers from body socket.write('\r\n'); } } const response = { write(chunk) { if (!headersSent) { // If there's no content-length header, then specify Transfer-Encoding chunked if (!responseHeaders['content-length']) { isChunked = true; setHeader('transfer-encoding', 'chunked'); } sendHeaders(); } if (isChunked) { const size = chunk.length.toString(16); socket.write(`${size}\r\n`); socket.write(chunk); socket.write('\r\n'); } else { socket.write(chunk); } }, end(chunk) { if (!headersSent) { // We know the full length of the response, let's set it if (!responseHeaders['content-length']) { // Assume that chunk is a buffer, not a string! setHeader('content-length', chunk ? chunk.length : 0); } sendHeaders(); } if (isChunked) { if (chunk) { const size = (chunk.length).toString(16); socket.write(`${size}\r\n`); socket.write(chunk); socket.write('\r\n'); } socket.end('0\r\n\r\n'); } else { socket.end(chunk); } }, setHeader, setStatus(newStatus, newStatusText) { status = newStatus, statusText = newStatusText }, // Convenience method to send JSON through server json(data) { if (headersSent) { throw new Error('Headers sent, cannot proceed to send JSON'); } const json = new Buffer(JSON.stringify(data)); setHeader('content-type', 'application/json; charset=utf-8'); setHeader('content-length', json.length); sendHeaders(); socket.end(json); } }; // Send the request to the handler! requestHandler(request, response); }); } return { listen: (port) => server.listen(port) }; } const webServer = createWebServer((req, res) => { // This is the as our original code with the http module :) console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); res.setHeader('Content-Type','text/plain'); res.end('Hello World!'); }); webServer.listen(3000);
Как упоминалось в начале этого поста, этот серверный код не имеет бизнеса в производстве. Его единственная цель – узнать больше о потоках и буферах.
Большая часть кода в нем должна быть объяснительным. Мы анализируем запрос и отправляем ответ, используя правила, которые мы узнали в предыдущем разделе.
Единственный новый бит – Переводное кодирование: Colked
, что необходимо, когда мы заранее не знаем длину ответа. Вы можете Узнайте больше о кодировании Colked Transfer на Wikipedia Отказ
Заключение
Используя некоторые строевые строительные блоки, такие как Чистая
, Поток
и Буфер
, мы смогли создать элементарный HTTP-сервер на основе TCP-сервера и некоторой логики разборки. В процессе мы узнали о том, как работают HTTP-запросы и ответы, а также основы читаемых и писшечных потоков и буферов.