Введение

В мире, где скорость передачи данных и использование ресурсов имеют первостепенное значение, понимание того, как использовать HTTP-заголовки Content-Range, может изменить правила игры, позволяя нам извлекать только те части данных, которые нам нужны, а не извлекать весь набор данных.

Что такое заголовки HTTP?

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

Заголовки HTTP похожи на те дополнительные биты информации на конверте, когда вы отправляете письмо. Когда ваш компьютер хочет связаться с сервером веб-сайта, он отправляет HTTP-запрос (что-то вроде вашего письма). В этом запросе есть главное, чего он хочет, например «дайте мне этот текстовый файл», но в заголовках также есть дополнительные инструкции. Эти заголовки могут говорить что-то вроде «Мне нужны эти данные в определенном формате» или «Мне нужна только часть данных, а не все».

Контент-диапазон

Заголовки Content-Range похожи на закладки для данных. Они сообщают вашему компьютеру, какую часть файла нужно захватить с сервера, что позволяет загружать только определенную часть, которую вы хотите, например, читать только главу из книги, а не целиком.

Учебник по кодированию

В этом руководстве мы создадим приложение server.ts для обслуживания файла test.txt и ответа на заголовок диапазона, отправленный нашим клиентским интерфейсом client.ts, для загрузки фрагментов нашего файла.

test.txt

hello
world

server.ts

import express, { Request, Response } from "express";
import fs from "fs";
import rangeParser, {Ranges} from "range-parser";

const app = express();
const PORT = 3000;
const filePath = "./test.txt";

app.get("/", (req: Request, res: Response) => {
    const { size } = fs.statSync(filePath);

    const range = req.headers["range"];
    if (range) {
        const ranges: Ranges = rangeParser(size, range) as Ranges;
        // Partial content
        const { start, end } = ranges[0];
        const chunkSize = end - start + 1;

        const fileStream = fs.createReadStream(filePath, { start, end });
        res.status(206);
        res.setHeader("Content-Range", `bytes ${start}-${end}/${size}`);
        res.setHeader("Content-Length", chunkSize);
        fileStream.pipe(res);
    } else {
        // Full content
        const fileStream = fs.createReadStream(filePath);
        res.setHeader("Content-Length", size);
        res.setHeader("Content-Type", "text/plain");
        fileStream.pipe(res);
    }
});

app.listen(PORT, () => {
    console.log(`Server is running at http://localhost:${PORT}`);
});

На нашем сервере у нас один маршрут в корне, мы пытаемся получить заголовок диапазона из клиентского запроса. Затем мы используем библиотеку rangeParser, чтобы получить начальный и конечный байты, прежде чем, наконец, создать поток файла от начального до конечного запрошенного байта для отправки обратно клиенту (вместе с заголовками для Content-Range и Content-Length).

клиент.ts

import axios, { AxiosResponse } from "axios";

const serverUrl = "http://localhost:3000";

async function sendRequest(startByte?: number, endByte?: number) {
    if (startByte === undefined || endByte === undefined || isNaN(startByte) || isNaN(endByte)) {
        // Send a request to the server to get the entire file
        const fullFileResponse: AxiosResponse = await axios.get(`${serverUrl}`);
        console.log("Full file content:");
        console.log(fullFileResponse.data);
    } else {
        // Send a request to the server to get a specific range of the file
        const rangeHeader = `bytes=${startByte}-${endByte}`;
        const partialFileResponse: AxiosResponse = await axios.get(`${serverUrl}`, {
            headers: { "range": rangeHeader },
        } as any);
        console.log(`Partial file content (${rangeHeader}):`);
        console.log(partialFileResponse.data);
    }
}

const args = process.argv.slice(2);
const startByte = parseInt(args[0]);
const endByte = parseInt(args[1]);

sendRequest(startByte, endByte);

Теперь в нашем клиенте мы принимаем два аргумента для начального и конечного байта. Получив их, мы создаем заголовок диапазона, где байты = начало-конец, который мы добавим в наши заголовки запроса. Наконец, мы отправляем этот запрос и выходим из частичного файлового ответа.

Заключение

Теперь, когда мы понимаем, как работают Content-Ranges, мы можем использовать их для ускорения наших сетевых запросов, чтобы загружать ТОЛЬКО те фрагменты файла, которые нам нужны. Для этого есть много вариантов использования: мы могли бы использовать это для чтения заголовков csv, чтобы убедиться, что это то, что мы хотим, прежде чем загружать все это, мы могли бы передавать аудио или видео вместо того, чтобы загружать все это заранее, мы могли бы возобновить прерванный загрузок, мы могли бы разбивать ответы API на страницы и т. д.