Какие есть варианты передачи больших файлов по HTTP?

внешний интерфейс HTTP JavaScript
Какие есть варианты передачи больших файлов по HTTP?

Эта статья приняла участиеПожалуйста, проверьте|У вас есть возможность подать заявку на бесплатные подарки в Nuggets.Мероприятие, приходите и оставляйте сообщение для участия в мероприятии!

существуетКак реализовать одновременную загрузку больших файлов в JavaScript?иКак реализовать параллельную загрузку больших файлов в JavaScript?В этих двух статьях брат Абао рассказал, как использоватьasync-poolЭта библиотека для оптимизации функции передачи больших файлов. В этой статье Brother Abao представит несколько схем HTTP-передачи больших файлов. Однако, прежде чем вводить конкретные решения, мы сначала используемNode.jsизfsмодуль для создания «большого» файла.

const fs = require("fs");

const writeStream = fs.createWriteStream(__dirname + "/big-file.txt");
for (let i = 0; i <= 1e5; i++) {
  writeStream.write(`${i} 我是阿宝哥,欢迎关注全栈修仙之路\n`, "utf8");
}

writeStream.end();

После успешного выполнения приведенного выше кода создается файл размером5.5 MB, который послужит «начинкой» для следующей схемы. После того, как приготовления закончены, введем первую схему -Сжатие данных.

1. Сжатие данных

При использовании HTTP для передачи больших файлов мы можем рассмотреть возможность сжатия больших файлов. Обычно, когда браузер отправляет запрос, он будет содержатьacceptиaccept-*Информация заголовка запроса используется для сообщения серверу типов файлов, поддерживаемых текущим браузером, списка поддерживаемых форматов сжатия и поддерживаемых языков.

accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9

Степень сжатия gzip обычно может превышать 60%, а алгоритм br специально разработан для HTML, а эффективность и производительность сжатия даже лучше, чем у gzip, что позволяет увеличить плотность сжатия еще на 20%.

в приведенном выше заголовке HTTP-запросаAccept-EncodingПоле используется, чтобы сообщить серверу метод кодирования контента (обычно какой-то алгоритм сжатия), который может понять клиент. В ходе согласования содержимого сервер выберет метод, поддерживаемый клиентом, и передаст заголовок ответа.Content-Encodingуведомить клиента о выборе.

cache-control: max-age=2592000
content-encoding: gzip
content-type: application/x-javascript

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

пониматьAccept-EncodingиContent-Encodingполе, давайте проверим, что оно не включеноgzipи включиgzipЭффект.

1.1 gzip не включен

const fs = require("fs");
const http = require("http");
const util = require("util");
const readFile = util.promisify(fs.readFile);

const server = http.createServer(async (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/plain;charset=utf-8",
  });
  const buffer = await readFile(__dirname + "/big-file.txt");
  res.write(buffer);
  res.end();
});

server.listen(3000, () => {
  console.log("app starting at port 3000");
});

1.2 Включить gzip

const fs = require("fs");
const zlib = require("zlib");
const http = require("http");
const util = require("util");
const readFile = util.promisify(fs.readFile);
const gzip = util.promisify(zlib.gzip);

const server = http.createServer(async (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/plain;charset=utf-8",
    "Content-Encoding": "gzip"
  });
  const buffer = await readFile(__dirname + "/big-file.txt");
  const gzipData = await gzip(buffer);
  res.write(gzipData);
  res.end();
});

server.listen(3000, () => {
  console.log("app starting at port 3000");
});

Глядя на два изображения выше, мы можем интуитивно почувствовать, когда передача5.5 MBизbig-file.txtфайл, если открытgzipПосле сжатия файл сжимается в256 kB. Это значительно ускоряет передачу файлов. В реальном рабочем сценарии мы можем использоватьnginxилиkoa-staticвключатьgzipФункция сжатия. Далее, давайте представим другое решение -Кодирование групповой передачи.

Во-вторых, блочное кодирование передачи

Поблочное кодирование передачи в основном используется в сценариях, когда необходимо передать большой объем данных, но длина ответа не может быть получена до тех пор, пока запрос не будет обработан. Например, когда вам нужно сгенерировать большую HTML-таблицу с данными, полученными из запроса в базе данных, или когда вам нужно передать большое количество изображений.

Чтобы использовать фрагментированное кодирование передачи, вам необходимо настроить заголовок ответаTransfer-Encodingполе и установите его значение равнымchunkedилиgzip, chunked:

Transfer-Encoding: chunked
Transfer-Encoding: gzip, chunked

заголовок ответаTransfer-EncodingЗначение поляchunked, что указывает на то, что данные отправляются в виде серии фрагментов. должен быть в курсеTransfer-EncodingиContent-LengthЭти два поля являются взаимоисключающими, то есть эти два поля не могут появляться в ответном пакете одновременно. Давайте посмотрим на правила кодирования для блочной передачи:

  • Каждый блок состоит из двух частей: длины блока и блока данных;
  • Длина блока выражается в виде шестнадцатеричного числа, где\r\nконец;
  • Блок данных следует сразу за длиной блока, также используя\r\nконец, но данные не содержат\r\n;
  • Завершающий блок — это обычный блок, обозначающий конец блока. Отличие в том, что его длина равна 0, т.е.0\r\n\r\n.

Поняв знание кодирования блочной передачи, брат Абао будет использоватьbig-file.txtПервые 100 строк данных в файле, чтобы продемонстрировать, как реализовано кодирование передачи по частям.

2.1 Блокировка данных

const buffer = fs.readFileSync(__dirname + "/big-file.txt");
const lines = buffer.toString("utf-8").split("\n");
const chunks = chunk(lines, 10);

function chunk(arr, len) {
  let chunks = [],
    i = 0,
    n = arr.length;
  while (i < n) {
    chunks.push(arr.slice(i, (i += len)));
  }
  return chunks;
}

2.2 Блокировка передачи

// http-chunk-server.js
const fs = require("fs");
const http = require("http");

// 省略数据分块代码
http
  .createServer(async function (req, res) {
    res.writeHead(200, {
      "Content-Type": "text/plain;charset=utf-8",
      "Transfer-Encoding": "chunked",
      "Access-Control-Allow-Origin": "*",
    });
    for (let index = 0; index < chunks.length; index++) {
      setTimeout(() => {
        let content = chunks[index].join("&");
        res.write(`${content.length.toString(16)}\r\n${content}\r\n`);
      }, index * 1000);
    }
    setTimeout(() => {
      res.end();
    }, chunks.length * 1000);
  })
  .listen(3000, () => {
    console.log("app starting at port 3000");
  });

использоватьnode http-chunk-server.jsПосле того, как команда запустит сервер, получите доступ к нему в браузереhttp://localhost:3000/адрес, вы увидите следующий вывод:

Вышеприведенная картинка представляет собой содержимое, возвращаемое первым блоком данных.После того, как все блоки данных будут переданы, сервер вернет блок завершения, то есть отправит его клиенту.0\r\n\r\n. Кроме того, для возвращаемых фрагментированных данных мы также можем использоватьfetchОбъект ответа в API для чтения возвращенного блока данных в виде потока, то есть черезresponse.body.getReader()чтобы создать читатель, а затем вызватьreader.read()способ чтения данных.

2.3 Потоковая передача

На самом деле при использованииNode.jsПри возврате больших файлов клиенту лучше всего использовать форму потока для возврата файлового потока, что позволяет не занимать слишком много памяти при обработке больших файлов. Конкретная реализация выглядит следующим образом:

const fs = require("fs");
const zlib = require("zlib");
const http = require("http");

http
  .createServer((req, res) => {
    res.writeHead(200, {
      "Content-Type": "text/plain;charset=utf-8",
      "Content-Encoding": "gzip",
    });
    fs.createReadStream(__dirname + "/big-file.txt")
      .setEncoding("utf-8")
      .pipe(zlib.createGzip())
      .pipe(res);
  })
  .listen(3000, () => {
    console.log("app starting at port 3000");
  });

Заголовки ответа HTTP при использовании потока для возврата данных файлаTransfer-EncodingЗначение поляchunked, что указывает на то, что данные отправляются в виде серии фрагментов.

Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/plain;charset=utf-8
Date: Sun, 06 Jun 2021 01:02:09 GMT
Transfer-Encoding: chunked

Если вы заинтересованы в Node.js Stream, вы можете прочитать Abaoge на Github.semlinker/node-deep — углубленное изучение основ Node.js StreamЭта статья.

адрес проекта:GitHub.com/Semelinker/Вы…

3. Запрос объема

Запросы области протокола HTTP позволяют серверу отправлять клиенту только часть сообщения HTTP. Запросы диапазона полезны при передаче больших медиафайлов или в сочетании с функцией возобновления загрузки файла. если присутствует в ответеAccept-Rangesзаголовок (и его значение не "none"), то сервер поддерживает запросы диапазона.

В заголовке Range можно одновременно запросить несколько частей, и сервер вернет их в виде составного файла. Если сервер возвращает ответ диапазона, вам нужно использовать206 Partial Contentкод состояния. Если запрошенный диапазон недействителен, сервер вернет416 Range Not SatisfiableКод состояния, указывающий на ошибку клиента. Серверу разрешено игнорировать заголовок Range и, таким образом, возвращать весь файл с кодом состояния 200 .

3.1 Синтаксис диапазона

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
  • unit: единица измерения, используемая для запроса диапазона, обычно байты.
  • <range-start>: целое число, представляющее начальное значение диапазона в указанных единицах измерения.
  • <range-end>: целое число, представляющее конечное значение диапазона в указанных единицах измерения.Это значение является необязательным и, если его нет, указывает, что область действия распространяется до конца документа.

пониматьRangeПосле синтаксиса давайте взглянем на реальный пример использования:

3.1.1 Единая область применения
$ curl http://i.imgur.com/z4d4kWk.jpg -i -H "Range: bytes=0-1023"
3.1.2 Несколько областей применения
$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"

3.2 Пример запроса диапазона

3.2.1 Код сервера
// http/range/koa-range-server.js
const Koa = require("koa");
const cors = require("@koa/cors");
const serve = require("koa-static");
const range = require('koa-range');

const app = new Koa();

// 注册中间件
app.use(cors()); // 注册CORS中间件
app.use(range); // 注册范围请求中间件
app.use(serve(".")); // 注册静态资源中间件

app.listen(3000, () => {
  console.log("app starting at port 3000");
});
3.2.2 Клиентский код
<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>大文件范围请求示例(阿宝哥)</title>
  </head>
  <body>
    <h3>大文件范围请求示例(阿宝哥)</h3>
    <div id="msgList"></div>
    <script>
      const msgList = document.querySelector("#msgList");
      function getBinaryContent(url, start, end, responseType = "arraybuffer") {
        return new Promise((resolve, reject) => {
          try {
            let xhr = new XMLHttpRequest();
            xhr.open("GET", url, true);
            xhr.setRequestHeader("range", `bytes=${start}-${end}`);
            xhr.responseType = responseType;
            xhr.onload = function () {
              resolve(xhr.response);
            };
            xhr.send();
          } catch (err) {
            reject(new Error(err));
          }
        });
      }

      getBinaryContent(
        "http://localhost:3000/big-file.txt",
        0, 100, "text"
      ).then((text) => {
        msgList.append(`${text}`);
      });
    </script>
  </body>
</html>

использоватьnode koa-range-server.jsПосле того, как команда запустит сервер, получите доступ к нему в браузереhttp://localhost:3000/index.htmlадрес, вы увидите следующий вывод:

Соответствующие заголовки HTTP-запросов и заголовки ответов (которые содержат только часть информации заголовка), соответствующие этому примеру, следующие:

3.2.3 Заголовки HTTP-запроса
GET /big-file.txt HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Referer: http://localhost:3000/index.html
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,id;q=0.7
Range: bytes=0-100
3.2.4 Заголовки ответа HTTP
HTTP/1.1 206 Partial Content
Vary: Origin
Accept-Ranges: bytes
Last-Modified: Sun, 06 Jun 2021 01:40:19 GMT
Cache-Control: max-age=0
Content-Type: text/plain; charset=utf-8
Date: Sun, 06 Jun 2021 03:01:01 GMT
Connection: keep-alive
Content-Range: bytes 0-100/5243
Content-Length: 101

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

4. Резюме

В этой статье Брат Абао представляет 3 схемы для HTTP-передачи больших файлов.Я надеюсь, что понимание этих знаний поможет вам в вашей будущей работе. В реальном использовании следует обратить внимание наTransfer-EncodingиContent-Encodingразница между.Transfer-EncodingПосле передачи он будет автоматически декодирован для восстановления исходных данных иContent-EncodingОно должно быть расшифровано самим приложением.

Если вы знаете другие решения или у вас есть какие-либо предложения для этой статьи, вы можете оставить сообщение ниже. Брат Абао выберет сообщение с наибольшим количеством лайков (горячий комментарий) и самое продуманное (качественное) сообщение из сообщений, и отправит их каждому1 значок от официального представителя Nuggets.

5. Справочные ресурсы