⚡️Фронтальная многопоточная загрузка больших файлов в 10 раз быстрее (возьмите облачный диск Baidu)

внешний интерфейс

задний план

Да, вы правильно прочитали, это многопоточность на интерфейсе, а неNode. На этот раз исследование началось с недавней разработки.В требованиях к разработке, связанных с потоковым видео, был обнаружен специальный код статуса.Его зовут206~

屏幕快照 2020-09-21 23.21.05

Чтобы эта статья не была скучной, давайте сначала добавим в текст рендеры. (с3.7Mнапример, фотографии большого размера).

Сравнение эффекта анимации (один поток — слева и 10 потоков — справа)

single-vs-multiple-donwload

Сравнение времени (один поток против 10 потоков)

image-20200915235421355

Если вы немного взволнованы, когда видите это, то, пожалуйста, продолжайте слушать меня, тогда давайте возьмем сумку и посмотрим, как происходит весь процесс.

GET /360_0388.jpg HTTP/1.1
Host: limit.qiufeng.com
Connection: keep-alive
...
Range: bytes=0-102399

HTTP/1.1 206 Partial Content
Server: openresty/1.13.6.2
Date: Sat, 19 Sep 2020 06:31:11 GMT
Content-Type: image/jpeg
Content-Length: 102400
....
Content-Range: bytes 0-102399/3670627

...(这里是文件流)

Вы можете видеть, что запрос имеет еще одно поле здесьRange: bytes=0-102399, на сервере также есть еще одно полеContent-Range: bytes 0-102399/3670627, а возвращаемый код состояния206.

ТакRangeЧто тогда? Я до сих пор помню, что несколько дней назад я написал статью о загрузке файлов, в которой упоминалось, как загружать большие файлы.Rangeвещи, ноПредыдущийВ систематическом обзоре загрузок файлов нетrangeдля подробного ознакомления.

Все коды ниже находятся вGitHub.com/flower1995116/…

Основное введение в диапазон

Происхождение диапазона

RangeвHTTP/1.1Новое поле в , эта функция также является основным механизмом, который мы используем, например, Thunder, который поддерживает многопоточную загрузку и загрузку с точкой останова. (Вступительная копия, выдержка)

Сначала клиент инициируетRange: bytes=0-xxxзапрос, если сервер поддерживает Range, он будет добавлен в заголовок ответаAccept-Ranges: bytesчтобы указать запрос, который поддерживает диапазон, а затем клиент может инициировать запрос с диапазоном.

Сервер через заголовок запросаRange: bytes=0-xxx Чтобы решить, следует ли выполнять обработку диапазона, если это значение существует и допустимо, только часть запрошенного содержимого файла отправляется обратно, код состояния ответа становится 206, что указывает на частичное содержимое, и устанавливается Content-Range. Если недействителен, возвращается код состояния 416, указывающий, что диапазон запроса не удовлетворяется. Если в заголовке запроса нет Range, сервер ответит нормально и не будет устанавливать Content-Range и т.д.

Value Description
206 Partial Content
416 Range Not Satisfiable

Формат диапазона:

Range:(unit=first byte pos)-[last byte pos]

которыйRange: 单位(如bytes)= 开始字节位置-结束字节位置.

Возьмем пример: допустим, мы включили многопоточную загрузку и нам нужно разделить файл размером 5000 байт на 4 потока для загрузки.

  • Диапазон: байты=0-1199 первые 1200 байт
  • Диапазон: байт=1200-2399 Вторые 1200 байт
  • Диапазон: байт=2400-3599 Третий 1200 байт
  • Диапазон: байт=3600-5000 последние 1400 байт

Сервер отвечает:

1-й ответ

  • Длина контента: 1200
  • Content-Range: байты 0-1199/5000

2-й ответ

  • Длина контента: 1200
  • Диапазон содержимого: байты 1200-2399/5000

3-й ответ

  • Длина контента: 1200
  • Диапазон содержимого: байты 2400-3599/5000

4-й ответ

  • Длина контента: 1400
  • Контент-диапазон: байты 3600-5000/5000

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

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity length]которыйContent-Range:字节 开始字节位置-结束字节位置/文件大小.

Поддержка браузера

В настоящее время все основные браузеры поддерживают эту функцию.

image-20200916002624861

Поддержка сервера

Nginx

После того, как версия nginx версии 1.9.8 (плюс ngx_http_slice_module) автоматически поддерживается по умолчанию, вы можете добавитьmax_rangesУстановить как0для отмены этой настройки.

Node

Узел не предоставляет по умолчаниюRange Метод обработки, вам нужно написать свой собственный код для обработки.

router.get('/api/rangeFile', async(ctx) => {
    const { filename } = ctx.query;
    const { size } = fs.statSync(path.join(__dirname, './static/', filename));
    const range = ctx.headers['range'];
    if (!range) {
        ctx.set('Accept-Ranges', 'bytes');
        ctx.body = fs.readFileSync(path.join(__dirname, './static/', filename));
        return;
    }
    const { start, end } = getRange(range);
    if (start >= size || end >= size) {
        ctx.response.status = 416;
        ctx.body = '';
        return;
    }
    ctx.response.status = 206;
    ctx.set('Accept-Ranges', 'bytes');
    ctx.set('Content-Range', `bytes ${start}-${end ? end : size - 1}/${size}`);
    ctx.body = fs.createReadStream(path.join(__dirname, './static/', filename), { start, end });
})

или вы можете использоватьkoa-sendэта библиотека.

GitHub.com/столб JS/цвет…

Практика стрельбища

Обзор архитектуры

Давайте сначала взглянем на обзор диаграммы архитектуры процесса. Один поток очень прост, вы можете скачать его в обычном режиме, вы можете обратиться ко мне, если вы не понимаетеПредыдущийстатья. Если многопоточное, то хлопот больше будет.Качать надо по частям.После скачивания нужно сливать и потом качать. (Для получения информации о методах загрузки, таких как blob, вы все равно можете обратиться кПредыдущий)

1600705973008

код сервера

просто, это правильноRangeсделал совместимым.

router.get('/api/rangeFile', async(ctx) => {
    const { filename } = ctx.query;
    const { size } = fs.statSync(path.join(__dirname, './static/', filename));
    const range = ctx.headers['range'];
    if (!range) {
        ctx.set('Accept-Ranges', 'bytes');
        ctx.body = fs.readFileSync(path.join(__dirname, './static/', filename));
        return;
    }
    const { start, end } = getRange(range);
    if (start >= size || end >= size) {
        ctx.response.status = 416;
        ctx.body = '';
        return;
    }
    ctx.response.status = 206;
    ctx.set('Accept-Ranges', 'bytes');
    ctx.set('Content-Range', `bytes ${start}-${end ? end : size - 1}/${size}`);
    ctx.body = fs.createReadStream(path.join(__dirname, './static/', filename), { start, end });
})

html

Потом давай html писать, тут и говорить нечего, пиши две кнопки для отображения.

<!-- html -->
<button id="download1">串行下载</button>
<button id="download2">多线程下载</button>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>

общедоступные параметры js

const m = 1024 * 520;  // 分片的大小
const url = 'http://localhost:8888/api/rangeFile?filename=360_0388.jpg'; // 要下载的地址

часть с одной резьбой

Однопоточный код загрузки, перейдите непосредственно к запросу наblobметод получения, а затем использоватьblobURLспособ скачать.

download1.onclick = () => {
    console.time("直接下载");
    function download(url) {
        const req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.responseType = "blob";
        req.onload = function (oEvent) {
            const content = req.response;
            const aTag = document.createElement('a');
            aTag.download = '360_0388.jpg';
            const blob = new Blob([content])
            const blobUrl = URL.createObjectURL(blob);
            aTag.href = blobUrl;
            aTag.click();
            URL.revokeObjectURL(blob);
            console.timeEnd("直接下载");
        };
        req.send();
    }
    download(url);
}

многопоточная часть

Сначала отправьте запрос головы, чтобы получить размер файла, а затем рассчитайте расстояние скольжения каждого фрагмента в соответствии с длиной и заданным размером фрагмента. пройти черезPromise.allв обратном вызове используйтеconcatenateФункция объединяет фрагментированные буферы в большой двоичный объект, а затем используетblobURLспособ скачать.

// script
function downloadRange(url, start, end, i) {
    return new Promise((resolve, reject) => {
        const req = new XMLHttpRequest();
        req.open("GET", url, true);
        req.setRequestHeader('range', `bytes=${start}-${end}`)
        req.responseType = "blob";
        req.onload = function (oEvent) {
            req.response.arrayBuffer().then(res => {
                resolve({
                    i,
                    buffer: res
                });
            })
        };
        req.send();
    })
}
// 合并buffer
function concatenate(resultConstructor, arrays) {
    let totalLength = 0;
    for (let arr of arrays) {
        totalLength += arr.length;
    }
    let result = new resultConstructor(totalLength);
    let offset = 0;
    for (let arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}
download2.onclick = () => {
    axios({
        url,
        method: 'head',
    }).then((res) => {
        // 获取长度来进行分割块
        console.time("并发下载");
        const size = Number(res.headers['content-length']);
        const length = parseInt(size / m);
        const arr = []
        for (let i = 0; i < length; i++) {
            let start = i * m;
            let end = (i == length - 1) ?  size - 1  : (i + 1) * m - 1;
            arr.push(downloadRange(url, start, end, i))
        }
        Promise.all(arr).then(res => {
            const arrBufferList = res.sort(item => item.i - item.i).map(item => new Uint8Array(item.buffer));
            const allBuffer = concatenate(Uint8Array, arrBufferList);
            const blob = new Blob([allBuffer], {type: 'image/jpeg'});
            const blobUrl = URL.createObjectURL(blob);
            const aTag = document.createElement('a');
            aTag.download = '360_0388.jpg';
            aTag.href = blobUrl;
            aTag.click();
            URL.revokeObjectURL(blob);
            console.timeEnd("并发下载");
        })
    })
}

Полный пример

GitHub.com/flower1995116/…

// 进入目录
cd file-download
// 启动
node server.js
// 打开 
http://localhost:8888/example/download-multiple/index.html

Поскольку Google Chrome имеет ограничения на одно доменное имя в HTTP/1.1, максимальное количество одновременных запросов для одного доменного имени составляет 6.

Это может быть отражено в исходном коде и обсуждениях с официальным персоналом.

Адрес для обсуждения

не говорите .chromium.org/afraid/chromium/…

Исходный код Хрома

// https://source.chromium.org/chromium/chromium/src/+/refs/tags/87.0.4268.1:net/socket/client_socket_pool_manager.cc;l=47
// Default to allow up to 6 connections per host. Experiment and tuning may
// try other values (greater than 0).  Too large may cause many problems, such
// as home routers blocking the connections!?!?  See http://crbug.com/12066.
//
// WebSocket connections are long-lived, and should be treated differently
// than normal other connections. Use a limit of 255, so the limit for wss will
// be the same as the limit for ws. Also note that Firefox uses a limit of 200.
// See http://crbug.com/486800
int g_max_sockets_per_group[] = {
    6,   // NORMAL_SOCKET_POOL
    255  // WEBSOCKET_SOCKET_POOL
};

Поэтому, чтобы соответствовать этой функции, я разделил файл на 6 сегментов, каждый сегмент520kb(Да, для написания кода требуется число, которое вас любит), то есть открыть 6 потоков для скачивания.

Я скачал 6 раз в один поток и в несколько потоков, и скорость вроде бы одинаковая. Так почему же все иначе, чем мы ожидали?

image-20200919165242745

Исследуйте причины неудач

Я начал тщательно сравнивать два запроса и наблюдать за скоростью двух запросов.

6 потоков одновременно

image-20200919170313455

один поток

image-20200919170512650

Если рассчитать по скорости 3,7M 82 мс, то загрузка 46 КБ занимает около 1 мс, а фактическую ситуацию можно увидеть, 533 КБ, средняя загрузка составляет около 20 мс (время подключения было запланировано, время загрузки чистого контента) .

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

Фактическая скорость передачи по сети делится на скорость восходящей линии связи и скорость нисходящей линии связи.Скорость исходящего каналаЭто скорость, с которой данные отправляются, а нисходящий канал — это скорость, с которой данные принимаются. ADSL - это метод передачи, реализованный в соответствии с привычкой, которую мы обычно просматриваем в Интернете и отправляем относительно небольшие требования к данным для загрузки данных. Мы говорим, что для 4MШирокополосный доступ, то наша теоретическая максимальная скорость загрузки составляет 512 КБ/с, что является так называемой скоростью нисходящего канала. --Энциклопедия Baidu

Итак, какова наша текущая ситуация?

Сравнив сервер с большой водопроводной трубой, позвольте мне использовать диаграмму, чтобы смоделировать ситуацию с загрузкой одного и нескольких потоков. Левая сторона — серверная, правая — клиентская. (Все следующие случаи считаются идеальными, просто для имитации процесса и без учета эффектов гонки некоторых других программ.)

один поток

IMG_01

Многопоточность

IMG_02

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

Ниже я объясню в нескольких случаях, в какой ситуации сработает наша многопоточность?

Пропускная способность сервера больше, чем пропускная способность пользователя без каких-либо ограничений

По сути, эта ситуация аналогична той, с которой мы столкнулись.

Пропускная способность сервера намного превышает пропускную способность пользователя, что ограничивает скорость одного соединения.

IMG_03

Если сервер ограничивает скорость загрузки одного широкополосного доступа, это также имеет место для большинства из них.Например, Baidu Cloud такой.Например, очевидно, что у вас широкополосный доступ 10M, но фактическая скорость загрузки составляет всего 100 КБ / с. В этом случае мы можем открыть больше потоков для загрузки, потому что это часто ограничивает загрузку одного TCP, конечно, онлайн-среда не означает, что пользователи могут открывать неограниченное количество потоков, ограничения все равно будут , что ограничит максимальный TCP вашего текущего IP. Ограничение загрузки в этом случае часто является максимальной скоростью вашего пользователя. Согласно приведенному выше примеру, если вы открыли 10 потоков и достигли максимальной скорости, потому что независимо от того, насколько велика запись, ваша запись была ограничена, тогда каждый поток будет вытеснять скорость, и открывать больше потоков бесполезно.

улучшить предложения

Из-за Node я не нашел относительно простого способа управления скоростью загрузки, поэтому представил Nginx.

Мы контролируем скорость каждого TCP-соединения до 1M/s.

добавить конфигурациюlimit_rate 1M;

Готов к работе

1.nginx_conf

server {
    listen 80;
    server_name limit.qiufeng.com;
    access_log  /opt/logs/wwwlogs/limitqiufeng.access.log;
    error_log  /opt/logs/wwwlogs/limitqiufeng.error.log;

    add_header Cache-Control max-age=60;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,range,If-Range';
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    limit_rate 1M;
    location / {
        root 你的静态目录;
        index index.html;
    }
}

2. Настройте локальный хост

127.0.0.1 limit.qiufeng.com

Глядя на эффект, скорость в основном нормальная, а многопоточная загрузка быстрее, чем однопоточная. Скорость в основном 5-6:1, но я обнаружил, что если в процессе загрузки несколько раз быстро нажимать, использоватьRangeЗагрузка будет происходить все быстрее и быстрее (подозревается, что Nginx выполняет какое-то кеширование, и я пока не изучал его подробно).

修改代码中的下载地址
const url = 'http://localhost:8888/api/rangeFile?filename=360_0388.jpg';
变成
const url = 'http://limit.qiufeng.com/360_0388.jpg';

Тест скорости загрузки

image-20200919201613507

Помните, что я говорил выше, оHTTP/1.1Один и тот же сайт может иметь только 6 одновременных запросов, а лишние запросы будут помещены в следующий пакет. ноHTTP/2.0Не подлежит этому ограничению, мультиплексирование заменяетHTTP/1.xизПоследовательность и механизмы блокировки. давайте обновимHTTP/2.0Приходите проверить это.

Сертификат должен быть сгенерирован локально. (Метод создания сертификата:nuggets.capable/post/684490…

server {
    listen 443 ssl http2;
    ssl on;
    ssl_certificate /usr/local/openresty/nginx/conf/ssl/server.crt;
    ssl_certificate_key /usr/local/openresty/nginx/conf/ssl/server.key;
    ssl_session_cache shared:le_nginx_SSL:1m;
    ssl_session_timeout 1440m;

    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers RC4:HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    server_name limit.qiufeng.com;
 
    access_log  /opt/logs/wwwlogs/limitqiufeng2.access.log;
    error_log  /opt/logs/wwwlogs/limitqiufeng2.error.log;

    add_header Cache-Control max-age=60;
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,range,If-Range';
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    limit_rate 1M;
    location / {
        root 你存放项目的前缀路径/node-demo/file-download/;
        index index.html;
    }
}

10 потоков

将单个下载大小进行修改
const m = 1024 * 400;

image-20200919200203877

12 потоков

image-20200919202302096

24 потока

image-20200919202138838

Конечно, чем больше потоков, тем лучше.После тестирования выяснилось, что когда количество потоков достигает определенного числа, скорость будет медленнее. Ниже показан рендеринг 36 одновременных запросов.

image-20200919202427985

Практическое исследование приложений

Какая польза от такого количества загрузок процессов? Правильно, как я сказал в начале, этот механизм сегментирования является основным механизмом для загрузки программного обеспечения, такого как Thunder.

Облачный класс NetEase

Study.163.com/course/Соберитесь…

Мы открыли консоль и легко нашли URL-адрес загрузки, который представлял собой адрес загрузки mp4.

image-20200920222053726

Запустите наш тестовый скрипт из консоли.

// 测试脚本,由于太长了,而且如果仔细看了上面的文章也应该能写出代码。实在写不出可以看以下代码。
https://github.com/hua1995116/node-demo/blob/master/file-download/example/download-multiple/script.js

Скачать

image-20200920221657541

многопоточная загрузка

image-20200920221853959

Видно, что поскольку NetEase Cloud Classroom не имеет ограничений на скорость загрузки одного TCP, скорость улучшения не столь очевидна.

Облако Байду

Давайте протестируем веб-версию Baidu Cloud.

image-20200919210106839

Возьмем в качестве примера файл размером 16,6 МБ.

Откройте веб-страницу Baidu Cloud Disk, нажмите «Загрузить».

image-20200920222309345

В это время нажмите паузу, откройтеchrome -> 更多 -> 下载内容 -> 右键复制下载链接

image-20200922004619680

По-прежнему используйте указанный выше курс NetEase Cloud, чтобы загрузить сценарий курса. Вам просто нужно изменить параметры.

url 改成对应百度云下载链接
m 改成 1024 * 1024 * 2 合适的分片大小~

Скачать

Ограничение скорости одного TCP-соединения в Baidu Cloud действительно негуманно, и оно заняло целых 217 секунд! ! ! Просто файл 17M, мы обычно этим страдаем. (кроме VIP-игроков)

image-20200919211105023

многопоточная загрузка

image-20200919210516632

Поскольку это HTTP/1.1, нам нужно открыть только 6 или более потоков для загрузки. Ниже приведена скорость многопоточной загрузки, которая заняла около 46 секунд.

image-20200919210550840

Давайте посмотрим на разницу в скорости через эту картинку.

image-20200922010911389

Это действительно ароматно. Это бесплатно, и мы реализуем эту функцию только на нашем интерфейсе. Это слишком ароматно. Почему бы вам не попробовать? ?

Недостатки схемы

1. Есть определенные ограничения на верхний предел больших файлов

из-заblobВ основных браузерах есть ограничение на размер верхнего предела, поэтому этот метод все же имеет определенные недостатки.

Browser Constructs as Filenames Max Blob Size Dependencies
Firefox 20+ Blob Yes 800 MiB None
Firefox < 20 data: URI No n/a Blob.js
Chrome Blob Yes 2GB None
Chrome for Android Blob Yes RAM/5 None
Edge Blob Yes ? None
IE 10+ Blob Yes 600 MiB None
Opera 15+ Blob Yes 500 MiB None
Opera < 15 data: URI No n/a Blob.js
Safari 6.1+* Blob No ? None
Safari < 6 data: URI No n/a Blob.js
Safari 10.1+ Blob Yes n/a None

2. На сервере есть ограничение на одиночную скорость TCP

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

конец

Статья написана на скорую руку, и выражение может быть не совсем точным, если есть ошибки, прошу указать.

Оглядываясь назад и выясняя, есть ли подключаемый модуль для веб-версии Baidu Cloud для ускорения, если нет, создайте подключаемый модуль для веб-версии Baidu Cloud для загрузки~.

использованная литература

Управление пропускной способностью Nginx:blog.huoding.com/2015/03/20/…

openresty развертывает https и включает поддержку http2:вооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооооо

Давайте поговорим о диапазоне HTTP:Лепешка 1022.GitHub.IO/2016/12/24/…

❤️ Всем спасибо

Если вы считаете этот контент полезным:

  1. Поставьте лайк и поддержите его, чтобы больше людей могли увидеть этот контент, ваши лайки — моя самая большая поддержка.
  2. Обратите внимание на публичный аккаунт秋风的笔记, свяжитесь с автором и изучите интересные знания вместе.
  3. Загружать и скачивать серии статей