Рубрики
Без рубрики

Позвольте код веб-сервера с нуля с потоками Nodejs!

Узнайте о потоках Nodejs, создавая элементарный HTTP-сервер.

Автор оригинала: Ziad Saab.

Вступление

Статьи, исследующие новые технологии.

В этом посте я хочу вернуться к основам и построить простой веб-сервер с нуля с 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 Отказ
  • Первая строка называется «строкой состояния». Он состоит из:
  • Каждая строка после этого является заголовка отклика, с той же структурой в качестве заголовков запроса.
  • Есть только линия с \ 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-запросы и ответы, а также основы читаемых и писшечных потоков и буферов.