См. HTTP/2 из исходного кода Chrome

HTTP JavaScript Chrome CSS

Я здесь"Как обновить сайт до http/2"Представляет метод обновления до http/2 и объясняет преимущества http/2:

  • сжатие заголовка http
  • мультиплексирование
  • Server Push

По одному будет описано ниже.

1. Сжатие заголовков

Зачем вам нужно сжатие заголовков? Я здесь"Веб-сокет и TCP/IP«Говорят: HTTP-заголовок относительно длинный, если отправляемые данные относительно малы, необходимо отправить большой HTTP-заголовок, как показано на следующем рисунке:

Когда количество таких запросов велико, пропускная способность сети будет низкой. Более того, относительно большой HTTP-заголовок быстро заполнит окно перегрузки во время процесса медленного запуска, что приведет к увеличению задержки. Поэтому сжатие заголовков HTTP очень необходимо.Предшественник HTTP/2, SPDY, представил алгоритм сжатия deflate, но говорят, что он уязвим для атак.HTTP/2 использует новый метод сжатия.RFC 7541объяснил. Что касается сжатия заголовка, приложение спецификации дает очень яркий пример. Этот пример используется здесь как иллюстрация, чтобы объяснить, как заголовки HTTP могут быть сжаты.

Во-первых, часто используемые поля заголовка HTTP пронумерованы и представлены статической таблицей:

Index Header Name Header Value
1 :authority
2 :method GET
3 :method POST
4 :path /
5 :path /index.html
6 :scheme http
7 :scheme https
8 status 200
9 status 204
... ... ...
16 accept-encoding gzip, deflate
17 accept-language
... ... ...
61 www-authenticate

Всего их 61, из которых начало двоеточия, например: метод, находится в строке запроса, 2 означает метод: POST, если вы хотите иметь в виду метод: ВАРИАНТ? Используйте следующее представление:

0206OPTION

Среди них 02 представляет индекс индекса в статической таблице.Проверьте эту таблицу, чтобы узнать, что имя заголовка, представленное 2, является: метод. Следующий 06 указывает, что длина имени метода равна 6, за которым следует содержимое поля, т. е. имя метода — OPTION. Так как же он узнает, что 06, за которым следует 02, не является полем заголовка ":scheme http" с индексом 6? Потому что, если и имя заголовка, и значение заголовка используют эту таблицу, например, метод POST выражается как:

0x82

Вместо 02 здесь 8-я позиция заменена на 1, которая становится двоичной 1000 0002, указывая на то, что имя/значение точно совпадают. А если 8-й бит не равен 1, например 0000 0002, то значение значения настраивается, а следующий байт — это длина символа значения, за которым следуют символы соответствующей длины.

Символ значения кодируется с помощью Хаффмана, а спецификация устанавливает значение в соответствии с частотой использования символа.Таблица кодирования, эта таблица кодирования контролирует размер часто используемых символов от 5 до 7 бит, что меньше, чем 8 бит кодирования ASCII. По таблице кодирования:

sym code as bits code as hex len in bits
'O' 1101010 6a 7
'P' 1101011 6b 7
'T' 1101111 6f 7
'I' 1100100 64 7
'N' 1101001 69 7

ОПЦИЯ будет закодирована как: 6a6b 6f64 6a69, поэтому Метод: ОПЦИЯ, наконец, закодирована как:

0206 6a6b 6f64 6a69

Всего 8 байт, для исходной строки требуется 14 байт.

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

Например, поле заголовка первого запроса «Метод: ОПЦИЯ» не существует в статической таблице, оно будет помещено в стек, на данный момент стек имеет только один элемент, а индекс равен 62 = 61 + 1 к укажите это поле.Если это поле используется во втором и третьем запросах, оно будет представлено индексом 62, то есть, если встречается 62, оно будет представлять Метод: ОПЦИЯ. Если в этот стек будет помещено другое пользовательское поле, индекс этого поля будет равен 62, а метод: ОПЦИЯ станет равным 63, и чем ближе к помещенному числу, тем дальше вперед.

Индекс статической таблицы начинается с 1, динамической таблицы начинается с 62, а индекс 0 указывает на имя настраиваемого поля, которое представлено длиной ключа + ключом + значением длины + значением При нажатии этого настраиваемого поля к динамической таблице Оказавшись внутри, она имеет индекс. Конечно, вы можете контролировать, нужно ли вам помещать поле в динамическую таблицу, установив флаг, который здесь не будет объясняться.

Этот алгоритм называется HPACK, более подробный процесс можно посмотретьRFC 7541: HPACK: сжатие заголовков для HTTP/2(Вы можете сразу потянуть до конца, чтобы увидеть пример).

Chrome выполняет синтаксический анализ заголовка в каталоге src/net/http2/hpack, а статическая форма находится в этом файле.hpack_static_table_entries,Как показано ниже:

Согласно документации, динамические таблицы имеют максимальное количество полей по умолчанию 4096:

  // The last received DynamicTableSizeUpdate value, initialized to
  // SETTINGS_HEADER_TABLE_SIZE.
  size_t size_limit_ = 4096;  // Http2SettingsInfo::DefaultHeaderTableSize();

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

Динамические таблицы в Chrome представлены векторной структурой данных:

  const std::vector<HpackStringPair>* const table_;

Вектор — это динамический массив в C++. Вставляйте перед массивом каждый раз, когда вы вставляете:

table_.push_front(entry);

При поиске используйте индекс массива для прямого поиска.Следующее предназначено для поиска динамического массива:

// Lookup函数
index -= kFirstDynamicTableIndex; // kFirstDynamicTableIndex等于62
if (index < table_.size()) {
  const HpackDecoderTableEntry& entry = table_[index];
  return entry;
}
return nullptr;

Основное содержание сжатия заголовков здесь, а затем поговорим о более мощном мультиплексировании.

2. Мультиплексирование

Для улучшения параллелизма в традиционном HTTP/1.1 необходимо увеличить количество подключений, то есть одновременно отправлять еще несколько запросов.Поскольку одно подключение может отправить только один запрос, необходимо установить еще несколько TCP-соединения. Установление TCP-соединения требует накладных расходов на потоки, и мы знаем, что Chrome может одновременно устанавливать не более 6 соединений для одного и того же домена. Так что есть решения по уменьшению количества запросов, такие как карты спрайтов, слияние файлов кода и т.д.

В HTTP / 2 домен должен только установить соединение TCP для передачи нескольких ресурсов. Несколько потоков / сигналов / сигналов передач передаются через один канал, что делает полное использование высокоскоростных каналов, которое называется мультиплексированием.

В HTTP/1.1 ресурс передается через TCP-соединение, а большой ресурс может быть разбит на несколько сегментов TCP, каждый сегмент имеет свой номер, в порядке возрастания от начала к концу, получатель объединяет полученные сегменты сообщения последовательно в получить полный ресурс. Конечно, это естественная особенность TCP-передачи, которая не имеет ничего общего с HTTP/1.1.

Итак, как использовать одно соединение для передачи нескольких ресурсов? HTTP/2 называет передачу каждого ресурса потоком, каждый поток имеет свой уникальный номер идентификатора потока, поток может быть разделен на несколько кадров, каждый кадр отправляется последовательно, а количество TCP-пакетов может быть гарантировано. быть в большем порядке, чем те, которые были отправлены ранее. В HTTP/1.1 одна и та же последовательность ресурсов увеличивается непрерывно, потому что ресурс всего один, в HTTP/2, вероятно, увеличивается дискретно, и кадры для отправки других потоков будут вставляться в середину, но до тех пор, пока она гарантировал, что все потоки соединены по порядку. Такие какСледующий рисунокпоказано:

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

При общении поток делится на несколько фреймов, HTTP/2 определяет 11 типов фреймов, включая HEADERS/DATA/SETTINGS и т. д. HEADERS используется для передачи http-заголовков, а DATA — для отправки данных запроса/ответа. используется для управления во время передачи. Формат кадра показан на следующем рисунке:

Заголовок кадра имеет 9 байтов, а первые 3 байта (24 бита) представляют длину полезной нагрузки кадра (Полезная нагрузка кадра), поэтому максимальный объем данных, которые могут быть переданы в каждом кадре, составляет 2^24 = 16 МБ, но стандарт устанавливает значение по умолчанию. Максимум 2^14 = 16Кб, если только обе стороны не контролируются через фрейм настроек. Четвертый тип байта указывает тип кадра, например, 0x0 для данных, 0x1 для заголовков и 0x4 для настроек. Флаги — это флаговые биты, используемые каждым фреймом для управления некоторыми параметрами.Например, когда первый бит флага в фрейме данных установлен на 0x1, это означает END_STREAM, то есть текущий фрейм данных является последним фреймом данных текущий поток. Идентификатор потока — это идентификатор потока, то есть номер потока, а его первый R — зарезервированный бит (зарезервированный для последующего использования). Наконец, есть Payload, полезная нагрузка текущего кадра.

Каждый запрос создает поток, и каждый поток создается запросчиком, отправляющим кадр заголовка, то есть кадр заголовка используется для открытия потока, и каждый поток имеет свой приоритет, который помещается в кадр заголовка. Кадр заголовка потока также содержит поле заголовка HTTP Compression, упомянутое в пункте 1 выше.

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

Более подробное описание можно найти в этом документе:Hypertext Transfer Protocol Version 2 (HTTP/2).

мы получаем доступWalking DogЭта страница используется в качестве иллюстрации, чтобы увидеть, как передаются потоки и кадры.Эта страница загружает в общей сложности 13 ресурсов:

Включает index.html, main.js, main.css и 10 изображений.

Каталог, в котором Chrome декодирует кадры HTTP/2, находится в src/net/http2, а каталог для кодирования — в src/net/spdy.

Так называемое декодирование заключается в анализе полученного кадра http/2 в соответствии с форматом и наблюдении за этим процессом путем печати журнала в Chrome, как показано в следующем примере кода:

Объясните в порядке печати:

(1) НАСТРОЙКИ (stream_id = 0; флаги = 0; длина = 18)

Во-первых, получен кадр SETTINGS, а содержимое полезной нагрузки:

parameter=MAX_CONCURRENT_STREAMS(0x3), value=128
parameter=INITIAL_WINDOW_SIZE(0x4), value=65536
parameter=MAX_FRAME_SIZE(0x5), value=16777215

Другая сторона, сервер (nginx), устанавливает для max_concurrent_streams значение 128, указывая, что максимальное количество одновременных потоков равно 128, то есть одновременно может быть не более 128 запросов. window_size используется для выполненияУправление потоком, указывает емкость буфера, полученную другой стороной, которая делится на емкость глобального буфера и емкость буфера отдельного потока.Если stream_id равен 0, это означает глобальный, а если он не равен 0, это соответствующий поток. Вышеупомянутая служба устанавливает начальный размер окна равным 64 КБ. Это значение может быть скорректировано в процессе отправки. Когда буферное пространство получателя заполнено, оно может быть установлено равным 0 и отправлено другой стороне, чтобы сообщить другой стороне, чтобы отправь мне еще.Это очень похоже на окно перегрузки TCP, но делается это на уровне приложения.Удобно контролировать получение каждого потока.Например,при недостатке места в буфере поток с высоким приоритетом может дать больший размер окна. max_frame_size указывает максимальную полезную нагрузку каждого кадра, для которой установлено максимальное значение 16 МБ. При этом браузер также отправляет в сервис собственные настройки. Если настройки не являются значениями по умолчанию, указанными в используемом стандарте, то рамка настроек передается.

Затем был получен второй кадр:

(2) WINDOW_UPDATE (идентификатор потока = 0; флаг = 0; длина = 4)

Фрейм типа window_update используется для обновления размера окна, а содержимое полезной нагрузки:

window_size_increment=2147418112

Здесь для параметра window_size установлено максимальное значение 2 ГБ, и max_frame_size в первом кадре также является максимальным значением, что означает, что сервис не ограничивает скорость приема. Идентификатор потока здесь равен 0, что также означает, что эта конфигурация является глобальной.

Так почему бы не установить размер окна напрямую при его инициализации напрямую, так это реализовано. Для сравнения, кадр, полученный при подключении к gstatic.com от Google, выглядит так:

INITIAL_WINDOW_SIZE, value=1048576

MAX_HEADER_LIST_SIZE, value=16384

window_size_increment=983041

Это выглядит более разумно (кроме того, здесь также устанавливается максимальное количество полей заголовка).

В исходном коде Chrome я вижу только, что размер окна используется в одном месте, то есть когда размер окна другой стороны равен 0, поток будет поставлен в очередь:

  if (session_->IsSendStalled() || send_window_size_ <= 0) {
    return Requeue;
  }

и черезисходный код нгинкс, мы обнаружили, что nginx будет вычитать текущий размер кадра из размера окна каждый раз при отправке кадра:

ngx_http_v2_queue_frame(h2c, frame);
h2c->send_window -= frame_size; 
stream->send_window -= frame_size;

После успешной отправки cleanup добавит его обратно, и если send_window станет равным 0, оно будет поставлено в очередь.

(3) НАСТРОЙКИ (идентификатор потока = 0; флаг = 1; длина = 0)

Третий фрейм — это тоже настройки, но на этот раз контента нет, длина равна 0, а флаг установлен в 1 для обозначения ACK, указывая на то, что он согласен с SETTINGS, отправленными ему браузером. Флаги имеют разное значение в разных типах фреймов, как показано в следующем коде:

enum Http2FrameFlag {
  END_STREAM = 0x01,   // DATA, HEADERS 表示当前流结束
  ACK = 0x01,          // SETTINGS, PING settings表示接受对方的设置,而ping是用来协助对方测量往返时间的
  END_HEADERS = 0x04,  // HEADERS, PUSH_PROMISE, CONTINUATION 表示header部分完了
  PADDED = 0x08,       // DATA, HEADERS, PUSH_PROMISE 表示payload后面有填充的数据
  PRIORITY = 0x20,     // HEADERS 表示有当前流有设置权重weight
};

Затем принимается 4-й кадр:

(4) ЗАГОЛОВКИ (идентификатор потока = 1; флаг = 4; длина = 107)

Это заголовок ответа на запрос, который является inde.html. Флаг 4 означает END_HEADERS, что означает, что этот заголовок имеет только один фрейм. Если флаг равен 0, это означает, что есть еще. Затем Chrome анализирует полученный заголовок байт за байтом, следуя обратному процессу метода сжатия заголовка, упомянутого выше.

Сначала выньте первый байт, чтобы определить, какой тип заголовка, индексированный или неиндексированный, так называемый индексированный означает, что таблица может быть найдена, например, первый байт 0x82 — это IndexedHeader. Затем удалите старшие биты, оставив 0x02 для представления индекса таблицы:

Если это IndexedHeader, то он перейдет к просмотру динамической таблицы и статической таблицы:

В противном случае вам придется проанализировать ключ/значение и длину строки l. Это может быть код Хаффмана или нет. Код делает вывод:

uint8_t h_and_prefix = db->DecodeUInt8();
bool huffman_encoded = (h_and_prefix & 0x80) == 0x80;

Chrome также поддерживает таблицу Хаффмана:

После разбора пары заголовков клавиш / значений он может нажать его в динамическую таблицу. Затем продолжайте разбирать следующий до конца.

(5) ДАННЫЕ (идентификатор потока = 1; флаг = 1; длина = 385)

После кадра заголовка идет кадр данных index.html, где флаг равен 1, чтобы указать END_STREAM, а длина равна 385. Поскольку данных относительно мало, отправляется один кадр.

Распечатываем полученную полезную нагрузку прямо вот так (я отключил gzip, иначе будет распечатывать сжатый контент):

Этот контент представляет собой html-текст, мы видим, что HTTP/2 не обрабатывает отправленный контент, а только контролирует отправленную форму. Часто говорят, что HTTP/2 бинарный, и следует сказать, что заголовок фрейма бинарный, но содержимое еще какое.

(6) ЗАГОЛОВКИ (идентификатор потока = 3; флаг = 4; длина = 160)

Затем был получен еще один кадр заголовка, это заголовок ответа main.css, а идентификатор потока равен 3.

(7) ДАННЫЕ (идентификатор потока = 3; флаг = 1; длина = 827)

Это кадр данных main.css:

(8) ЗАГОЛОВКИ (идентификатор потока = 5; флаг = 4; длина = 171)

Это заголовок ответа main.js

(9) ДАННЫЕ (идентификатор потока ДАННЫХ = 5; флаг = 1; длина = 4793)

Полезная нагрузка main.js, как показано на следующем рисунке:

(10) HEADERS (идентификатор потока HEADERS = 7; флаг = 4; длина = 163)

Это заголовок ответа 0.png

(11) ДАННЫЕ (идентификатор потока = 7; флаг = 0; длина = 8192)

0.png полезная нагрузка:

Обратите внимание, что флаг здесь равен 0, а не 1, что указывает на то, что позади находится фрейм данных. Затем был получен еще один кадр:

(12) ДАННЫЕ (идентификатор потока = 7; флаг = 1; длина = 2843)

Идентификатор этого потока по-прежнему равен 7, но флаг равен 1, чтобы указать END_STREAM. Описание 0.png (11 КБ) разбивается на два кадра и отправляется.

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


Теперь добавьте кадры, отправленные Chrome, и вам не составит труда понять Chrome с помощью приведенной выше основы.

Chrome также отправит другой стороне свой фрейм настроек, что делается при инициализации сеанса.Код находится в функции SendInitialData файла net/spdy/chromium/spdy_session.cc. Распечатываем интересующие кадры по порядку:

(1) НАСТРОЙКИ

Содержание следующее:

HEADER_TABLE_SIZE, value = 65536

MAX_CONCURRENT_STREAMS, value = 1000

INITIAL_WINDOW_SIZE, value = 6291456

Когда Chrome является получателем, максимальное количество одновременных потоков равно 1000.

(2) WINDOW_UPDATE

Недавно добавленный размер окна:

window_size_increment = 15663105

Около 15Мб.

Затем поток ввода-вывода Chrome выполняет задачу запроса, и первым извлекается запрос index.html, который печатается следующим образом:

../../net/spdy/chromium/spdy_http_stream.cc (95) SpdyHttpStream::InitializeStream : request url = www.rrfed.com/html/walking-dog/index.html

Сначала он создает фрейм HEADERS

(3) ЗАГОЛОВКИ (index.html stream_id = 1; вес = 256; зависимость = 0; флаг = 1)

Выводим два других параметра заголовка фрейма: вес и поток, от которого зависит зависимость. Вес равен 256, а зависимый поток равен 0 или отсутствует. Вес используется в качестве ориентира для приоритета, а значение находится в диапазоне от 1 до 256, поэтому текущий поток имеет наивысший приоритет. Зависимость упоминается ниже.

После получения HTML-кода Chrome анализирует тег ссылки main.css и тег скрипта main.js, а затем повторно инициализирует два потока, которые открываются путем отправки кадров HEADERS.

(4) ЗАГОЛОВКИ (main.css stream_id = 3; вес = 256; зависимость = 0; флаг = 1)

main.css также имеет наивысший приоритет

(6) ЗАГОЛОВКИ (main.js stream_id = 3; вес = 220; зависимость = 3; флаг = 1)

Вес main.js равен 220, вес этого файла скрипта меньше, чем у html/css, и зависит от потока, id которого равен 3, то есть main.css.

После получения JS, анализа JS, JS запускает загрузку 9 изображений png, а затем Chrome инициализирует 9 потоков за один раз:

(7) ~ (15)

0.png stream_id = 7, weight = 147, dependent_stream_id = 0, flags = 1

1.png stream_id = 9, weight = 147, dependent_stream_id = 7, flags = 1

2.png stream_id = 11, weight = 147, dependent_stream_id = 9, flags = 1

...

Видно, что вес картинки меньше, чем вес скрипта, и потоки этих картинок имеют отношение зависимости, причем последний поток зависит от первого потока.

Зависимости указаны в полезной нагрузке HEADER, как показано на следующем рисунке:

Зависимости приоритетов и размер окна — два наиболее важных метода управления мультиплексированием в HTTP/2. Какая польза от этой зависимости? Инструкции для следующих документов:

Inside the dependency tree, a dependent stream SHOULD only be allocated resources if either all of the streams that it depends on (the chain of parent streams up to 0x0) are closed or it is not possible to make progress on them.

Это означает, что зависимый дочерний узел может выделять ресурсы для обработки только до тех пор, пока не будут обработаны все его родительские узлы. Другими словами, в этом дереве зависимостей приоритетов родительский узел имеет более высокий приоритет обработки, чем дочерний узел. Документ лишь описывает некоторые особенности дерева зависимостей приоритетов, и не объясняет, как его реализовать, а лишь говорит, что разные сценарии могут иметь разные реализации. Как Chrome это делает?

Он использует двумерный массив, первое измерение приоритетное, то есть в массив помещается id потока с таким же приоритетом, а при инициализации потока он помещается в массив соответствующего приоритета. Обратите внимание, что приоритет здесь относится к атрибутам spdy3, от самого высокого уровня оптимизации 0 до самого низкого уровня оптимизации 7, и имеет отношение преобразования с весом ([1, 256]), и способ преобразования здесь не обсуждается. они означают одно и то же. Добавьте поток в конец соответствующего массива приоритетов, как показано в следующем коде:

id_priority_lists_[priority].push_back(std::make_pair(stream_id, priority));

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

let stream_id = 1, // 当前流的id,从外面传进来
    priority = 0, // 当前流的优先级,从外面传进来的
    id_priority_lists_ = []; // 它是一个二维数组
id_priority_lists_[0] = [];  // 这里先初始化一下
const kV3HighestPriority = 0; // 最高优先级为0

let dependent_stream_id = 0; // 父结点的stream id
for (let i = priority; i >= kV3HighestPriority; i--) {
    let length = id_priority_lists_[i].length;
    if (length) {
        dependent_stream_id = id_priority_lists_[i][length - 1];
        break;
    }
}
id_priority_lists_[priority].push(stream_id);

Этот код должен быть простым для понимания. В цикле for ищите от текущего приоритета до высокого уровня оптимизации, пока не найдете один. Если нет, то его родительский поток равен 0, что указывает на то, что это корневой узел.

Кроме того, после закрытия потока текущий поток удаляется из двумерного массива. Поэтому после получения html поток html удаляется, поэтому вновь созданный поток CSS не имеет зависимых родительских узлов, но вновь созданный поток JS имеет более низкий приоритет, чем поток CSS, поэтому поток JS имеет более низкий приоритет, чем поток Поток CSS.Родительским узлом является поток CSS.

Мы видим, что Chrome на самом деле не хранит такое дерево, а просто использует такой двумерный массив, чтобы найти родительский узел текущего потока, установить зависимость кадра HEADER, а затем передать его серверу, чтобы сообщить серверу приоритет этих потоков.

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

struct ngx_http_v2_node_s {
    ngx_uint_t                       id;
    ngx_http_v2_node_t              *index;
    ngx_http_v2_node_t              *parent;
    ngx_queue_t                      queue;
    ngx_queue_t                      children;
    ngx_queue_t                      reuse;
    ngx_uint_t                       rank;
    ngx_uint_t                       weight;
    double                           rel_weight;
    ngx_http_v2_stream_t            *stream;
};

В фактических вычислениях используются относительный вес и ранговое ранжирование rel_weight.Этот относительный вес и ранг рассчитываются с использованием веса и зависимости:

// 如果当前结点没有父结点,那么它的排名就为第一
if (parent == null) {
    node->rank = 1;
    node->rel_weight = (1.0 / 256) * node->weight;
}
// 否则的话,它的排名就是父结点的排名加1
// 而它的相对权重就是父结点的 weight/256
else {
    node->rank = parent->rank + 1;
    node->rel_weight = (parent->rel_weight / 256) * node->weight;
}

Можно видеть, что относительный вес rel_weight дочернего узла равен весу родительского узла, умноженному на 256. Обратите внимание, что вес

Какой смысл знать этих двоих?

Когда количество параллельных потоков превышает максимальное количество параллельных потоков max_concurrent_streams или буферное пространство буфера израсходовано, текущий поток должен быть помещен в очередь ожидания в это время.Эта очередь имеет порядок.Чем раньше элемент может быть обработан быстрее, чем выше приоритет, тем больше вперед. При вставке в эту очередь потока, который нуждается в ожидании, вам нужно использовать ранжирование приоритета, чтобы определить, куда его вставить.Реализация функции ngx_http_v2_waiting_queue выглядит следующим образом:

// stream表示要插入的流
stream->waiting = 1;

// 从waiting列队的最后一个元素开始,依次往前遍历,直到完了
for (q = ngx_queue_last(&h2c->waiting);
     q != ngx_queue_sentinel(&h2c->waiting);
     q = ngx_queue_prev(q))
{
    // 取出当前元素的数据
    s = ngx_queue_data(q, ngx_http_v2_stream_t, queue);

    // 这段代码的核心在于这个判断
    // 如果要插入的流的排名比当前元素的排名要靠后,
    // 或者排名相等但是相对权重比它小,就插到它后面。
    if (s->node->rank < stream->node->rank
        || (s->node->rank == stream->node->rank
            && s->node->rel_weight >= stream->node->rel_weight))
    {   
        break;
    }   
}

// 这里执行插入,如果stream的优先级比队列的任何一个元素
// 都要高的话,就插到队首去了
ngx_queue_insert_after(q, &stream->queue);

Вставить текущий поток после элемента с более высоким приоритетом, чем он, используя rank и rel_weight.Rank определяется зависимостью, а rel_weight в основном определяется весом.

Мы обнаружили, что nginx также использует аналогичную оценку для определения порядка отправки кадров при отправке очередей кадров.Следующий код функции ngx_http_v2_queue_frame:

if ((*out)->stream->node->rank < frame->stream->node->rank
    || ((*out)->stream->node->rank == frame->stream->node->rank
        && (*out)->stream->node->rel_weight
           >= frame->stream->node->rel_weight))
{
    break;  
}

Здесь представлено мультиплексирование HTTP/2.Упомянутые выше потоки активно открываются браузером, а поток HTTP/2 Server Push запускается службой.

3. Server Push

Когда мы используем HTTP/1.1, Chrome одновременно загружает до 6:

Когда мы используем HTTP/2, такого ограничения нет, и некоторые из них являются максимальным количеством одновременных потоков, например 100, упомянутых выше, как показано на следующем рисунке:

Мы заметили, что время завершения загрузки картинки идет сверху вниз, на что должна влиять упомянутая выше зависимость приоритета, последняя картинка будет зависеть от предыдущей картинки, поэтому приоритет предыдущей картинки будет выше. . Зеленый цвет на временной шкале представляет время ожидания TTFB (время до первого байта), то есть время, необходимое для получения первого байта после отправки запроса, а синий цвет — время загрузки содержимого. Здесь видно, что время ожидания TTFB последовательно увеличивается.

Хотя используется HTTP/2, ограничение в 6 не ограничено, но мы обнаружили, что css/js необходимо анализировать перед анализом html, чтобы инициировать загрузку, а изображения запускаются для загрузки через новое изображение JS, поэтому им нужно подождать, пока JS загружается и парсится, чтобы начать загрузку.

Таким образом, Server Push должен решить эту проблему задержки загрузки и заранее отправить ресурсы, необходимые веб-странице, в браузер.Nginx 1.13.9Версию начали поддерживать, и то совсем недавно (2018/2). Скомпилировав новую версию nginx, вы сможете испытать функцию Server Push и добавить следующую конфигурацию в nginx.conf:

location = /html/walking-dog/index.html {
    http2_push /html/walking-dog/main.js?ver=1;
    http2_push /html/walking-dog/main.css;
    http2_push /html/walking-dog/dog/0.png;
    http2_push /html/walking-dog/dog/1.png;
    http2_push /html/walking-dog/dog/2.png;
    http2_push /html/walking-dog/dog/3.png;
    http2_push /html/walking-dog/dog/4.png;
    http2_push /html/walking-dog/dog/5.png;
    http2_push /html/walking-dog/dog/6.png;
    http2_push /html/walking-dog/dog/7.png;
    http2_push /html/walking-dog/dog/8.png;
}

Укажите ресурсы, которые необходимо отправить, а затем наблюдайте за графиком загрузки:

Мы обнаружили качественное изменение времени загрузки, которое в основном закончилось примерно за 100 мс. Так что, если Sever Push использовать правильно, эффект довольно большой.

Поток Server Push открывается через фрейм типа Push Promise, формат фрейма Promise показан на следующем рисунке:

На самом деле это запрошенный кадр HEADER, но это не то же самое, что и HEADER, без веса/зависимости.

По вышеописанному методу наблюдайте, как происходит процесс передачи кадров после добавления Push Promise.

Браузер запрашивает загрузку index.html в потоке с stream_id = 1. В это время сервис не сразу отвечает на заголовок и данные, а возвращает подряд 11 кадров Push Promise, id потока 2, 4 , и 6 соответственно. Подождите, и тогда браузер сразу же создаст соответствующий поток.Получив промис 2, он создаст поток 2, а получив обещание 4, создаст поток 4. В это время поток создается и начинает загружаться, не дожидаясь, пока он будет разобран на html или js.

Во время этого процесса Chrome сначала разрешит обещанный идентификатор потока, как показано на следующем рисунке:

Затем проанализируйте заголовок Hpack и используйте этот заголовок для создания потока. Мы не будем подробно обсуждать Server Push, читатели могут открытьэтот URLПочувствуй это.


Подводя итог, мы в основном обсудили три основные особенности HTTP/2:

(1)сжатие заголовка, задав статическую таблицу полей заголовков и таблицу, динамически создаваемую в процессе фактической передачи, сократив большое количество избыточных полей заголовков HTTP в нескольких похожих запросах и внедрив кодирование Хаффмана для уменьшения длины строковых констант.

(2)мультиплексирование, используйте только одно TCP-соединение для передачи нескольких ресурсов, уменьшите количество TCP-соединений и введите метод, зависящий от приоритета, чтобы позволить высокоприоритетным ресурсам, таким как CSS, обрабатываться в первую очередь. Из-за большого количества параллелизма и одновременной передачи множества ресурсов, если скорость сети очень высока, это может привести к переполнению буферного пространства, поэтому вводится управление потоком, и две стороны контролируют отправку друг друга через размер окна.

(3)Server Push, чтобы решить проблему задержки триггера загрузки ресурсов при традиционной передаче HTTP. Когда браузер создает первый поток, служба сообщает браузеру, какие ресурсы могут быть загружены первыми, и браузер загружается заранее, а не ждет, пока он будет проанализирован.

Я не видел большого количества внутреннего использования HTTP/2. Taobao раньше включал HTTP/2, но я не знаю, почему он снова отключился.Есть много веб-сайтов, использующих HTTP/2 за границей, таких как Google Search. , CSS-Tricks, Twitter, Facebook и т. д. — все они используют HTTP/2. Если у вас есть собственный сайт, попробуйте.


Связанное чтение:

  1. Как обновить сайт до http/2
  2. Глядя на HTTPS из исходного кода Chrome
  3. См. HTTP из исходного кода Chrome