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

Создание HTTP -сервера с нуля: Понимание запроса и ответа

О, привет! Я рад, что вы добрались до этого второго поста «Создайте систему: HTTP Server» … Tagged с JavaScript, Deno, Node, http.

О, привет!

Я рад, что вы попали во второй пост серии «Построить систему: HTTP Server». Этот пост посвящен декодированию HTTP -запросов и кодированию ответа. Я также предложу надежный способ проверить наш код для более устойчивого проекта. Если вы еще не читали первый пост сериала, я думаю, вы можете. Просто нажмите здесь, чтобы прочитать его. Я терпеливо жду вашего возвращения.

Эта статья представляет собой стенограмму видео на YouTube, которое я сделал.

Хорошо, теперь, когда я знаю, что мы все на одной странице, давайте напишем немного кода. Для этого проекта я буду использовать JavaScript и Deno, но концепции не меняются, независимо от того, какой язык или время выполнения вы используете. Кроме того, последний отказ от ответственности: этот проект первой целью является обучение его никоим образом не будет полным или самым исполнительным! Я расскажу конкретно о улучшениях, которые мы можем принести, чтобы сделать его более эффективным, и я буду проходить различную итерацию с учетом этого. В конце проекта, если есть часть спасения, я заменю основные части. Все это, чтобы сказать, просто наслаждайтесь поездкой.

Первое, что мне нужно сделать, это объявить о прослушивании в порту. Входящее соединение будет представлено читаемым/записным ресурсом. Во -первых, мне нужно будет прочитать из ресурса определенное количество байтов. Для этого примера я буду читать вокруг КБ. Переменная xs это Uint8array . Я уже написал статью об этом Но в течение длительной истории, напечатанный массив – это массив Это может содержать только определенное количество битов на предмет. В этом случае нам нужно 8 бит (или один байт), потому что вам нужно 8 бит Кодировать один символ UTF-8.

🐙 Вы найдете код для этого поста здесь: https://github.com/i-y-land/http/tree/episode/02

В качестве удобства я расшифлю байты на строку и войдет в систему в консоли. Наконец, я кодирую ответ и напишу его в ресурс.

// scratch.js
for await (const connection of Deno.listen({ port: 8080 })) {
  const xs = new Uint8Array(1024);
  await Deno.read(connection.rid, xs);

  console.log(new TextDecoder().decode(xs));

  await Deno.write(
    connection.rid,
    new TextEncoder().encode(
      `HTTP/1.1 200 OK\r\nContent-Length: 12\r\nContent-Type: text/plain\r\n\r\nHello, World`
    )
  );
}

Теперь я запускаю код:

deno run --allow-net="0.0.0.0:8080" scratch.js

На другом сеансе терминала я могу использовать Curl Чтобы отправить HTTP -запрос.

curl localhost:8080

На терминале сервера мы можем увидеть запрос, а на терминале клиента мы можем увидеть тело ответа: «Привет, мир»

Большой!

Чтобы начать это на правой ноге, я переработаю код в функцию с именем служить В файле называется Server.js Анкет Эта функция займет слушателя и функцию, которая принимает Uint8array и возвращает обещание о Uint8array !

// library/server.js
export const serve = async (listener, f) => {
  for await (const connection of listener) {
    const xs = new Uint8Array(1024);
    const n = await Deno.read(connection.rid, xs);

    const ys = await f(xs.subarray(0, n));
    await Deno.write(connection.rid, ys);
  }
};

Обратите внимание, что Читать Функция возвращает количество байта, который был прочитан. Итак, мы можем использовать Subarray Метод для прохождения Лензу на соответствующей последовательности функции.

// cli.js
import { serve } from "./server.js";

const $decoder = new TextDecoder();
const decode = $decoder.decode.bind($decoder);
const $encoder = new TextEncoder();
const encode = $encoder.encode.bind($encoder);

if (import.meta.main) {
  const port = Number(Deno.args[0]) || 8080;
  serve(
    Deno.listen({ port }),
    (xs) => {
      const request = decode(xs);
      const [requestLine, ...lines] = request.split("\r\n");
      const [method, path] = requestLine.split(" ");
      const separatorIndex = lines.findIndex((l) => l === "");
      const headers = lines
        .slice(0, separatorIndex)
        .map((l) => l.split(": "))
        .reduce(
          (hs, [key, value]) =>
            Object.defineProperty(
              hs,
              key.toLowerCase(),
              { enumerable: true, value, writable: false },
            ),
          {},
        );

      if (method === "GET" && path === "/") {
        if (
          headers.accept.includes("*/*") ||
          headers.accept.includes("plain/text")
        ) {
          return encode(
            `HTTP/1.1 200 OK\r\nContent-Length: 12\r\nContent-Type: text/plain\r\n\r\nHello, World`,
          );
        } else {
          return encode(
            `HTTP/1.1 204 No Content\r\nContent-Length: 0\r\n\r\n`,
          );
        }
      }

      return encode(
        `HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n`,
      );
    },
  )
    .catch((e) => console.error(e));
}

Теперь, когда у меня есть способ проанализировать заголовки, я думаю, что это хорошая возможность определять все это и написать новую функцию утилиты и соответствующие тесты.

// library/utilities.js

export const parseRequest = (xs) => {
  const request = decode(xs);
  const [h, body] = request.split("\r\n\r\n");
  const [requestLine, ...ls] = h.split("\r\n");
  const [method, path] = requestLine.split(" ");
  const headers = ls
    .map((l) => l.split(": "))
    .reduce(
      (hs, [key, value]) =>
        Object.defineProperty(
          hs,
          key.toLowerCase(),
          { enumerable: true, value, writable: false },
        ),
      {},
    );

  return { method, path, headers, body };
};
// library/utilities_test.js

Deno.test(
  "parseRequest",
  () => {
    const request = parseRequest(
      encode(`GET / HTTP/1.1\r\nHost: localhost:8080\r\nAccept: */*\r\n\r\n`),
    );

    assertEquals(request.method, "GET");
    assertEquals(request.path, "/");
    assertEquals(request.headers.host, "localhost:8080");
    assertEquals(request.headers.accept, "*/*");
  },
);

Deno.test(
  "parseRequest: with body",
  () => {
    const request = parseRequest(
      encode(
        `POST /users HTTP/1.1\r\nHost: localhost:8080\r\nAccept: */*\r\n\r\n{"fullName":"John Doe"}`,
      ),
    );

    assertEquals(request.method, "POST");
    assertEquals(request.path, "/users");
    assertEquals(request.headers.host, "localhost:8080");
    assertEquals(request.headers.accept, "*/*");
    assertEquals(request.body, `{"fullName":"John Doe"}`);
  },
);

Теперь, когда у меня есть Parserequest Функция, логически мне нужна новая функция для строительства ответа …

// library/utilities.js

import { statusCodes } from "./status-codes.js";

export const normalizeHeaderKey = (key) =>
  key.replaceAll(/(?<=^|-)[a-z]/g, (x) => x.toUpperCase());

export const stringifyHeaders = (headers = {}) =>
  Object.entries(headers)
    .reduce(
      (hs, [key, value]) => `${hs}\r\n${normalizeHeaderKey(key)}: ${value}`,
      "",
    );

export const stringifyResponse = (response) =>
  `HTTP/1.1 ${statusCodes[response.statusCode]}${
    stringifyHeaders(response.headers)
  }\r\n\r\n${response.body || ""}`;
// library/utilities_test.js

Deno.test(
  "normalizeHeaderKey",
  () => {
    assertEquals(normalizeHeaderKey("link"), "Link");
    assertEquals(normalizeHeaderKey("Location"), "Location");
    assertEquals(normalizeHeaderKey("content-type"), "Content-Type");
    assertEquals(normalizeHeaderKey("cache-Control"), "Cache-Control");
  },
);

Deno.test(
  "stringifyResponse",
  () => {
    const body = JSON.stringify({ fullName: "John Doe" });
    const response = {
      body,
      headers: {
        ["content-type"]: "application/json",
        ["content-length"]: body.length,
      },
      statusCode: 200,
    };
    const r = stringifyResponse(response);

    assertEquals(
      r,
      `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 23\r\n\r\n{"fullName":"John Doe"}`,
    );
  },
);

Итак, теперь у нас есть все, что нам нужно, чтобы репроектировать нашу функцию обработчика и сделать ее более кратким и декларативным.

import { serve } from "./library/server.js";
import {
  encode,
  parseRequest,
  stringifyResponse,
} from "./library/utilities.js";

if (import.meta.main) {
  const port = Number(Deno.args[0]) || 8080;
  serve(
    Deno.listen({ port }),
    (xs) => {
      const request = parseRequest(xs);

      if (request.method === "GET" && request.path === "/") {
        if (
          request.headers.accept.includes("*/*") ||
          request.headers.accept.includes("plain/text")
        ) {
          return Promise.resolve(
            encode(
              stringifyResponse({
                body: "Hello, World",
                headers: {
                  "content-length": 12,
                  "content-type": "text/plain",
                },
                statusCode: 200,
              }),
            ),
          );
        } else {
          return Promise.resolve(
            encode(stringifyResponse({ statusCode: 204 })),
          );
        }
      }

      return Promise.resolve(
        encode(
          stringifyResponse({
            headers: {
              "content-length": 0,
            },
            statusCode: 404,
          }),
        ),
      );
    },
  )
    .catch((e) => console.error(e));
}

Таким образом, мы можем эффективно справиться с любым простым запросом. Чтобы завершить это и подготовить проект к будущей итерации, Я добавлю тест для служить функция Очевидно, эта функция невозможно сохранить чистую и проверять без Сложные интеграционные тесты – которые я держу позже. Фактическое соединение немного волнозно Поэтому я подумал, что смогу издеваться над ним, используя файл в качестве ресурса, так как файлы читаемый/ворчаемый. Первое, что я сделал, – это написать функцию, чтобы факторизировать асинхровый итератор и намеренно заставить ее сломаться после первого итерация. После этого я создаю файл с разрешениями на чтение/запись. С этим я могу написать HTTP -запрос, затем переместить курсор Вернемся к началу файла для служить функция для чтения обратно. В рамках функции обработчика я делаю немного Утверждения по запросу о саке в области здравомыслия, затем промойте контент и перенесите курсор обратно в начало до Написание ответа. Наконец, я могу переместить курсор обратно в начало в последний раз, чтобы прочитать ответ, сделать одно последнее утверждение, затем Очистка, чтобы завершить тест.

// library/server_test.js

import { assertEquals } from "https://deno.land/std@0.97.0/testing/asserts.ts";

import { decode, encode, parseRequest } from "./utilities.js";
import { serve } from "./server.js";

const factorizeConnectionMock = (p) => {
  let i = 0;

  return {
    p,
    rid: p.rid,
    [Symbol.asyncIterator]() {
      return {
        next() {
          if (i > 0) {
            return Promise.resolve({ done: true });
          }
          i++;
          return Promise.resolve({ value: p, done: false });
        },
        values: null,
      };
    },
  };
};

Deno.test(
  "serve",
  async () => {
    const r = await Deno.open(`${Deno.cwd()}/.buffer`, {
      create: true,
      read: true,
      write: true,
    });

    const xs = encode(`GET /users/1 HTTP/1.1\r\nAccept: */*\r\n\r\n`);

    await Deno.write(r.rid, xs);

    await Deno.seek(r.rid, 0, Deno.SeekMode.Start);

    const connectionMock = await factorizeConnectionMock(r);

    await serve(
      connectionMock,
      async (ys) => {
        const request = parseRequest(ys);

        assertEquals(
          request.method,
          "GET",
          `The request method was expected to be \`GET\`. Got \`${request.method}\``,
        );
        assertEquals(
          request.path,
          "/users/1",
          `The request path was expected to be \`/users/1\`. Got \`${request.path}\``,
        );
        assertEquals(
          request.headers.accept,
          "*/*",
        );

        await Deno.ftruncate(r.rid, 0);
        await Deno.seek(r.rid, 0, Deno.SeekMode.Start);

        const body = JSON.stringify({ "fullName": "John Doe" });

        return encode(
          `HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: ${body.length}\r\n\r\n${body}`,
        );
      },
    );

    await Deno.seek(r.rid, 0, Deno.SeekMode.Start);

    const zs = new Uint8Array(1024);
    const n = await Deno.read(r.rid, zs);

    assertEquals(
      decode(zs.subarray(0, n)),
      `HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: 23\r\n\r\n{"fullName":"John Doe"}`,
    );

    Deno.remove(`${Deno.cwd()}/.buffer`);
    Deno.close(r.rid);
  },
);

На данный момент у нас есть хорошая база для работы. К сожалению, наш сервер немного ограничен, например, если запрос больше, чем KB, нам не хватает части сообщения, это означает, что не загружать или загружать файлы среднего размера. Это то, что я планирую прикрыть в следующем посте. Это заставит нас быть немного более знакомыми с манипулированием бинарными байтами.

В любом случае, если эта статья была для вас полезна, нажмите кнопку «Напоминание», оставьте комментарий, чтобы сообщить мне или лучше всего, следуйте, если вы еще этого не сделали!

Хорошо, пока сейчас …

Оригинал: “https://dev.to/sebastienfilion/building-a-http-server-from-scratch-understanding-request-response-4120”