Some checks failed
Published content check / validate (push) Failing after 7s
Полный текст выпусков с анимированными SVG-схемами: «Письмо в сортировочном центре», «Что видит наблюдатель», «Читаем командную строку zapret2», «Где резать: метки», «multisplit: резать на части», «Перемешать: disorder», «Внутри программы». Список видео в Zapret2/Zapret2.md обновлён. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
1072 lines
190 KiB
Markdown
Executable file
1072 lines
190 KiB
Markdown
Executable file
---
|
||
title: "Видео 6. Перемешать: disorder"
|
||
date: 2026-10-25
|
||
tags:
|
||
- zapret2
|
||
- видео
|
||
- сценарий
|
||
- multidisorder
|
||
- disorder
|
||
- tcp
|
||
aliases:
|
||
- "Видео 6"
|
||
description: "Видео 6 серии о Zapret 2: как multidisorder отправляет части в обратном порядке и что происходит с сервером, с анимированными схемами."
|
||
---
|
||
|
||
> [!info] О чём заметка
|
||
> Это полный текст видео номер 6 из серии о том, как работает [[Zapret2/Zapret2|Zapret 2]], разбитый по сценам, с анимированными схемами. Каждую сцену можно читать отдельно.
|
||
|
||
> [!tip] Как читать пометки
|
||
> **«В нашем тесте»** значит настоящий запуск в лаборатории проекта: клиент, роутер, сервер. **«По коду»** значит, что мы прочитали исходники, но не запускали. **«По общим сведениям»** значит, что это вне нашего кода и лаборатории. О настоящем оборудовании ТСПУ мы ничего не утверждаем: наш «инспектор» это модель.
|
||
|
||
|
||
## Повтор и вопрос: страницы пришли не по порядку
|
||
|
||
Добро пожаловать в выпуск «Перемешать: disorder». Он про стратегию, которая отправляет куски запроса не по порядку. Сначала короткий повтор, чтобы эту часть можно было смотреть отдельно.
|
||
|
||
Запрос на сайт едет по сети пакетами. В каждом пакете записан порядковый номер: с какого байта сообщения начинаются его данные. По общим сведениям, по этим номерам получатель складывает сообщение обратно, даже если пакеты пришли вразнобой. Обратно получатель, по общим сведениям, присылает подтверждение: номер байта, которого он ждёт следующим. Так это выглядело в выпуске «Что видит наблюдатель».
|
||
|
||
Повтор про командную строку, выпуск «Читаем командную строку zapret2». Стратегия там записана как вызов Lua-функции: имя, двоеточие и аргументы через двоеточие, например pos равно midsld. Какие пакеты дойдут до вызова, решают условия профиля и лёгкие фильтры; один из них, фильтр вида данных, у нас пускает только клиентское приветствие. Сегодня мы заглянем внутрь одной такой функции: той, что режет приветствие и отправляет куски с конца.
|
||
|
||
Повтор про метки, выпуск «Где резать: метки». Место разреза это позиция: номер байта, перед которым режут, счёт с нуля. Вместо числа можно написать метку, слово, которое программа сама превращает в номер байта. Наш пример прежний: приветствие ClientHello на двести пятьдесят байт, в нём имя www.example.com с девяносто седьмого байта по сто одиннадцатый. Метка midsld, середина второго справа слова имени, указывает на байт сто четыре. Это граница между «www.exa» и «mple.com».
|
||
|
||
Выпуск «multisplit: резать на части» резал приветствие по такой метке на два куска, сто четыре байта и сто сорок шесть, и отправлял их по порядку. Игрушечный вахтёр из того выпуска, который смотрит в одно место каждого пакета, имя после этого не находил: оно разорвано пополам. Игрушечный: это наша модель, а не настоящее оборудование.
|
||
|
||
Теперь вопрос. А если отправить сначала конец, потом начало? Дойдёт ли сообщение целым? Что скажет получатель, когда пакет с более поздними байтами придёт раньше пакета с ранними?
|
||
|
||
![[wiki-anim-video06-s01-tape-and-question.svg|Лента, multisplit и вопрос]]
|
||
|
||
Договоримся о слове. Пакет «из будущего» это пакет, чьи байты лежат в сообщении правее того места, до которого получатель уже дочитал. Получатель ждёт другие байты, а эти пришли раньше времени, и ему надо решить, что с ними делать.
|
||
|
||
Игрушка, выдуманная. Письмо из трёх страниц, на каждой в углу номер. Первая страница: «Встретимся». Вторая: «у фонтана». Третья: «в шесть». Почтальон принёс их в порядке «три, один, два». Есть два писаря, и читают они по-разному. Первый читает страницы в порядке прихода и записывает: «в шесть Встретимся у фонтана». Второй сначала раскладывает страницы по номерам и записывает: «Встретимся у фонтана в шесть».
|
||
|
||
![[wiki-anim-video06-s01-three-pages.svg|Письмо из трёх страниц и два писаря]]
|
||
|
||
Какой из них похож на настоящего наблюдателя, мы не знаем. О настоящем ТСПУ мы ничего не утверждаем. Игрушечные читатели обоих видов будут в конце выпуска. А сейчас смотрим, что делает код zapret2 и что делает с таким порядком получатель.
|
||
|
||
План. В этой части: как стратегия режет и отправляет с конца, и что отвечает получатель. Дальше накладка, аргумент seqovl, знакомый по выпуску «multisplit: резать на части», потом большое приветствие и старая версия функции. Потом две новые стратегии: tcpseg, которая отправляет серверу один выбранный кусок сообщения, и drop, маленькая функция, которая выбрасывает пакет. И в конце игрушечные читатели. Как программа устроена внутри, здесь знать не нужно: о таких местах мы будем говорить одним предложением и называть выпуск, где они разобраны.
|
||
|
||
Договоримся о двух словах про программу, они встретятся много раз. Первое: «оригинал», он же «пакет ядра». Так мы зовём исходный пакет, который ядро Linux отдало программе через очередь: ядро держит его у себя и ждёт ответа, выбросить его или пропустить дальше. Второе: «движок». Это часть программы, написанная на Си: она берёт пакеты у ядра, зовёт функции стратегий на Lua, читает их ответы и возвращает ядру итоговый ответ, вердикт. Как она устроена, расскажет выпуск «Внутри программы»; сейчас нам хватит этого описания.
|
||
|
||
И два слова про лабораторию. «На проводе» значит: в записи, снятой на сетевой карте клиента, уже после программы; такую запись мы называли захватом. «Читатель» это любая из наших игрушечных моделей наблюдателя: правило в роутере, которое мы сравнивали с вахтёром, и модели, которые читают записанные пакеты. В прошлых выпусках такую модель называли игрушечным инспектором или проверяющим. Все они модели, которые мы написали сами. Настоящего оборудования среди них нет.
|
||
|
||
Итак, пакеты с номерами можно отправить не по порядку, и писари из игрушки читают такую пачку по-разному. Кто из них настоящий, нам неизвестно. Мы смотрим на код и на то, что делает с таким порядком получатель.
|
||
|
||
|
||
## Первый запуск: двести пятьдесят байт, длинный конец первым
|
||
|
||
Что получается на проводе, если резать приветствие по метке и отправлять с конца? Сначала на бумаге, потом в нашем опыте.
|
||
|
||
Игрушка, выдуманная. Лента из двухсот пятидесяти клеток, это наше приветствие. Клетки считаем с нуля. Режем перед клеткой сто четыре: слева полоска из ста четырёх клеток, справа полоска из ста сорока шести. Обычный разрез отправил бы левую полоску первой. Наша стратегия отправляет правую: длинный конец уезжает первым, начало вторым.
|
||
|
||
Договоримся о двух счётах. Считая клетки с нуля, получаем смещение: метка midsld стоит на смещении сто четыре. Считая с единицы, получаем номер: тот же байт имеет номер сто пять.
|
||
|
||
![[wiki-anim-video06-s02-strips-and-counts.svg|Две полоски и два счёта]]
|
||
|
||
Теперь настоящий опыт. Стратегия «резать и слать с конца» в коде называется multidisorder. Английское слово disorder значит «беспорядок», часть multi «много кусков», хотя в этом опыте их два. Аргумент pos равен midsld: режем по той же метке, что и в выпуске «multisplit: резать на части». В команде стоит ещё фильтр вида данных, та же настройка вызова, что в выпуске «Читаем командную строку zapret2»: он пускает в функцию только клиентское приветствие TLS.
|
||
|
||
Журнал нашего опыта говорит четыре вещи. Метка midsld превратилась в число сто четыре. Отправляется часть вторая, конец: сто сорок шесть байт, смещения со ста четырёх по двести сорок девятое. Потом часть первая, начало: сто четыре байта, смещения с нуля по сто третье. И последняя запись: пакет ядра выброшен. Журнал считает части с единицы, код с нуля.
|
||
|
||
Теперь провод у клиента, уже после программы. Первым ушёл пакет на сто сорок шесть байт, вторым пакет на сто четыре. Пакета ядра на двести пятьдесят байт на проводе нет.
|
||
|
||
Почему оригинала нет: программа вернула «выбросить», и ядро его уничтожило, а два новых пакета она отправила сама, в обход очереди.
|
||
|
||
Теперь номера, по одному. Начальный номер в каждом запуске другой, как говорилось в выпуске «Что видит наблюдатель». В нашем опыте настоящий порядковый номер первого байта приветствия равен трём миллиардам десяти миллионам четырёмстам двадцати трём тысячам восьмистам пятидесяти девяти. С такими числами неудобно, поэтому этот байт зовём «номер один» и считаем от него.
|
||
|
||
Кусок «начало» несёт байты с первого по сто четвёртый, его номер совпадает с номером оригинала. Кусок «конец» начинается на сто четыре байта дальше: это байт номер сто пять. Чтобы получить его настоящий номер, прибавляем сто четыре к номеру первого байта приветствия: три миллиарда десять миллионов четыреста двадцать три тысячи девятьсот шестьдесят три.
|
||
|
||
Начало с первого по сто четвёртый, конец со ста пятого по двести пятидесятый. Сто четыре плюс сто сорок шесть равно двести пятьдесят: ни дыры, ни перекрытия.
|
||
|
||
![[wiki-anim-video06-s02-log-and-wire.svg|Журнал и провод первого запуска]]
|
||
|
||
Две мелочи. Первая: у каждого IPv4-пакета в заголовке есть номер-идентификатор, IP id. По умолчанию первому отправленному пакету программа оставляет идентификатор оригинала, каждому следующему даёт на единицу больше. Поэтому они растут в порядке отправки, а не байтов: у конца в нашем опыте пятьдесят тысяч сто тридцать шесть, у начала на один больше.
|
||
|
||
Вторая: флаги пакета копируются с обработанного пакета. У нашего приветствия это PSH, «протолкнуть», и ACK, и оба куска уходят с ними.
|
||
|
||
Проверь себя, на бумаге, по коду. Метка равна ста: режем по смещению сто, считая с нуля. Какие два пакета уйдут, в каком порядке и с какими номерами?
|
||
|
||
Сначала конец: сто пятьдесят байт, начиная с байта номер сто один. Потом начало: сто байт, начиная с байта номер один. Сто пятьдесят плюс сто равно двести пятьдесят, дыры нет.
|
||
|
||
Итак, приветствие ушло двумя пакетами: сначала длинный конец, потом начало. Номера не пересекаются и не оставляют дыры. Пакет ядра на провод не вышел. Идентификаторы IP id идут в порядке отправки, а не байтов.
|
||
|
||
|
||
## Код: ворота, позиции и цикл, идущий с конца
|
||
|
||
Как из строки midsld получаются куски и почему они уходят с конца? Посмотрим на функцию в коде. Программа вызывает её с двумя аргументами: ключом ctx и таблицей с описанием пакета. Начало почти такое же, как у multisplit, поэтому пойдём быстро.
|
||
|
||
Ворота функции мы разбирали в выпуске «multisplit: резать на части», здесь только повтор. Четыре вопроса. Есть ли в пакете TCP? Есть ли данные для разреза: берётся аргумент blob, иначе собранное сообщение (так называется приветствие, склеенное из нескольких пакетов; выпуск «Сборка и повторный проигрыш»), иначе сам пакет? Направление то, что нужно, по умолчанию исходящее? Тип нагрузки известный? Хоть одно «нет», и пакет идёт дальше как был. Для нашего приветствия в одном пакете данные это те же двести пятьдесят байт.
|
||
|
||
Заодно функция делает отсечку: просит движок больше не звать этот вызов для входящих пакетов соединения. В журнале нашего опыта это видно по строке «in равно один, out равно ноль»: отсечка есть для входящих и нет для исходящих. Как устроены отсечки, объясняет выпуск «Lua внутри программы».
|
||
|
||
До функции стоят ещё два лёгких фильтра, их мы читали в выпуске «Читаем командную строку zapret2»: диапазон пакетов и фильтр вида данных, он же тип нагрузки. Для входящих диапазон по умолчанию пуст, и функцию для них не зовут вообще. В нашем опыте пропускается только клиентское приветствие TLS, поэтому пустое подтверждение клиента останавливает фильтр ещё до Lua, и в журнале так и написано. Как программа определяет тип нагрузки, расскажет выпуск «Тип нагрузки и имя сайта».
|
||
|
||
Дальше позиции, и тут всё знакомо. Строку pos переводит в числа тот же переводчик, что в выпуске «Где резать: метки». Мост к Lua прибавляет единицу: midsld даёт позицию сто пять, а в журнале стоит сто четыре. Если pos не написан, берётся строка «два». Вычёркиватель единицы, delete_pos_1, убирает позицию «единица», разрез перед самым началом; если список опустел, функция пишет «нет допустимых позиций» и ничего не отправляет.
|
||
|
||
![[wiki-anim-video06-s03-gates-to-positions.svg|Ворота и позиции]]
|
||
|
||
Ещё строка про проигрыш, replay_first. Проигрыш это повторная выдача пакетов, которые программа удерживала. Зачем она их удерживает, будет видно на большом приветствии в следующей части, а устроен проигрыш в выпуске «Сборка и повторный проигрыш». Для приветствия в одном пакете строка всегда разрешает работать дальше.
|
||
|
||
Новое здесь одно: цикл. Игрушка, выдуманная: линейка с засечками, одна засечка даёт две части, две дают три. Режем по всем засечкам, а передаём с последнего отрезка к первому.
|
||
|
||
Цикл идёт в общем отправителе частей, multidisorder_send, его вызывает стратегия: для i от числа позиций вниз до нуля, шаг минус один. В multisplit он шёл от нуля вверх, здесь с конца. Это весь беспорядок: больше куски ничего не переставляет. Каждое значение i это кусок, в коде они нумеруются с нуля; в журнале номер на единицу больше, и кусок с i равным единице печатается там как «часть два».
|
||
|
||
Начало куска это позиция с номером i, а у куска ноль позиции нет, и начало это первый байт. Конец куска на единицу меньше следующей позиции, а у последнего куска это конец данных. Байты вырезает функция Lua string.sub.
|
||
|
||
Применим к позиции сто пять. Сначала i равно единице: начало сто пять, следующей позиции нет, конец двести пятьдесят. Это сто сорок шесть байт. Потом i равно нулю: начало один, конец сто четыре. Это сто четыре байта.
|
||
|
||
![[wiki-anim-video06-s03-loop-from-end.svg|Цикл с конца]]
|
||
|
||
Каждый кусок отправляет знакомый нам «отправщик части», в коде rawsend_payload_segmented: он берёт заголовки оригинала, подставляет кусок и сдвигает номер. Сдвиг равен началу куска минус один, и ещё вычитается длина перекрытия: ею займёмся в следующей части, сейчас она ноль. У конца сдвиг сто четыре, у начала ноль: те самые номера с провода.
|
||
|
||
После цикла функция обычно возвращает «выбросить»: пакет ядра на провод не пойдёт. Если отправка куска не удалась, возвращается «пропустить»: ушедшие куски остаются на проводе, оригинал уходит следом.
|
||
|
||
Проверь себя, на бумаге. Если бы позиций было три, с какого значения i начался бы цикл, и каким по счёту ушёл бы кусок «начало»?
|
||
|
||
Цикл начался бы с трёх: кусков четыре, с нулевого по третий. Кусок «начало» нулевой, и он ушёл бы четвёртым, последним.
|
||
|
||
Итак, метки превращаются в позиции так же, как раньше, а новое здесь цикл от последнего куска к первому: каждый кусок уходит со своим сдвигом номера. В конце обычно возвращается «выбросить». Столько позиций, столько кусков плюс один.
|
||
|
||
|
||
## Проверь себя: пять кусков, ноль против единицы, значение по умолчанию
|
||
|
||
Закрепим на опытах: пять кусков и значение по умолчанию.
|
||
|
||
Проверь себя. Метки: два, host, midsld, endhost: число два, начало имени, середина и конец. Это смещения два, девяносто семь, сто четыре и сто двенадцать, считая с нуля. Сколько получится кусков?
|
||
|
||
Четыре позиции, пять кусков.
|
||
|
||
Какого размера самый первый кусок по порядку приветствия и какого самый последний?
|
||
|
||
Первый два байта, последний сто тридцать восемь. Все пять по порядку приветствия: два, девяносто пять, семь, восемь и сто тридцать восемь. Сумма двести пятьдесят.
|
||
|
||
Уйдут они с конца, как на экране: сто тридцать восемь байт со смещения сто двенадцать, восемь со ста четырёх, семь с девяноста семи, девяносто пять со второго, и последними два с нуля. Всё это совпало с нашим тестом. Кусок на семь байт начинается с «www.exa», кусок на восемь с «mple.com»: имя разорвано, и две части едут в обратном порядке.
|
||
|
||
Ещё проверка, на бумаге. Метки host и endhost, то есть смещения девяносто семь и сто двенадцать. Какие получатся куски, и чем это плохо для замысла?
|
||
|
||
Три куска: девяносто семь байт, потом пятнадцать байт имени, потом сто тридцать восемь. Уйдут в порядке: сто тридцать восемь, пятнадцать, девяносто семь. Кусок в пятнадцать байт это само имя целиком, и наша игрушечная модель, читающая пакеты по одному, найдёт его в этом пакете. Вывод: чтобы разорвать имя для такого читателя, разрез должен попасть внутрь имени, как midsld. Такого опыта в проекте нет, это расчёт по формуле цикла.
|
||
|
||
![[wiki-anim-video06-s04-five-parts.svg|Пять кусков и порядок отправки]]
|
||
|
||
Теперь про первый байт. Про ноль и единицу мы говорили в выпуске «Где резать: метки», здесь только как это выглядит у нашей стратегии. Аргумент pos равен нулю: в списке Lua это позиция «единица», её вычёркивают, разреза нет. Метка за пределами данных, например девятьсот девяносто девять, тоже выпадает. В обоих случаях на проводе один пакет на двести пятьдесят байт, оригинал прошёл как был. Аргумент pos равен единице: настоящий разрез после одного байта. В нашем тесте с метками один и midsld ушли три пакета: сто сорок шесть байт со смещения сто четыре, сто три со смещения один и один байт с нуля.
|
||
|
||
Третий случай: pos не написан совсем. Тогда подставляется «два». Первый кусок два байта, второй двести сорок восемь, и в нашем тесте они ушли в таком порядке: сначала длинный, потом короткий. Имя целиком остаётся внутри длинного куска. Против игрушечного читателя «по одному пакету» оно остаётся видно. Значит, по умолчанию стратегия режет, но имя остаётся целым: против такого читателя этого мало.
|
||
|
||
В нашем тесте на HTTP-запросе в восемьдесят один байт с меткой method, началом метода запроса, плюс два ушли пакеты на семьдесят девять и на два байта: сначала длинный конец, потом начало, две первые буквы слова GET.
|
||
|
||
![[wiki-anim-video06-s04-zero-one-default.svg|Ноль, единица и значение по умолчанию]]
|
||
|
||
Итак, куски идут с конца, и кусков на один больше, чем разрезов. Пять кусков уходят в порядке: сто тридцать восемь, восемь, семь, девяносто пять и два. Аргумент pos равен нулю: не разрез. Равен единице: настоящий разрез. Значение по умолчанию два оставляет имя целым.
|
||
|
||
|
||
## Получатель: что отвечает сервер на кусок из будущего
|
||
|
||
Кусок из будущего пришёл раньше. Что делает сервер? Ответ даёт, по общим сведениям, не zapret2, а сетевой стек сервера. Посмотрим, что он сказал в нашем тесте.
|
||
|
||
Игрушка, выдуманная. Склад принимает длинное письмо, и каждая страница несёт буквы со своими номерами. У кладовщика ящик и один вопрос: какая буква нужна следующей? На стол ложится страница с буквами от ста пятой до двухсот пятидесятой. Первых букв нет, и кладовщик говорит: «Жду первую». Страницу он не выбрасывает, а кладёт в отдельную коробку и добавляет: «Кроме того, у меня лежат буквы от ста пятой до двухсот пятидесятой».
|
||
|
||
Первая фраза называется ACK, «подтверждение». По общим сведениям, число в ней это номер следующего байта, которого получатель ждёт. Подтверждение с числом «один» значит: жду самый первый байт, по порядку ничего не получено.
|
||
|
||
Вторая фраза называется SACK, «выборочное подтверждение». Это блок «у меня лежат байты от такого-то до такого-то». Блок ничего не подтверждает, он только сообщает, что лежит в стороне.
|
||
|
||
По общим сведениям, это расширение TCP, и оба конца разрешают его в начале соединения. В нашем рукопожатии оба конца его разрешили.
|
||
|
||
На экране блок записан двумя числами: с какого байта лежат данные и номер первого байта после них. Поэтому правая граница на единицу больше последнего байта: запись «сто пять, двести пятьдесят один» значит байты со ста пятого по двести пятидесятый. В озвучке говорим словами.
|
||
|
||
![[wiki-anim-video06-s05-clerk-ack-sack.svg|Кладовщик, ACK и SACK]]
|
||
|
||
Числа в ответах пересчитаны от первого байта приветствия, как в сцене про первый запуск.
|
||
|
||
Теперь опыт. Первым на сервер пришёл пакет «конец», байты со ста пятого по двести пятидесятый. Сервер ответил: ACK один и блок от ста пятого до двести пятидесятого. Читаем: по порядку ничего нет, жду первый байт, а в стороне лежит конец.
|
||
|
||
Затем пришёл пакет «начало», байты с первого по сто четвёртый. Вместе с лежавшими байтами они закрывают всё: приходит ACK двести пятьдесят один и никакого блока. Число двести пятьдесят один значит: жду байт двести пятьдесят один, всё до двести пятидесятого у меня есть. Следом сервер отправил свои четырнадцать байт ответа.
|
||
|
||
Для сравнения два опыта без перемешивания. Если пакет не трогать, ответ один: ACK двести пятьдесят один. Если резать multisplit по той же метке и отправлять по порядку, после первого куска приходит ACK сто пять и блока нет: ничего из будущего в стороне не лежит.
|
||
|
||
![[wiki-anim-video06-s05-server-answers.svg|Ответы сервера на куски]]
|
||
|
||
Проверь себя. Допустим, на сервер пришло только «начало», байты с первого по сто четвёртый. Что скажет сервер, и будет ли блок?
|
||
|
||
ACK сто пять, блока нет. Всё до ста четвёртого байта получено по порядку, и сервер ждёт сто пятый.
|
||
|
||
Помните: измерен один получатель, сервер лаборатории на Linux, ядро версии шесть точка восемнадцать. У других систем ответы могут отличаться, мы не проверяли.
|
||
|
||
Итак, сервер отвечает на каждый пакет из будущего подтверждением «жду первый» и блоком «у меня лежат такие-то байты». Когда приходит начало, подтверждение одним скачком доходит до конца. Это в нашем тесте, на одном получателе.
|
||
|
||
|
||
## Пять кусков: блок растёт влево, приложению хватает одного чтения
|
||
|
||
Пять кусков вместо двух. Что сервер говорит после каждого и что в итоге видит приложение?
|
||
|
||
Игрушка, выдуманная. На складе в коробке «лежит» пачка страниц с буквами от ста тринадцатой до двухсот пятидесятой. Теперь на стол кладут страницы с буквами от ста пятой до ста двенадцатой. Они встают вплотную слева. Кладовщик не заводит вторую коробку, пачка просто становится длиннее.
|
||
|
||
Опыт с пятью кусками, метки два, host, midsld, endhost. Первым ушёл самый последний кусок: сто тридцать восемь байт, со ста тринадцатого по двести пятидесятый. Ответ: ACK один и блок от ста тринадцатого до двести пятидесятого.
|
||
|
||
Второй пакет: восемь байт, со ста пятого по сто двенадцатый. Они стыкуются слева с тем, что уже лежит. Ответ тот же ACK один, а блок теперь один, от ста пятого до двести пятидесятого: соседние диапазоны слились.
|
||
|
||
Проверим слияние арифметикой. Лежали байты со ста тринадцатого, пришли байты по сто двенадцатый включительно. Последний байт нового диапазона на единицу меньше первого байта лежащего: сто двенадцать и сто тринадцать. Значит, получается один диапазон от ста пятого до двести пятидесятого.
|
||
|
||
Проверь себя. Третий пакет: семь байт, с девяносто восьмого по сто четвёртый. ACK один, а каким будет блок?
|
||
|
||
Опять один блок, от девяносто восьмого до двести пятидесятого. Семь байт встали вплотную слева.
|
||
|
||
Четвёртый пакет: девяносто пять байт, с третьего по девяносто седьмой. Блок от третьего до двести пятидесятого. Не хватает двух первых байт. Пятый пакет как раз два байта, с первого по второй. Они закрывают дыру, и приходит ACK двести пятьдесят один без блока.
|
||
|
||
Итого: четыре ответа «жду первый» с блоком, растущим влево, а потом ACK на весь запрос. Это в нашем тесте, один получатель.
|
||
|
||
![[wiki-anim-video06-s06-block-grows-left.svg|Пять частей: блок растёт влево]]
|
||
|
||
Проверь себя на втором опыте, с метками один и midsld. Первым уходит кусок в сто сорок шесть байт со ста пятого, вторым сто три байта со второго, третьим один байт с первого. Какой блок назовёт сервер после второго куска?
|
||
|
||
Блок от второго до двести пятидесятого: сто три байта встали вплотную слева. Первым ответом был блок от ста пятого, а после третьего куска придёт ACK двести пятьдесят один.
|
||
|
||
Скажем, что здесь значит «приложение». В лаборатории на сервере работает маленькая принимающая программа. Она читает всё, что ядро сервера ей отдало, и записывает, сколько байт пришло и скольки чтениями. «Одно чтение» значит, что ей отдали все байты разом. «Два чтения» значит, что ей отдали байты двумя порциями, например потому, что она проснулась, когда пришла только часть. По общим сведениям, число чтений зависит от того, когда она проснулась, а протокол границ чтений не обещает.
|
||
|
||
А что видит приложение на сервере? В нашем тесте с двумя кусками оно прочитало за одно чтение двести пятьдесят байт, и все они совпали с отправленными.
|
||
|
||
В опыте с multisplit было иначе: первые сто четыре байта уже подтверждены по порядку, и приложение получило их отдельным чтением, а потом ещё сто сорок шесть. Почему чтений два, мы не выясняли.
|
||
|
||
А при перемешивании начала ещё нет: первые байты приходят последними. По общим сведениям, сетевой стек держит байты из будущего у себя и отдаёт приложению, когда закрылась дыра: это обычное поведение приёмника TCP. Другие системы мы не проверяли.
|
||
|
||
![[wiki-anim-video06-s06-app-reads.svg|Второй опыт и что читает приложение]]
|
||
|
||
Итак, сервер отвечает на каждый кусок из будущего одним и тем же «жду первый», соседние диапазоны склеивает в один блок и подтверждает весь запрос одним ответом, когда приходит начало. Приложение читает запрос за одно чтение, а не пятью.
|
||
|
||
|
||
## Пакет ядра и nodrop: дубликат и двойной отчёт
|
||
|
||
Что если пакет ядра не выбрасывать? У стратегии есть для этого флаг nodrop, «не выбрасывать».
|
||
|
||
Посмотрим на последние строки функции. Без nodrop она возвращает «выбросить», с ним «пропустить»: оригинал уходит дальше. Куски к этому моменту уже на проводе, и оригинал идёт следом за ними.
|
||
|
||
Почему оригинал идёт именно следом. Куски отправляются внутри вызова функции, один за другим, а пакет ядра выпускается или уничтожается только после того, как отработали все функции профиля. Поэтому в нашем опыте оригинал оказывается после кусков, а не между ними.
|
||
|
||
Игрушка, выдуманная. Письмо ушло отрывками, а потом приехало ещё и целиком. Кладовщик отвечает: «Эти страницы у меня уже были» и называет весь диапазон.
|
||
|
||
Писарю, который раскладывает страницы по номерам, копия не страшна: он видит, что страницы уже есть. Писарь, который клеит их в порядке прихода, получит копию в конце текста. Это игрушка: на какого из двух похож настоящий читатель, мы не знаем.
|
||
|
||
![[wiki-anim-video06-s07-return-verdict.svg|Вердикт пакета ядра и nodrop]]
|
||
|
||
Теперь опыт с nodrop. На проводе три пакета. Сначала конец на сто сорок шесть байт, потом начало на сто четыре, потом оригинал, все двести пятьдесят байт, с первого по двести пятидесятый. И у оригинала идентификатор IP id такой же, как у первого куска. Вы помните правило из сцены про первый запуск: первый отправленный пакет берёт идентификатор оригинала.
|
||
|
||
Что ответил сервер. На первый кусок: ACK один и блок от ста пятого до двести пятидесятого. На второй: ACK двести пятьдесят один. А на оригинал пришёл снова ACK двести пятьдесят один, и в нём есть блок.
|
||
|
||
Проверь себя. Оригинал пришёл последним, после обоих кусков. Какой диапазон назовёт этот блок?
|
||
|
||
Весь, от первого до двести пятидесятого байта. Эти байты у сервера уже были: к этому моменту у него есть все двести пятьдесят, и вся копия оказывается лишней.
|
||
|
||
В журнале последняя запись о пакете ядра теперь не «выброшен», а «пропущен без изменений».
|
||
|
||
Так выглядит дубликат, а блок в таком ответе называется «дублирующий SACK». По общим сведениям, он описан в стандарте RFC две тысячи восемьсот восемьдесят три и бывает двух видов: блок лежит ниже подтверждения, или блок лежит внутри другого блока того же ответа. И там и там это байты, которые пришли дважды. Здесь первый вид. В выпуске «multisplit: резать на части» мы называли его дублирующим блоком. Цена nodrop: лишний пакет и отчёт о нём.
|
||
|
||
То же было у multisplit. Здесь отличие одно: куски стоят в обратном порядке, а оригинал по-прежнему приходит последним.
|
||
|
||
Ещё проверка на бумаге. Пять кусков и nodrop. Сколько пакетов с данными уйдёт с клиента?
|
||
|
||
Шесть: пять кусков и оригинал. Так следует из кода, опыта с пятью кусками и nodrop в проекте нет.
|
||
|
||
![[wiki-anim-video06-s07-nodrop-wire.svg|nodrop: три пакета и дубликат]]
|
||
|
||
Итак, обычно функция возвращает «выбросить», и оригинал на провод не выходит. С nodrop она возвращает «пропустить», и оригинал идёт следом за кусками. Сервер отвечает на него дублирующим SACK, блоком с уже полученными байтами, и приложение всё равно видит запрос один раз.
|
||
|
||
|
||
## Часть, закрывающая дыру, потеряна
|
||
|
||
Первый кусок приходит последним. А что, если пакет, который закрывает дыру, потерялся по дороге?
|
||
|
||
Игрушка, выдуманная. Почтальон потерял первую страницу. Кладовщик говорит: «Жду первую». Остальные страницы лежат в коробке, читателю он не отдаёт ничего. Потом приходит повторное письмо, и почтальон снова кладёт сверху конец.
|
||
|
||
Теперь опыт. В нашей лаборатории на роутере стоит правило: выбросить один пакет длиной сто пятьдесят шесть байт. Это наш кусок «начало»: сто четыре байта данных и пятьдесят два байта заголовков, двадцать IP и тридцать два TCP. Правило стоит на входе роутера, программа о нём не знает, и придумано оно для опыта: это не свойство техники.
|
||
|
||
На проводе у клиента сначала ушёл конец на сто сорок шесть байт. Потом начало на сто четыре: оно ушло и пропало на роутере. Сервер ответил: ACK один и блок от ста пятого до двести пятидесятого. Он ждёт первый байт, приложению не отдано ничего.
|
||
|
||
Где лежат следы. На роутере мы снимаем пакеты у входа и у выхода. Потерянный пакет виден у входа и исчез у выхода, на сервере его тоже нет. Счётчик правила показал один пакет в сто пятьдесят шесть байт. Так мы убеждаемся, что пакет не дошёл, а не просто не записался.
|
||
|
||
![[wiki-anim-video06-s08-lab-loss-rule.svg|Потерянный пакет в лаборатории]]
|
||
|
||
Пока длится ожидание, приложение не получает ничего, даже конца: по общим сведениям, он лежит в стеке, как страницы в коробке кладовщика.
|
||
|
||
Через время, в нашем тесте около двухсот миллисекунд, ушла вторая пара: снова конец на сто сорок шесть байт, снова начало на сто четыре. Это первая повторная отправка ядра клиента нашей лаборатории, а не постоянная величина.
|
||
|
||
Проверь себя. Ядро клиента отправило приветствие второй раз, и снова все двести пятьдесят байт, а не только потерянный кусок. Пройдёт ли оно через нашу стратегию?
|
||
|
||
Да. Повторная отправка это новый исходящий пакет, а исходящие пакеты с приветствием функция получает, пока её для них не отсекли. Здесь не отсекли: стратегия снова режет приветствие и снова отправляет с конца. В журнале функция сработала дважды, и дважды пакет ядра выброшен.
|
||
|
||
Что ответил сервер. На вторую пару пришёл ACK один с двумя одинаковыми блоками: от ста пятого до двести пятидесятого, и второй такой же. Это дублирующий SACK второго вида, из прошлой сцены: блок лежит внутри другого блока, здесь они совпали; эти байты пришли дважды. Потом «начало» дошло, и пришёл ACK двести пятьдесят один. Приложение получило свои двести пятьдесят байт одним чтением, но только после повтора.
|
||
|
||
Почему ядро клиента отправило снова все двести пятьдесят байт, а не только дыру, мы не выясняли. Наблюдение точное, а объяснения мы не заявляем.
|
||
|
||
![[wiki-anim-video06-s08-lost-retransmit.svg|Потерян кусок с началом: повтор]]
|
||
|
||
Заметьте, какой кусок потерян. Начало стоит последним в очереди отправки, и именно оно закрывает дыру: так устроена стратегия. Что было бы, если бы потерялся конец, мы не проверяли.
|
||
|
||
Итак, если потерян пакет, закрывающий дыру, приложение ждёт повторной отправки. Повторная отправка опять проходит через стратегию и опять перемешивается. Время ожидания в нашем тесте это время ядра клиента, а не свойство стратегии.
|
||
|
||
|
||
## Что мы знаем: ответы получателя одной таблицей
|
||
|
||
Соберём знания первой трети выпуска. На экране карточка стратегии, окна загораются по одному.
|
||
|
||
Ворота. Функция работает на исходящих пакетах с данными известного типа, входящие до неё не доходят. Позиции. Метки превращаются в позиции, позицию «единица», то есть самое начало сообщения, вычёркивают, пустой список значит «ничего не делать». Цикл. В коде куски нумеруются с нуля, отправляются с последнего к первому, каждый со своим сдвигом номера. Вердикт. Обычно «выбросить»: пакет ядра на провод не выходит; с nodrop он идёт следом.
|
||
|
||
Теперь таблица ответов получателя. Мы читаем её по строкам, шесть опытов. Пакет без изменений: ACK двести пятьдесят один, одно чтение. Это точка отсчёта. multisplit по порядку: ACK сто пять, блока нет, приложение читает двумя чтениями, сто четыре и сто сорок шесть.
|
||
|
||
multidisorder на два куска: ACK один с блоком от ста пятого, потом ACK двести пятьдесят один, и приложение читает за одно чтение. Пять кусков: четыре ответа с одним и тем же «жду первый» и блоком, растущим влево, потом ACK на всё.
|
||
|
||
С nodrop в конце приходит ещё и дублирующий SACK, а приложение получает запрос один раз. Если потерялось начало, приложение ждёт повторной отправки, и она снова перемешивается.
|
||
|
||
Проверь себя по таблице. В какой строке приложение получило запрос позже всех? И в какой читало его двумя чтениями?
|
||
|
||
Позже всех там, где потерялось начало: пришлось ждать повторной отправки. Двумя чтениями там, где куски шли по порядку, в строке с multisplit.
|
||
|
||
![[wiki-anim-video06-s09-answers-table.svg|Ответы получателя одной таблицей]]
|
||
|
||
Теперь границы. Всё это измерено на одном получателе, сервере нашей лаборатории на Linux, ядро версии шесть точка восемнадцать. Для Windows, BSD и других систем мы этого не знаем. Определения ACK и SACK взяты по общим сведениям. Настоящее оборудование, которое могло бы стоять на пути, мы вообще не измеряли: наши писари из игрушки выдуманы.
|
||
|
||
Числа этой части. Двести пятьдесят байт приветствия, сто четыре по метке midsld, куски сто сорок шесть и сто четыре. Пять кусков: сто тридцать восемь, восемь, семь, девяносто пять и два.
|
||
|
||
И слова. ACK: следующий байт, которого ждёт получатель. SACK: блок «у меня лежат такие-то байты». Дублирующий SACK: блок с уже полученными байтами, раньше мы называли его дублирующим блоком. nodrop: не выбрасывать оригинал. Аргумент pos равен нулю: это не разрез.
|
||
|
||
К следующей части три вопроса. Что будет, если куски перекрываются. Что будет с приветствием из нескольких пакетов. И чем старая версия этой функции отличается от новой.
|
||
|
||
Что дальше. В кусок, который начинается с первого разреза, у нас это конец, можно вписать «черновик»: несколько лишних байт перед настоящими, они наложатся на хвост начала. Сколько их, задаёт метка левее разреза. В нашем тесте приложение сервера получило настоящие байты. Подробности в следующей части.
|
||
|
||
![[wiki-anim-video06-s09-numbers-words-next.svg|Числа, слова и мост к накладке]]
|
||
|
||
Итак, функция режет по меткам и отправляет куски с конца, пакет ядра выбрасывается, а получатель в нашем тесте отвечает блоками и в конце подтверждает весь запрос. Приложение читает запрос целиком. Это один получатель, о настоящем ТСПУ мы ничего не знаем.
|
||
|
||
|
||
## Повтор и идея: seqovl в multidisorder это метка, а накладки лежат внутри сообщения
|
||
|
||
Это середина выпуска «Перемешать: disorder», и её можно смотреть отдельно от остальных. Начнём с короткого повтора.
|
||
|
||
Сразу оговорка. Настоящего ТСПУ в нашей лаборатории нет, и о настоящем оборудовании мы ничего не знаем: всё, что мы скажем про читателей трафика, относится к игрушечным моделям проекта, и приём «работает» только против такой игрушки.
|
||
|
||
Повтор. Стратегия multidisorder режет приветствие на части в указанных местах и отправляет их с последней до первой. Наше приветствие в двести пятьдесят байт она режет на позиции сто четыре: сначала на провод уходит часть в сто сорок шесть байт, потом левая часть в сто четыре байта. Оригинал из ядра выбрасывается.
|
||
|
||
Сервер нашей лаборатории, Linux, отвечает на первый пакет: первый байт я всё ещё жду, но эти номера уже держу. Это блок выборочного подтверждения, по-английски SACK. Когда приходит начало, сервер подтверждает всё, и приложение получает все двести пятьдесят байт одним чтением.
|
||
|
||
Вопрос сцены: что делает у функции multidisorder аргумент seqovl? Пойдём медленно.
|
||
|
||
Сначала игрушка, пример выдуманный. Письмо лежит на пронумерованных страницах, и читатель складывает их по номерам. Мы отправляем страницу два раньше страницы один. Но в начало страницы два мы приписываем четыре черновые строки и нумеруем их как последние четыре строки страницы один.
|
||
|
||
Черновые строки не сдвигают текст и не добавляют новых мест. На четыре номера строк теперь два варианта: черновой, пришедший первым, и настоящий, который придёт позже. Какому поверит читатель, зависит от самого читателя.
|
||
|
||
Эти черновые байты мы зовём накладкой, как в выпуске «multisplit: резать на части»; там их называли ещё подкладкой, это одно и то же, а число их задаёт аргумент seqovl. В коде имя нигде не расшифровано; по-видимому, оно от английского «сиквенс оверлэп», то есть «наложение порядковых номеров».
|
||
|
||
Но в multidisorder seqovl это не число байт, а метка позиции, как в списке разрезов, и метка одна, не список. Число накладок равно смещению этой метки от начала сообщения. Сама метка не показывает, где лежит накладка: она задаёт только длину.
|
||
|
||
![[wiki-anim-video06-s10-recap-toy-marker.svg|Повтор и идея: seqovl это метка]]
|
||
|
||
Теперь код. Работает общий отправитель частей, функция multidisorder_send; её цикл мы читали в начале выпуска, сегодня нужен только блок про накладку, в нём пять шагов.
|
||
|
||
Первый шаг, условие. В коде части считаются с нуля: левая часть, с начала сообщения, это часть ноль, а вторая, которая начинается с первого разреза, это часть один. Дальше говорим «левая» и «вторая». Накладку получает только вторая, и только если seqovl задан.
|
||
|
||
Второй шаг, проверка. Если метка не левее первого разреза, накладка отменяется, а в журнал идёт строка об этом. Правило со смещениями с нуля: метка строго меньше первого разреза, поэтому накладка всегда ложится внутрь левой части.
|
||
|
||
Третий шаг, счёт. Число накладок равно Lua-значению метки минус один. Lua-значение это смещение плюс один, потому что в Lua позиции считаются с единицы. Метка на смещении ноль даёт ноль накладок, метка на смещении семь даёт семь.
|
||
|
||
Четвёртый шаг, узор, то есть содержимое накладки. По умолчанию это один нулевой байт. Помощник с именем pattern размножает узор и обрезает до нужной длины. Результат ставится перед настоящими байтами второй части: сначала накладка, потом то, что часть и так несёт.
|
||
|
||
Пятый шаг, адрес пакета. Часть отправляется со сдвигом: смещение её начала минус число накладок. С накладкой пакет начинается раньше на столько байт, сколько накладок, и накрывает собой хвост левой части.
|
||
|
||
Проверь себя. Первый разрез на смещении двенадцать, метка на смещении пять. Сколько накладок и с какого смещения начнётся пакет?
|
||
|
||
Пять накладок, а пакет начнётся на смещении семь, потому что двенадцать минус пять равно семи. Это арифметика по формуле, а не запуск.
|
||
|
||
![[wiki-anim-video06-s10-seqovl-code-steps.svg|Пять шагов накладки в коде]]
|
||
|
||
Итак. В multidisorder аргумент seqovl это метка, а не число, и накладок столько, на сколько байт метка отстоит от начала. Накладка ложится только перед второй частью, той, что начинается с первого разреза, и метка должна стоять левее этого разреза. Пакет с накладкой начинается раньше своих настоящих байт и накрывает хвост левой части.
|
||
|
||
|
||
## Три числа на проводе: seqovl равно единице, host и midsld минус один
|
||
|
||
Вопрос сцены: как накладка выглядит на проводе нашего клиента, если взять метки из нашего приветствия? Возьмём три значения seqovl и для каждого посмотрим, с какого байта начинается пакет и сколько в нём байт.
|
||
|
||
Исходная картина из начала выпуска: приветствие в двести пятьдесят байт, разрез на сто четвёртом смещении. Слово example разорвано: левая часть кончается буквами «www.exa», вторая начинается с «mple.com». Без накладки уходят сто сорок шесть байт со смещения сто четыре, а следом сто четыре байта с начала.
|
||
|
||
Первый опыт: seqovl равно единице. Это метка со смещением один, значит, накладка в один байт. В журнале строка про вторую часть: длина сто сорок семь, seqovl один. Сто сорок семь это один плюс сто сорок шесть.
|
||
|
||
Пакет начинается на смещении сто четыре минус один, то есть сто три. Это то место, где в настоящем сообщении стоит последняя буква левой части, буква а, латинская. В нашем тесте первый байт пакета нулевой, а за ним идут эм, пэ, эл, е. Вторым, как и раньше, уходит левая часть в сто четыре байта.
|
||
|
||
Договоримся о счёте. Смещение считается с нуля, а номер байта в потоке с единицы, как в выпуске «Что видит наблюдатель». Значит, смещение сто три это номер сто четыре.
|
||
|
||
На первый пакет сервер отвечает: подтвердить пока нечего, но номера со сто четвёртого до конца у меня есть. Когда приходит левая часть, сервер подтверждает всё и отдельным блоком называет номер сто четвёртый: он получил его дважды. Это дублирующий SACK, мы уже видели его при nodrop.
|
||
|
||
Второй опыт: seqovl равно host. Метка host стоит на смещении девяносто семь, это начало имени сайта. Значит, накладок девяносто семь, а пакет длиной двести сорок три байта: девяносто семь плюс сто сорок шесть. Начинается он на смещении сто четыре минус девяносто семь, то есть семь. В нашем тесте первые байты пакета нулевые.
|
||
|
||
Сервер сначала называет блок с восьмого номера до конца, а в итоговом подтверждении дублирующий блок с восьмого по сто четвёртый номер. Это девяносто семь номеров, ровно столько, сколько накладок.
|
||
|
||
Третий опыт: метка midsld минус один, смещение сто три. Это самое большое допустимое значение. Накладок сто три, пакет длиной двести сорок девять байт, начало на смещении один. Дублирующий блок идёт со второго номера по сто четвёртый.
|
||
|
||
![[wiki-anim-video06-s11-three-wire-packets.svg|Три накладки на проводе клиента]]
|
||
|
||
Сведём всё в таблицу. Длина пакета всегда сто сорок шесть плюс накладки. Начало всегда сто четыре минус накладки. Значит, начало и число накладок в сумме всегда дают сто четыре, разрез. Метка строго левее разреза, поэтому начало не ниже единицы, и пакет никогда не выходит левее первого байта сообщения.
|
||
|
||
Ещё одно слово: хэш. Это короткий отпечаток данных: принимающая программа считает его от всех полученных байт. Если он получился таким же, как у запроса, который отправил клиент, значит, байты сошлись все до одного.
|
||
|
||
Что получило приложение. В каждом из трёх опытов в нашем тесте приложение прочитало все двести пятьдесят байт одним чтением, и хэш равен оригиналу. Значит, до приложения дошли настоящие байты, а не нули.
|
||
|
||
Проверь себя. seqovl равно midsld минус два, то есть метка на смещении сто два. Сколько накладок, какая длина пакета и с какого смещения он начнётся?
|
||
|
||
Сто две накладки, длина двести сорок восемь байт, начало на смещении два. Это счёт по формуле, такой опыт мы не запускали.
|
||
|
||
![[wiki-anim-video06-s11-table-sum-104.svg|Таблица накладок и сумма 104]]
|
||
|
||
Итак. Накладка это байты слева от разреза, лежащие поверх хвоста левой части. Начало пакета равно разрезу минус число накладок: сто три, семь и один в наших трёх опытах. В нашем тесте сервер каждый раз называет перекрытый диапазон дублирующим блоком, а приложение получает настоящие байты.
|
||
|
||
|
||
## Отмена и неразрешённая метка; какая часть получает накладку
|
||
|
||
Вопрос сцены: когда функция отменяет накладку, и какая из частей её получает, если разрезов несколько?
|
||
|
||
Игрушка, выдуманная. У писца такое правило: черновые строки пишутся только поверх первой страницы, и перед ними должна остаться хотя бы одна настоящая строка этой страницы. Если черновик пришлось бы начинать с самой первой строки или левее, писец откладывает перо и отправляет страницу как есть.
|
||
|
||
В коде правило именно такое: метка строго левее первого разреза. Игрушка лишь пересказывает правило. Почему оно такое, в коде не сказано, а опыт покажет только, что при нарушении накладка отменяется.
|
||
|
||
Опыт первый: seqovl равно midsld, и первый разрез там же, на сто четвёртом смещении. Метка не левее разреза. В журнале нашего теста строка: seqovl отменён, потому что сто четыре не меньше первой позиции разреза, сто четыре. На проводе обычная картина: сто сорок шесть байт со смещения сто четыре, затем сто четыре байта с начала. В подтверждениях сервера нет дублирующего блока. Функция не падает: она просто работает как без seqovl.
|
||
|
||
Опыт второй: seqovl равно девятьсот девяносто девять. Такая метка стоит за концом сообщения и не разрешается. В журнале строка, что метку разрешить не удалось, накладка отменена. Разрез всё равно делается, и на проводе те же два пакета, что и без seqovl.
|
||
|
||
Ещё граничный случай: seqovl равно нулю. Метка на смещении ноль разрешается без ошибки, но накладок получается ноль, узор нулевой длины, и пакеты те же, что без seqovl. В журнале нет ни одной строки об отмене.
|
||
|
||
Во всех трёх опытах приложение прочитало все двести пятьдесят байт одним чтением, и хэш равен.
|
||
|
||
![[wiki-anim-video06-s12-cancel-rule-runs.svg|Отмена накладки: правило и три опыта]]
|
||
|
||
Теперь про разрезы. Возьмём три позиции разреза: host, midsld и endhost, а seqovl пусть будет host минус один, то есть девяносто шесть. Три разреза дают четыре части. Левая, девяносто семь байт с начала. Вторая, семь байт со смещения девяносто семь, это «www.exa».
|
||
|
||
Третья часть, восемь байт со смещения сто четыре. Последняя, сто тридцать восемь байт со смещения сто двенадцать.
|
||
|
||
Накладку получает часть, которая начинается с первого разреза, то есть вторая. Девяносто шесть меньше девяносто семи, проверка проходит, накладок девяносто шесть. Длина пакета девяносто шесть плюс семь, сто три, а начало на девяносто седьмом смещении минус девяносто шесть, то есть на смещении один.
|
||
|
||
На проводе в нашем тесте четыре пакета, с конца: сто тридцать восемь байт со смещения сто двенадцать, восемь со ста четырёх, сто три со смещения один, и последней левая часть в девяносто семь байт.
|
||
|
||
Итоговое подтверждение сервера несёт дублирующий блок со второго по девяносто седьмой номер. Это девяносто шесть номеров, ровно столько, сколько накладок.
|
||
|
||
Правило не про часть, где стоит имя, а про место: накладка достаётся части, которая начинается с первого разреза. Проверим: позиции два, host, midsld, endhost, а seqovl тот же, девяносто шесть. Первый разрез на втором смещении, девяносто шесть не меньше двух, и накладка отменена. На проводе обычная картина из пяти частей.
|
||
|
||
Проверь себя. Позиция разреза host, то есть смещение девяносто семь, а seqovl равно host плюс один. Что будет?
|
||
|
||
Метка на смещении девяносто восемь, а первый разрез на девяносто седьмом. Девяносто восемь не меньше девяноста семи, накладка отменяется, и в журнале будет строка про девяносто восемь и девяносто семь. Это вывод из кода, такой опыт мы не запускали.
|
||
|
||
![[wiki-anim-video06-s12-which-part-gets-it.svg|Какая часть получает накладку]]
|
||
|
||
Итак. Накладка отменяется, если метка не левее первого разреза или не разрешается, и тогда пакеты такие же, как без неё. Нулевая метка безвредна. Накладку получает часть, которая начинается с первого разреза, а при первом разрезе близко к началу почти любая метка отменяется.
|
||
|
||
|
||
## Накладка: нули, текст, шестнадцатеричная запись и пропавший блоб в середине цикла
|
||
|
||
Вопрос сцены: чем заполнена накладка, и что будет, если узор не найден?
|
||
|
||
Игрушка, выдуманная. Штамп из пяти знаков: один, два, три, четыре, пять. Нужно заполнить восемь клеток. Штамп ставится дважды и обрезается: получается «один, два, три, четыре, пять, один, два, три». Так работает помощник pattern: он повторяет узор и берёт нужное число байт с начала.
|
||
|
||
По умолчанию узор это один нулевой байт, поэтому накладка состоит из нулей. Это мы видели на проводе: у пакета с seqovl равным host все байты головы нулевые.
|
||
|
||
Узор можно заменить аргументом seqovl_pattern. Он принимает имя блоба, то есть заранее заданных байтов, или строку в шестнадцатеричной записи с приставкой «ноль икс». В нашем тесте узор это четырнадцать байтов, которые читаются как «www.decoy.test», то есть выдуманный текст-приманка.
|
||
|
||
Нужно заполнить девяносто семь байт узором в четырнадцать. Шесть повторов это восемьдесят четыре байта. До девяноста семи не хватает тринадцати, их берём с начала седьмого повтора.
|
||
|
||
На проводе пакет начинается словами «www.decoy.testwww.decoy.testwww» и так далее. Это двести сорок три байта со смещения семь. Приложение сервера получило двести пятьдесят байт, хэш равен оригиналу. Приманка до приложения не дошла.
|
||
|
||
![[wiki-anim-video06-s13-pattern-fill.svg|Чем заполнена накладка]]
|
||
|
||
Теперь отказ. Пусть узор назван именем, которого нет: блоб с выдуманным именем. Первый случай без аргумента optional, то есть «необязательно»: с ним, если блоба нет, берётся нулевой узор. Берём три разреза, как в прошлой сцене, и seqovl равно host минус один.
|
||
|
||
Цикл идёт с конца: сначала последняя часть, сто тридцать восемь байт со смещения сто двенадцать, затем третья, восемь байт со смещения сто четыре. Накладка нужна только второй части, и именно там функция ищет узор. Блоба нет, функция вызывает ошибку.
|
||
|
||
В журнале две строки: блоб недоступен, и пакет пропущен без изменений. Движок, то есть часть программы на Си, о которой мы договорились в начале выпуска, пропускает оригинал: на провод идёт целое приветствие в двести пятьдесят байт, после двух частей.
|
||
|
||
Уже отправленное не вернуть: две части уже на проводе, а как движок обращается с такими случаями, объясняет выпуск «Lua внутри программы». Сервер получил две части, затем оригинал целиком, и в итоговом подтверждении назвал дублирующим диапазон со ста пятого по двести пятидесятый номер. В нашем тесте приложение всё равно получило целое приветствие.
|
||
|
||
Второй случай, с аргументом optional. Узора нет, функция пишет в журнал, что берёт нулевой, и всё идёт как обычно: четыре части, сто тридцать восемь, восемь, сто три и девяносто семь байт.
|
||
|
||
Проверь себя. Один разрез midsld, seqovl равно host, узор seqovl_pattern назван, но блоба с таким именем нет, optional не задан. Успеет ли хоть одна часть уйти на провод до ошибки?
|
||
|
||
По коду нет. С одним разрезом цикл начинается со второй части, а там как раз и ищется узор, так что ошибка случится до первой отправки. Уйдёт только оригинал. Это вывод из кода, такой опыт мы не запускали.
|
||
|
||
Для сравнения: у аргумента blob, который заменяет данные для разреза, отказ похожий. Без optional в нашем тесте ошибка, с optional функция пропускает работу. В обоих случаях оригинал уходит целым, ничего не разрезано.
|
||
|
||
![[wiki-anim-video06-s13-missing-blob-mid-loop.svg|Узор не найден посреди цикла]]
|
||
|
||
Итак. Накладка по умолчанию из нулей, а seqovl_pattern заменяет их узором, который повторяется и обрезается. Отсутствующий узор без optional это ошибка, и при нескольких разрезах уже отправленные части остаются на проводе, а за ними идёт оригинал. С optional берутся нули.
|
||
|
||
|
||
## Приёмник и два игрушечных читателя: кто верит первой копии, а кто последней
|
||
|
||
Вопрос сцены: что делает приёмник с перекрытыми байтами, и чем отличаются два игрушечных читателя?
|
||
|
||
Приёмник сначала. В пяти наших опытах приложение сервера получило настоящие байты, хэш равен оригиналу, а сервер каждый раз назвал дублирующим блоком ровно перекрытый диапазон.
|
||
|
||
В нашем тесте на сервере с Linux мы увидели ровно это. По общим сведениям, форма дублирующего блока описана в документе RFC две тысячи восемьсот восемьдесят три. Какую из двух копий ядро держит внутри, из нашего захвата не видно, и мы этого не утверждаем.
|
||
|
||
Руководство к программе говорит: когда позднее перекрытие приходит, оно записывается поверх того, что уже лежит в буфере; так ведут себя все системы, кроме Windows, где техника не работает. Это слова руководства, а не наш опыт: мы проверили только Linux.
|
||
|
||
![[wiki-anim-video06-s14-receiver-dsack-table.svg|Приёмник и перекрытые байты]]
|
||
|
||
Теперь читатели. Они наши, игрушечные и не представляют настоящее оборудование. Представьте двух кладовщиков, принимающих пронумерованные страницы. Первый, увидев занятое место, его больше не трогает. Второй кладёт новую страницу поверх старой. Накладка из черновиков обманывает только первого.
|
||
|
||
Наши читатели устроены так же: оба пересобирают поток по номерам байтов, как приёмник. Первый записавший побеждает: занятое место не меняет. Последний записавший побеждает: перезаписывает. «Видит» значит: поиск находит строку www.example.com в собранном потоке, «слеп»: не находит.
|
||
|
||
Возьмём опыт с seqovl равным host. Сначала приходит пакет из нулей и настоящих байтов, с седьмого смещения. Нули ложатся на места с седьмого по сто третье, настоящие байты второй части на места со сто четвёртого. Первые семь байт имени, «www.exa», оказались под нулями. Затем приходит левая часть с настоящими байтами с нулевого по сто третий.
|
||
|
||
Первый записавший побеждает: места с седьмого по сто третье заняты нулями, настоящие байты он игнорирует, имя в потоке так и не собирается. Он слеп. Последний записавший побеждает: он записывает настоящие байты поверх нулей, и имя появляется целым. Он видит.
|
||
|
||
В нашей таблице так же в опытах с меткой на единицу меньше середины и с приманкой, а разбор захвата с seqovl равным единице даёт то же. Первый читатель слеп, с приманкой он видит её вместо имени. Последний читатель видит имя. А после обычного беспорядка, без накладки, оба видят имя: сортировка по номерам делает своё дело.
|
||
|
||
Теперь ещё один наш читатель, самый подробный. Все виды читателей мы сведём вместе в конце выпуска, а здесь нужен только результат. Ту же картину мы проверили с перехватом пакетов в очереди. В роутере на пути к серверу стоит второй nfqws2 с нашим простым читателем. Второй nfqws2 это второй экземпляр той же программы, запущенный в роутере: он ничего не отправляет и не меняет, а только решает, выбросить пакет или пропустить. Читатель ищет подстроку, без TLS и таймеров, и, найдя имя, выбрасывает этот пакет и все следующие данные клиента.
|
||
|
||
С накладкой и читателем «первый побеждает» сервер получил все двести пятьдесят байт. С читателем «последний побеждает» читатель нашёл имя, и сервер получил ноль: без начала приложение не получает ничего.
|
||
|
||
Проверь себя. Три разреза, seqovl равен host минус один: накладка занимает смещения с первого по девяносто шестое, а имя начинается с девяносто седьмого. Увидит ли имя читатель «первый побеждает»?
|
||
|
||
Увидит. Нули лежат левее имени, а само имя пришло настоящими байтами; разбор нашего захвата показал то же: оба читателя видят имя. Накладка скрывает имя от игрушечного читателя «первый побеждает» только если закрывает байты имени.
|
||
|
||
![[wiki-anim-video06-s14-two-readers.svg|Два игрушечных читателя]]
|
||
|
||
Итак. Приёмник в нашей лаборатории получает настоящие байты и называет перекрытие дублирующим блоком. Читатель «первый побеждает» слеп к имени, только если накладка накрывает хотя бы один его байт, а читатель «последний побеждает» видит имя во всех наших опытах. Какой из них похож на настоящее оборудование, мы не знаем.
|
||
|
||
|
||
## Проверь себя: одно слово seqovl в трёх стратегиях, три геометрии; что мы знаем о накладке
|
||
|
||
Вопрос сцены: слово seqovl встречается у нескольких стратегий. Значит ли оно везде одно и то же?
|
||
|
||
Игрушка, выдуманная: линейка с нулём у первого байта сообщения. Левее нуля место до первого байта, правее внутри сообщения. Накладку можно положить по обе стороны.
|
||
|
||
Первая строка: multisplit, которому посвящён выпуск «multisplit: резать на части». Там seqovl число, например пять. Первый пакет получает пять накладок и левую часть в сто четыре байта: сто девять, и начинается на пять байтов левее первого байта сообщения. Сервер в нашем тесте назвал дублирующим блок из пяти номеров левее первого.
|
||
|
||
Вторая строка: стратегия tcpseg. Её разберём позже в этом выпуске, а пока важно одно: seqovl у неё тоже число. В нашем тесте мы отправили всё сообщение одним пакетом с накладкой в один байт: двести пятьдесят один байт, начало на байт левее первого. Оригинал выбросила отдельная стратегия drop, о ней позже. Сервер назвал дублирующим нулевой номер.
|
||
|
||
Третья строка: multidisorder. Здесь seqovl метка, а пакет с накладкой начинается правее первого байта, на смещении семь для метки host. Разрез минус метка: сто четыре минус девяносто семь. Накладка лежит внутри сообщения, на хвосте левой части.
|
||
|
||
Имя одно, а геометрия разная. У multisplit и tcpseg seqovl это число, и пакет сдвигается влево от начала той части, которую отправляет. У multisplit накладку получает только левая часть, она всегда начинается с первого байта, и накладка уходит левее него. У tcpseg это зависит от позиции: в наших опытах часть начиналась с первого байта, и накладка тоже ушла левее него. У multidisorder seqovl это метка, и пакет с накладкой лежит внутри сообщения и начинается не раньше второго байта. Аргумент называется одинаково, а читать его нужно по функции.
|
||
|
||
Проверь себя. Seqovl равно пяти, разрез на сто четвёртом смещении. Где начнётся пакет с накладкой у multisplit и где у multidisorder?
|
||
|
||
У multisplit пакет начнётся на пять байтов левее первого, длина сто девять: этот опыт мы запускали. У multidisorder метка пять означает смещение пять, накладок пять, пакет в сто пятьдесят один байт начнётся на смещении девяносто девять, потому что сто четыре минус пять. Здесь счёт по формуле, такой опыт не запускали.
|
||
|
||
![[wiki-anim-video06-s15-one-word-three-geometries.svg|Одно слово, три геометрии]]
|
||
|
||
Что мы знаем об seqovl в multidisorder. Это метка, и число накладок равно её смещению. Накладку получает вторая часть, метка стоит строго левее первого разреза, начало пакета не ниже единицы.
|
||
|
||
Узор по умолчанию из нулей, а seqovl_pattern заменяет его блобом или шестнадцатеричной строкой; без блоба и без optional это ошибка, и при нескольких разрезах отправленные части остаются на проводе.
|
||
|
||
Про приёмник: в нашем тесте на Linux приложение получает настоящие байты, а перекрытие сервер называет дублирующим блоком; про Windows руководство говорит иначе, мы не проверяли. Про игрушки: читатель «первый побеждает» слеп к имени, если накладка накрывает его байты, а «последний побеждает» видит имя во всех наших опытах. Что из этого похоже на настоящее оборудование, мы не знаем.
|
||
|
||
![[wiki-anim-video06-s15-seqovl-knowledge-card.svg|Что мы знаем о seqovl]]
|
||
|
||
Итак. Одно слово, три геометрии: у multisplit и tcpseg в наших опытах накладка левее первого байта, у multidisorder внутри сообщения. Всё это пока про лабораторию и наши игрушки.
|
||
|
||
|
||
## Проигрыш и новая функция: большое приветствие режется целиком, один раз
|
||
|
||
Вопрос сцены: что делает новая функция, когда приветствие пришло не одним пакетом, а двумя сегментами?
|
||
|
||
Нам нужна одна идея из выпуска «Сборка и повторный проигрыш», вот она коротко. Приветствие в тысячу четыреста восемьдесят четыре байта не влезает в один пакет, и клиент отправляет его двумя сегментами: тысяча четыреста сорок восемь байт и тридцать шесть. Программа удерживает оба, собирает сообщение целиком, а потом возвращает их из очереди по одному. Это называется проигрыш.
|
||
|
||
Стратегию вызывают на каждый кусок проигрыша, и на каждом ей доступна вся сборка. Поэтому договор такой: стратегия действует на первом куске и по всей сборке, оставляет пометку в записи об этом соединении (программа хранит такие записи, об этом выпуск «Память о соединении»), а остальные куски выбрасывает по пометке. Это делают помощники replay_first и replay_drop, и multidisorder следует этому договору.
|
||
|
||
Теперь игрушка, выдуманная. Наборщик получает два листа одной статьи. Он склеивает их в одну ленту, режет по меткам и подаёт части с конца. Прежние границы листов он уже забыл.
|
||
|
||
![[wiki-anim-video06-s16-assemble-then-cut-once.svg|Сборка, один разрез, остальное выбросить]]
|
||
|
||
Наш опыт, самый простой: позиция разреза midsld, то есть сто четвёртый байт. На первом куске функция режет всю сборку на две части.
|
||
|
||
Всё сообщение это тысяча четыреста восемьдесят четыре байта, левая часть сто четыре. Значит, вторая: минус сто четыре, то есть тысяча триста восемьдесят. Первой уходит длинная, со смещения сто четыре, потом левая, сто четыре байта с начала.
|
||
|
||
Обратите внимание, чего на проводе нет: пакетов в тысячу четыреста сорок восемь и в тридцать шесть байт. Первый оригинал выброшен после отправки частей, второй выброшен по пометке, в журнале строка, что сборка уже отправлена. На втором куске функция лишь пишет в журнал, что не действует на последующие куски.
|
||
|
||
Сервер в нашем тесте ответил как обычно при беспорядке: сначала блок со ста пятого номера до конца, потом подтверждение всего, тысячи четырёхсот восьмидесяти пяти. Приложение прочитало все тысячу четыреста восемьдесят четыре байта одним чтением. На обоих отправленных пакетах только флаг ACK, без PSH: флаги копируются с первого куска, а у него PSH не было.
|
||
|
||
Проверь себя. Позиции разреза midsld и минус один, то есть последний байт сообщения. Сколько частей и сколько байт в средней?
|
||
|
||
Три части. Минус один это смещение тысяча четыреста восемьдесят три. Средняя часть идёт со ста четвёртого смещения до тысяча четыреста восемьдесят второго: тысяча триста семьдесят девять байт. Вместе со ста четырьмя и одним байтом получается тысяча четыреста восемьдесят четыре.
|
||
|
||
Теперь тот же опыт в нашем тесте. Первым уходит один байт, последний байт приветствия: он принадлежал второму сегменту ядра, а функция режет всю сборку, поэтому ей это всё равно. Потом тысяча триста семьдесят девять байт со смещения сто четыре, и последней левая часть.
|
||
|
||
Подтверждения сервера растут влево. Сначала блок только из последнего номера, потом один блок от ста пятого до конца, и когда приходит начало, подтверждено всё. Приложение снова читает всё одним чтением, тысячу четыреста восемьдесят четыре байта.
|
||
|
||
![[wiki-anim-video06-s16-lab-two-cuts.svg|Опыт: один разрез и проверь себя с двумя]]
|
||
|
||
Итак. Новая функция действует на первом куске проигрыша и режет всю собранную ленту, границы сегментов ядра для неё ничего не значат. Остальные куски выбрасываются по пометке, поэтому оригиналов на проводе нет. Позиция может лежать в любом месте сообщения, кроме самого начала, даже в последнем байте, который тогда отправляется первым.
|
||
|
||
|
||
## Старая функция: позиции в окне куска и пересчётчик окна
|
||
|
||
Вопрос сцены: как старая функция, multidisorder_legacy, понимает, какая позиция разреза относится к какому куску проигрыша?
|
||
|
||
Старую функцию вызывают на каждый кусок проигрыша отдельно, и она видит только его байты, а метки посчитаны на всём сообщении. Поэтому метку нужно перевести в номер внутри куска.
|
||
|
||
Игрушка, выдуманная. Статья в двадцать строк печатается на двух страницах: на первой строки с первой по двенадцатую, на второй с тринадцатой по двадцатую. Каждая типография получает только свою страницу и нумерует строки заново, с единицы. Редактор отметил, что резать нужно на строке пятнадцать. На первой странице такой строки нет. На второй это пятнадцать минус тринадцать плюс один, строка три.
|
||
|
||
Отметка на строке пять достаётся первой странице, а на второй её нет. Строка тринадцать это первая строка второй страницы: на первой её нет, на второй она становится строкой номер один. Разрез перед первой строкой страницы разрезом не считается.
|
||
|
||
![[wiki-anim-video06-s17-toy-pages-numbering.svg|Статья на двух страницах: своя нумерация]]
|
||
|
||
Теперь то же самое в программе. У каждого куска проигрыша есть окно позиций. Нижняя граница это первая позиция куска, верхняя это первая позиция следующего куска, нумерация в коде идёт с единицы. Нижняя граница входит в окно, верхняя нет. У первого куска границы один и тысяча четыреста сорок девять, у второго тысяча четыреста сорок девять и тысяча четыреста восемьдесят пять.
|
||
|
||
Работу делает маленький пересчётчик окна, в коде он называется pos_normalize. Первое условие: позиция не меньше нижней границы и меньше верхней. Если оно выполнено, результат равен позиции минус нижняя граница плюс один: номер внутри куска. Если нет, возвращается пусто, nil.
|
||
|
||
Метка midsld. Это смещение сто четыре, а в коде позиция сто пять. Для первого куска окно начинается с единицы, сто пять входит, результат сто пять минус один плюс один, то есть сто пять. Для второго куска сто пять меньше нижней границы, результат пусто.
|
||
|
||
Метка минус один, последний байт, смещение тысяча четыреста восемьдесят три, позиция в коде тысяча четыреста восемьдесят четыре. Для первого куска она не меньше верхней границы, результат пусто. Для второго куска тысяча четыреста восемьдесят четыре минус тысяча четыреста сорок девять плюс один равно тридцати шести. В журнале, где числа с нуля, мы увидим тридцать пять.
|
||
|
||
Список позиций обрабатывает пересчёт списка, в коде pos_array_normalize. Он идёт по списку, в каждую ячейку записывает пересчитанное значение, и если вышло пусто, удаляет ячейку и не двигает счётчик. Возьмём список из двух позиций: сто пять и тысяча четыреста восемьдесят четыре. На первом куске сто пять остаётся, а вторая позиция даёт пусто и удалена. Останется один элемент.
|
||
|
||
На втором куске сто пять превращается в пусто и удаляется. Счётчик остаётся на месте, и теперь на первом месте стоит тысяча четыреста восемьдесят четыре. Она превращается в тридцать шесть, счётчик двигается, список кончился. Останется одно значение, тридцать шесть. После пересчёта функция убирает позицию один, как в выпуске «multisplit: резать на части»: разрез перед первым байтом куска разрезом не считается.
|
||
|
||
![[wiki-anim-video06-s17-windows-normalizer.svg|Окно куска и пересчётчик]]
|
||
|
||
Проверь себя. Метка стоит на смещении тысяча четыреста сорок восемь: это первый байт второго куска. Что с ней сделает пересчётчик на каждом из двух кусков?
|
||
|
||
В коде это позиция тысяча четыреста сорок девять. На первом куске она равна верхней границе, а верхняя не входит: результат пусто. На втором куске она равна нижней границе и входит, результат один, и потом позицию один удаляют. Метка пропала на обоих кусках и разреза нет нигде. Это счёт по коду, в опыте проверим в конце части.
|
||
|
||
Итак. У каждого куска проигрыша своё окно: нижняя граница входит, верхняя нет. Пересчётчик переводит позицию сообщения в номер внутри куска или возвращает пусто, а список чистится на месте. Метка точно на границе кусков в этой схеме пропадает.
|
||
|
||
|
||
## Старая функция по строкам и её первый прогон на большом приветствии
|
||
|
||
Вопрос сцены: что отправляет старая функция на нашем приветствии в тысячу четыреста восемьдесят четыре байта, и чем это отличается от новой?
|
||
|
||
Старая функция называется multidisorder_legacy. В комментарии сказано, что порядок сегментов у неё такой же, как в первой версии программы; там эта версия названа «nfqws один». О ней мы говорили в выпуске «Письмо в сортировочном центре»: стратегии в ней были зашиты в код на Си. Про новую функцию написано осторожнее: алгоритм не на сто процентов тот же. Читаем обе по коду.
|
||
|
||
Ворота те же: не TCP, направление и тип нагрузки. Отличий три: у старой функции нет аргумента blob, нет аргумента nodrop и нет помощников проигрыша, replay_first и replay_drop.
|
||
|
||
Поэтому каждый кусок проигрыша она судит сам по себе и выбрасывает или пропускает именно его.
|
||
|
||
Данные. Их две: нагрузка именно этого пакета, то есть этого куска, и вся сборка. Для обычного одного пакета они совпадают. Метки разрешаются на сборке, поэтому host, midsld и минус один означают одни и те же байты для любого куска, а режет функция только то, что лежит в её куске.
|
||
|
||
Дальше окно куска и пересчёт, как в прошлой сцене. Функция пишет в журнал диапазон куска, пересчитывает позиции и убирает позицию один.
|
||
|
||
Если какая-то позиция осталась, функция, когда задан seqovl, так же пересчитывает его метку и отправляет части этого куска тем же общим отправителем, что и новая.
|
||
|
||
Если не осталось ни одной, она отправляет кусок как есть и возвращает «выбросить».
|
||
|
||
Посмотрим на кусок как есть. Функция отправляет глубокую копию текущего пакета и применяет к ней все аргументы подмены полей из строки стратегии, например время жизни пакета, TTL, как в конце выпуска «multisplit: резать на части»; такие аргументы меняют служебные поля копии, а подробно их разберёт выпуск «Фейк и его условия». Оригинал выбрасывается, копия уходит на провод. «Как есть» здесь не значит «не трогали», об этом в следующей части выпуска.
|
||
|
||
![[wiki-anim-video06-s18-legacy-conveyor.svg|Старая функция по шагам]]
|
||
|
||
Теперь наш опыт, позиция разреза midsld. Первый кусок: в журнале диапазон от нуля до тысяча четыреста сорок седьмого, пересчитанная позиция сто четыре. Это те же окна, что в прошлой сцене, только в журнале числа с нуля.
|
||
|
||
Режутся только эти тысяча четыреста сорок восемь байт. Левая часть сто четыре, правая это остаток, тысяча триста сорок четыре. Первой уходит правая, со смещения сто четыре, потом левая.
|
||
|
||
Второй кусок: диапазон от тысяча четыреста сорок восьмого до тысяча четыреста восемьдесят третьего. Журнал: в этом пакете нет пересчитанной позиции. Функция отправляет копию как есть, тридцать шесть байт со смещения тысяча четыреста сорок восемь. Размер пакета на проводе восемьдесят восемь байт: двадцать байт заголовка IP, тридцать два TCP, из них двенадцать на метку времени, и тридцать шесть данных.
|
||
|
||
На проводе в нашем тесте три пакета: тысяча триста сорок четыре байта, сто четыре и тридцать шесть. Сервер ответил блоком от ста пятого номера, потом подтвердил первые тысячу четыреста сорок восемь байт, то есть ждёт тысяча четыреста сорок девятый, а хвост пришёл по порядку. Приложение сделало два чтения: тысяча четыреста сорок восемь байт и тридцать шесть. Сколько чтений получилось, решает момент чтения: здесь первые тысяча четыреста сорок восемь байт уже были подтверждены, когда пришёл хвост.
|
||
|
||
Новая функция на тех же аргументах отправила тысячу триста восемьдесят и сто четыре байта, прежней границы сегментов на проводе нет. Старая оставила каждый пакет внутри прежнего сегмента. Сколько чтений сделает приложение, решает момент чтения: здесь у новой одно, а в опыте с минус один будет два.
|
||
|
||
![[wiki-anim-video06-s18-legacy-first-run.svg|Первый запуск старой функции]]
|
||
|
||
Проверь себя. Старая функция, позиция midsld, приветствие в двести пятьдесят байт, один пакет. Что она отправит?
|
||
|
||
То же, что новая: сто сорок шесть байт со смещения сто четыре, затем сто четыре байта с начала. В журнале диапазон от нуля до двести сорок девятого и пересчитанная позиция сто четыре: окно одного пакета равно всему сообщению. В нашем тесте провод совпадает с опытом новой функции.
|
||
|
||
Итак. Старая функция режет только тот кусок, в окне которого лежит позиция, остальные отправляет копией как есть. Метки она разрешает на всей сборке, а блоба, nodrop и помощников проигрыша у неё нет. На нашем приветствии она отправляет тысячу триста сорок четыре, сто четыре и тридцать шесть байт.
|
||
|
||
|
||
## Поздние куски и границы: три опыта, две функции; итог части
|
||
|
||
Вопрос сцены: где две функции расходятся на нашем большом приветствии, когда позиция лежит в позднем куске или на границе кусков? Три опыта, а в таблице к ним добавится строка midsld из прошлых сцен.
|
||
|
||
Опыт первый: позиции midsld и минус один. Старая функция на первом куске делает то же, что раньше: тысяча триста сорок четыре и сто четыре. На втором куске окно с тысяча четыреста сорок восьмого, метка минус один пересчитывается в тридцать пять, это видно в журнале. Кусок в тридцать шесть байт режется на тридцать пять и один, и последним байтом вперёд.
|
||
|
||
На проводе в нашем тесте: тысяча триста сорок четыре, сто четыре, потом один байт и тридцать пять. Ни один пакет не пересёк границу клиентских сегментов. А новая функция в этом опыте отправила последний байт приветствия самым первым, до всего остального.
|
||
|
||
Опыт второй: только минус один. Старая функция: на первом куске разреза нет, она отправляет копию целиком, тысячу четыреста сорок восемь байт, а потом на втором куске сначала один байт, затем тридцать пять.
|
||
|
||
Новая функция при той же позиции сначала отправляет один байт, потом остальное: тысячу четыреста сорок восемь байт и тридцать пять. Часть в тысячу четыреста восемьдесят три байта не поместилась в пакет, где данных не больше тысячи четырёхсот сорока восьми, и ушла двумя.
|
||
|
||
Почему такой потолок, расскажет следующая часть выпуска. Важно другое: последний байт ушёл первым, и сервер сначала отвечает блоком из одного последнего номера.
|
||
|
||
![[wiki-anim-video06-s19-new-vs-legacy-late-pieces.svg|Новая и старая: поздние куски]]
|
||
|
||
Опыт третий: позиция тысяча четыреста сорок восемь, ровно граница между двумя сегментами клиента. Новая функция сначала отправляет тридцать шесть байт со смещения тысяча четыреста сорок восемь, затем тысяча четыреста сорок восемь байт с начала. Приложение читает все тысячу четыреста восемьдесят четыре байта одним чтением.
|
||
|
||
Старая функция делает иначе, и это ответ на вопрос из сцены про окна кусков. В журнале дважды: в этом пакете нет пересчитанной позиции. Метка выпала из обоих окон: на первом куске она равна верхней границе, на втором становится позицией один и удаляется. На проводе обычный порядок: тысяча четыреста сорок восемь байт и тридцать шесть, ничего не переставлено.
|
||
|
||
Сведём в таблицу. Первая строка, midsld: новая отправляет тысячу триста восемьдесят и сто четыре. Старая отправляет тысячу триста сорок четыре, сто четыре и тридцать шесть. Вторая, midsld и минус один: у новой последний байт первым, у старой он идёт после первого куска. Третья, только минус один: новая начинает с последнего байта, старая с копии первого куска. Четвёртая, граница: новая переставляет сегменты, старая нет.
|
||
|
||
Проверь себя. Старая функция, позиция тысяча четыреста сорок семь, то есть байт перед границей. Что она отправит и в каком порядке?
|
||
|
||
Смещение тысяча четыреста сорок семь, в коде позиция тысяча четыреста сорок восемь. В окно первого куска она входит. Первый кусок режется на один байт и тысячу четыреста сорок семь, и уходят они в обратном порядке: сначала один байт, потом остальное. Во втором куске позиция уже не входит, и он уходит копией. Это счёт по коду, такой опыт мы не запускали.
|
||
|
||
Что мы узнали о проигрыше в этой части выпуска. Новая функция действует на первом куске и режет всю сборку, остальные куски выбрасывает по пометке. Старая режет каждый кусок в его окне, не пересекает границу клиентских сегментов, и метка на границе кусков у неё пропадает.
|
||
|
||
Итак, итог этой части выпуска. Накладка в multidisorder лежит внутри сообщения, а приёмник в нашем тесте получает настоящие байты. Новая функция режет всю собранную ленту, старая каждый кусок проигрыша в его окне. Дальше: нарезка по размеру сегмента, ловушки старой функции, tcpseg и drop и итоговая таблица игрушечных читателей.
|
||
|
||
![[wiki-anim-video06-s19-boundary-table-recap.svg|Граница кусков и итог части]]
|
||
|
||
|
||
## Повтор и вопрос: кусок длиннее пакета, ломтики
|
||
|
||
Это последняя часть выпуска «Перемешать: disorder». Начнём с короткого повтора, чтобы её можно было смотреть отдельно.
|
||
|
||
Наш пример прежний: приветствие на двести пятьдесят байт, метка midsld на смещении сто четыре. Стратегия multidisorder отправляет с конца: сначала длинный кусок, сто сорок шесть байт, потом короткий, сто четыре. Получатель в нашем тесте сначала отвечает «жду первый байт, но вот мой блок», потом подтверждает всё, и приложение в этом запуске прочитало всё за одно чтение.
|
||
|
||
Ещё у этой функции есть параметр seqovl, «накладка»: в начало одного из кусков вписываются лишние байты. В нашем тесте приложение получило настоящие байты, а перекрытый диапазон получатель отметил в ответе как дубликат.
|
||
|
||
Теперь большое приветствие. Ядро клиента отправляет его двумя пакетами: в первом тысяча четыреста сорок восемь байт, во втором тридцать шесть. Программа задерживает оба и проигрывает заново: так называется повторная выдача удержанных пакетов, она разобрана в выпуске «Сборка и повторный проигрыш». Для отправки с конца есть две функции: новая режет всё сообщение один раз, старая, multidisorder_legacy, режет каждый проигрываемый пакет отдельно и сохраняет разбивку клиента.
|
||
|
||
Вопрос такой. Новая функция режет сообщение на два куска. При обычном размере пакета длинный кусок, тысяча триста восемьдесят байт, в нашем тесте уходит одним пакетом. А если пакеты мельче? Мы уменьшили пакет на клиенте. Как мы это сделали: ограничили размер пакета на клиенте шестью сотнями байт. Это приём лаборатории, а не часть стратегии. Что тогда с длинным куском?
|
||
|
||
Размер пакета задаёт размер сегмента, MSS; в выпуске «multisplit: резать на части» мы называли его порцией. Каждая сторона объявляет своё значение, код берёт меньшее, а если известны не оба, запасное, тысяча двести двадцать, и вычитает байты опций заголовка. В нашем опыте клиент объявил пятьсот шестьдесят, сервер тысячу четыреста шестьдесят; опции занимают двенадцать байт, значит, максимум данных в пакете пятьсот сорок восемь. Ровно так ядро клиента нарезало это приветствие: пятьсот сорок восемь, пятьсот сорок восемь и триста восемьдесят восемь. В обычных опытах обе стороны объявляли тысячу четыреста шестьдесят, и тот же расчёт даёт потолок из прошлой части, тысячу четыреста сорок восемь.
|
||
|
||
![[wiki-anim-video06-s20-mss-ceiling.svg|Потолок данных в пакете]]
|
||
|
||
Что делает новая функция на этом приветствии? Короткий кусок, сто четыре байта, помещается в пакет. Длинный не помещается, и его нарезают на ломтики; это слово из выпуска «multisplit: резать на части», повторим его, чтобы не путать ломтики с кусками по метке. Два ломтика по пятьсот сорок восемь байт, вместе тысяча девяносто шесть. Остаток, двести восемьдесят четыре байта, это последний ломтик.
|
||
|
||
Порядок на проводе в нашем тесте. Смещения считаются с нуля, а номера байтов с единицы, поэтому смещение сто четыре это номер сто пятый. Сначала ломтик со смещения сто четыре. Потом следующий, со смещения шестьсот пятьдесят два: к смещению прибавили длину ломтика. Потом со смещения тысяча двести. И только потом короткий кусок с самого начала. Внутри длинного куска порядок обычный, между кусками с конца.
|
||
|
||
Получатель отвечает три раза «жду первый байт». Каждый раз он показывает блок с номера сто пять, и блок растёт вправо. Первый блок кончается номером шестьсот пятьдесят два. Второй номером тысяча двести. Третий номером тысяча четыреста восемьдесят четыре. Потом приходит подтверждение на всё. В первой части пять кусков шли с конца, и блок рос влево, а здесь ломтики идут вверх.
|
||
|
||
Код тот же, что у multisplit. Функция нарезки при отправке, rawsend_dissect_segmented, получает от программы размер сегмента, вычитает из него байты опций и получает максимум данных, в коде max_data. Если в куске данных больше, она отрезает ломтик за ломтиком, а последнему отдаёт остаток. По умолчанию каждый ломтик получает свой номер IP id, на единицу больше предыдущего, а порядковый номер сдвигается на длину ломтика, поэтому ломтики идут по возрастанию.
|
||
|
||
Тот же потолок виден с накладкой. В нашем тесте seqovl со значением host добавил к длинному куску девяносто семь байт, всего тысяча четыреста семьдесят семь. Это больше потолка, поэтому ушло два ломтика: тысяча четыреста сорок восемь байт и двадцать девять.
|
||
|
||
Проверь себя. Мы взяли другую стратегию, multisplit, и те же условия: максимум данных пятьсот сорок восемь байт, метка midsld. В каком порядке уйдут куски и ломтики?
|
||
|
||
В нашем тесте так: по порядку, с начала. Сначала сто четыре байта со смещения ноль. Потом три ломтика. Первый, пятьсот сорок восемь байт, со смещения сто четыре. Второй, тоже пятьсот сорок восемь, со смещения шестьсот пятьдесят два. Третий, двести восемьдесят четыре байта, со смещения тысяча двести.
|
||
|
||
![[wiki-anim-video06-s20-slices-acks-ceiling.svg|Ломтики, ответы и потолок с накладкой]]
|
||
|
||
Итак, кусок, который не влезает в пакет, нарезается на ломтики. Размер ломтика равен максимуму данных: размер сегмента минус опции, в нашем тесте пятьсот сорок восемь байт. Внутри куска ломтики идут по возрастанию, а между кусками порядок прежний, с конца.
|
||
|
||
|
||
## Старая функция: четыре ловушки
|
||
|
||
Старая функция на маленьком приветствии делает то же, что новая. Но на большом у неё есть особенности, которые легко не заметить. Разберём четыре, по одной.
|
||
|
||
Вопрос первой ловушки. Что старая функция делает с куском, в котором резать нечего?
|
||
|
||
Игрушка, выдуманная. Копировальная контора получила два листа: длинный и короткий. Длинный она режет по плану на два, короткий резать нечего. На каждый вылетающий лист ставится штамп «время жизни три». Короткий лист контора не отдаёт обратно, а переписывает на чистый бланк и тоже штампует. Итого три листа, на всех штамп.
|
||
|
||
В нашем тесте так и случилось. Старая функция с аргументом подмены полей «время жизни три» на большом приветствии: три пакета, тысяча триста сорок четыре байта, сто четыре и тридцать шесть, и у всех время жизни три. Хвост мы не резали, а штамп получил.
|
||
|
||
По общим сведениям, время жизни ограничивает число переходов пакета по сети. В нашей лаборатории от клиента до сервера один переход, и трёх хватает; опыта с недостаточным значением нет.
|
||
|
||
Откуда это в коде. Это ветка «ничего не режем». Если в этом пакете не осталось позиций разреза, функция пишет в журнал «нет позиций для разреза» и отправляет копию текущего пакета со всеми аргументами подмены полей. Если отправка удалась, возвращается «выбросить»: оригинал заменён копией.
|
||
|
||
![[wiki-anim-video06-s21-office-else-branch.svg|Ловушка 1: кусок без разреза]]
|
||
|
||
Вторая ловушка: какие параметры старая функция просто не читает? Первый, blob. Если он указывает на несуществующий блоб, она не падает и режет как обычно. Новая на тех же аргументах пишет ошибку и пропускает пакет как есть.
|
||
|
||
Второй, nodrop. С ним старая функция всё равно выбрасывает оригинал, и на проводе в нашем тесте только куски. Новая на большом приветствии отправляет два куска, а следом задержанные оригиналы, и получатель отвечает дублирующими блоками.
|
||
|
||
Третья ловушка, мелкая: что куски берут у проигрываемого пакета? Счётчик IP id у куска начинается с номера этого пакета. Ядро нумерует свои два пакета подряд, поэтому в нашем тесте у куска в сто четыре байта и у хвоста в тридцать шесть номер один и тот же. Флаги копируются: у первого пакета ядра был только ACK, у второго ещё и PSH, проталкивание. Смотрит ли на эти поля настоящее оборудование, мы не знаем.
|
||
|
||
Четвёртая касается журнала: строки отправки и отмены накладки подписаны словом «multidisorder», даже когда работает старая функция, потому что их пишет общий помощник multidisorder_send. Не удивляйтесь.
|
||
|
||
![[wiki-anim-video06-s21-three-traps.svg|Ловушки 2, 3 и 4 старой функции]]
|
||
|
||
И ещё одна особенность, про накладку. У старой функции метка seqovl считается по всему приветствию, но накладка должна попасть в диапазон текущего пакета ядра. В нашем тесте seqovl равно host, и первый пакет на проводе длиной тысяча четыреста сорок один байт: впереди накладка в девяносто семь байт. Получатель отметил перекрытый диапазон как дубликат. А метка «минус пять» указала в хвост приветствия, за пределы первого пакета: накладки не было.
|
||
|
||
Проверь себя. Старая функция, маленькое приветствие, pos равно нулю, время жизни три. Что окажется на проводе?
|
||
|
||
Один пакет в двести пятьдесят байт со временем жизни три. Позиция «ноль» выбрасывается, резать нечего, функция отправляет копию, а оригинал ядра выброшен.
|
||
|
||
И ещё проверка на старой функции. Максимум данных пятьсот сорок восемь, большое приветствие, метка midsld. Что на проводе?
|
||
|
||
Первый пакет ядра режется: часть после метки, четыреста сорок четыре байта, идёт первой, потом короткие сто четыре. Второй и третий пакеты, пятьсот сорок восемь и триста восемьдесят восемь байт, идут целыми, по возрастанию. В этом запуске приложение сделало три чтения, их число зависит от моментов чтения. У новой функции ломтики идут первыми, короткий кусок последним.
|
||
|
||
![[wiki-anim-video06-s21-legacy-seqovl-checks.svg|Накладка и проверки старой функции]]
|
||
|
||
Итак, у старой функции четыре ловушки. Кусок без разреза она отправляет копией, со всеми аргументами подмены полей. Параметров nodrop и blob не читает. Проигрываемые куски берут IP id и флаги у пакета ядра. Строки отправки в журнале подписаны именем другой функции. А seqovl у неё привязан к текущему пакету ядра.
|
||
|
||
|
||
## tcpseg: идея, код и мерка диапазона resolve_range
|
||
|
||
Последняя стратегия выпуска называется tcpseg. К ней можно добавить парную функцию drop. Начнём с идеи.
|
||
|
||
Вопрос. Можно ли отправить серверу только начало запроса, а остальное оставить самому клиенту?
|
||
|
||
Игрушка, выдуманная. Помощник берёт телеграмму в двести пятьдесят слов, отправляет адресату первые сто пять, а саму телеграмму рвёт. Адресат отвечает: получил сто пять. Что будет дальше, увидим на настоящем опыте.
|
||
|
||
Теперь настоящая команда нашего опыта: tcpseg с параметром pos «ноль, midsld» и отдельный вызов drop. Назовём tcpseg «одиночный сегмент»: она вырезает один диапазон байт и отправляет его одним сегментом. Слишком длинный диапазон нарежется на ломтики, как в первой сцене.
|
||
|
||
Посчитаем диапазон. Первая метка, ноль, это смещение ноль, самый первый байт. Вторая, midsld, это смещение сто четыре. Оба конца входят в диапазон, значит, от нуля до ста четырёх включительно, сто пять байт. В нашем тесте журнал так и говорит: отправляется сто пять байт. Первый кусок multisplit по той же метке равен ста четырём байтам: там метка стоит на границе.
|
||
|
||
Теперь код. Начало знакомо: если пакет не TCP, функция уходит, потом отсечка для входящих, то есть просьба к движку больше не звать этот вызов для входящих (выпуск «Lua внутри программы»). Новое одно: параметр pos обязателен, без него функция сразу вызывает ошибку, раньше работы с данными. Дальше берутся данные: указанный блоб, иначе собранное сообщение, иначе сам пакет.
|
||
|
||
Потом проверка проигрыша, как в выпуске «Сборка и повторный проигрыш»: при проигрыше большого приветствия функция действует только на первом проигрываемом пакете, но на всём собранном сообщении.
|
||
|
||
Затем resolve_range превращает список из двух меток в два номера, её разберём после кода. Затем строка, которая берёт срез. Срез в Lua включает обе границы, поэтому байт, названный второй меткой, тоже попадает в сегмент. В multisplit позиция «ноль» ничего не режет, а здесь это начало диапазона.
|
||
|
||
Остался отправляющий вызов. Функция отправки получает кусок и сдвиг порядкового номера, начало диапазона минус параметр seqovl. Результат отправки никто не проверяет. Потом функция кончается без оператора return, то есть ничего не возвращает, а для движка «нет значения» означает вердикт «пропустить».
|
||
|
||
Проверь себя. Параметр pos равен «ноль, один». Сколько байт уйдёт одним сегментом?
|
||
|
||
Два: байт со смещением ноль и байт со смещением один, обе границы входят. Так и получилось в нашем опыте: сегмент из двух байт.
|
||
|
||
![[wiki-anim-video06-s22-inclusive-range-code.svg|tcpseg: диапазон и код]]
|
||
|
||
Как список из двух меток становится диапазоном? Этим занимается «мерка диапазона», в коде resolve_range. Она написана не на Lua, а на языке Си.
|
||
|
||
Игрушка, выдуманная. Линейка и два флажка, «начало» и «конец». Если оба стоят, диапазон между ними. Если потерялся первый флажок, диапазон идёт от края линейки. Если потерялся второй, до противоположного края. Если потеряны оба, или флажки в обратном порядке, диапазона нет.
|
||
|
||
Теперь то же в коде. Сначала список разбирается, и меток должно быть ровно две. Если больше, ошибка «плохой список меток». Если меньше, ошибка «нужны две метки».
|
||
|
||
Потом каждая метка вычисляется тем же переводчиком меток, что в выпуске «Где резать: метки». Метка может не найтись, например имя сайта не обнаружено. Дальше правила как у линейки. Обе не найдены, ответ пустой. Не найдена первая, она становится началом данных. Не найдена вторая, она становится последним байтом. Первая позже второй, ответ пустой.
|
||
|
||
Иначе функция возвращает два числа с прибавленной единицей, как на мосту в Lua из выпуска «Где резать: метки»; журнал переводит их обратно на счёт с нуля.
|
||
|
||
Сначала обычный случай. Pos равно «host, midsld», обе метки найдены, смещения девяносто семь и сто четыре. Оба конца входят, значит, восемь байт: «www.exam». Это чтение кода, не опыт.
|
||
|
||
Теперь примеры в нашем тесте. Pos равно «host, девятьсот девяносто девять»: вторая метка за пределами данных и становится последним байтом. Диапазон со смещения девяносто семь по смещение двести сорок девять, сто пятьдесят три байта, затем идёт оригинал.
|
||
|
||
Pos равно «девятьсот девяносто девять, midsld». Теперь не найдена первая. Диапазон со смещения ноль по смещение сто четыре, сто пять байт, и оригинал следом.
|
||
|
||
Pos равно «midsld, host». Первая метка позже второй. В журнале написано «диапазон не удалось определить», и на проводе только оригинал.
|
||
|
||
И полезный случай. Pos равно «host, endhost минус один». Метка endhost на смещении сто двенадцать, это первый байт после имени. Поэтому диапазон идёт со смещения девяносто семь по сто одиннадцать включительно, ровно пятнадцать байт имени www.example.com. Минус один нужен из-за включённого конца: без него диапазон дошёл бы до смещения сто двенадцать включительно, шестнадцать байт, имя и ещё один. Это чтение кода, а не опыт.
|
||
|
||
![[wiki-anim-video06-s22-resolve-range.svg|resolve range: мерка диапазона]]
|
||
|
||
Итак, tcpseg вырезает один диапазон, оба конца включены, и отправляет его сегментом; слишком длинный нарежется на ломтики. Вердикта не даёт, оригинал пока жив. «Мерка» требует ровно две метки: потерянный первый конец заменяется началом данных, потерянный второй концом данных, а при обратном порядке или двух потерянных диапазона нет.
|
||
|
||
|
||
## tcpseg без drop: лишний сегмент
|
||
|
||
Что будет, если вызвать только tcpseg, без drop?
|
||
|
||
Игрушка, выдуманная. Курьер отправил первые сто пять слов телеграммы, и на столе осталась вся телеграмма, которую тоже отправляют. Адресат получает первые слова дважды.
|
||
|
||
В нашем тесте на проводе два пакета. Сначала сто пять байт с самого начала. Потом весь оригинал, двести пятьдесят байт, и у обоих пакетов один и тот же номер IP id: первый пакет, отправленный функцией, берёт номер оригинала.
|
||
|
||
Договоримся о словах. Когда сервер получил сто пять байт, он ждёт байт номер сто шесть, и в ответе стоит число сто шесть. Мы будем говорить: «подтверждено сто пять, жду сто шестой».
|
||
|
||
Получатель отвечает: подтверждено сто пять, жду сто шестой. Потом приходит оригинал, и ответ такой: подтверждено двести пятьдесят, жду двести пятьдесят первый, и ещё блок от первого до сто пятого байта. Блок лежит ниже подтверждения, это дублирующий SACK из первой части выпуска: первые сто пять байт пришли дважды.
|
||
|
||
Напомним, что такое блок ниже подтверждения. Это сообщение о дубликате, а не ошибка: байты, которые получатель уже имел, он отбросил. Приложение получило двести пятьдесят байт один раз, в этом запуске двумя чтениями: сначала сто пять, потом сто сорок пять. Это чтения приложения, а не пакеты.
|
||
|
||
Почему оригинал вообще идёт дальше? Потому что функция не возвращает вердикта, она кончается без return, и движок читает это как «пропустить». Параметра nodrop у неё нет: tcpseg ничего не выбрасывает, поэтому отменять нечего. В нашем тесте с этим параметром, вместе с drop, на проводе то же самое, что без него: параметр просто не читается.
|
||
|
||
Из этого следует важное. Без drop идея «остаток пришлёт ядро» не работает: остаток и так приходит в оригинале, вместе со всем, что уже отправлено. Чтобы ядру пришлось досылать, оригинал надо убрать. Этим и занимается drop.
|
||
|
||
![[wiki-anim-video06-s23-extra-segment.svg|tcpseg без drop: лишний сегмент]]
|
||
|
||
Тот же приём с диапазоном из двух байт. На проводе два байта, потом оригинал. Ответы: подтверждено два байта, жду третий; потом подтверждено двести пятьдесят, жду двести пятьдесят первый, и блок от первого до второго байта. Приложение сделало два чтения: два байта и двести сорок восемь.
|
||
|
||
А что если добавочный сегмент не должен дойти до сервера? Аргументы подмены полей действуют и на него. С аргументом времени жизни, равным единице, в нашем тесте добавочный сегмент на роутере исчезает: захват на роутере со стороны клиента его видит, захваты со стороны сервера и на самом сервере нет. Оригинал доходит, соединение обычное. Что с таким сегментом делает настоящее оборудование, мы не знаем.
|
||
|
||
Проверь себя. В опыте без drop, с обычным временем жизни: сколько раз первые сто пять байт приходят на сервер и сколько раз их получает приложение?
|
||
|
||
На сервер дважды, один раз в добавочном сегменте и один раз в оригинале. Приложение получает их один раз, повтор получатель отбрасывает и сообщает о нём дублирующим блоком.
|
||
|
||
![[wiki-anim-video06-s23-two-bytes-ttl1.svg|tcpseg без drop: два байта и TTL 1]]
|
||
|
||
Итак, tcpseg сам по себе добавляет сегмент, а оригинал идёт следом. Получатель сообщает о дубликате и отдаёт приложению данные один раз. Аргументы подмены полей действуют на добавочный сегмент.
|
||
|
||
|
||
## tcpseg и drop: первый запуск
|
||
|
||
Как заменить оригинал, а не добавлять к нему? Добавить вызов drop. Начнём с него.
|
||
|
||
Функция drop, «выбросить», очень короткая. Она проверяет направление и тип данных и возвращает вердикт «выбросить». Обратите внимание: умолчания у неё другие, чем у стратегий. Направление любое, а не только исходящее, тип данных любой, а не только известный. Но опция payload, поставленная в командной строке перед ней, всё равно ограничивает, какие пакеты до неё дойдут.
|
||
|
||
А если вызовы идут друг за другом, кто главнее? Когда вызовы идут друг за другом, движок сливает их ответы: «выбросить» сильнее «изменить», «изменить» сильнее «пропустить». Это разобрано в выпуске «Lua внутри программы». Движок не останавливается после «выбросить», и tcpseg выполняется всё равно. С обратным порядком вызовов провод в нашем тесте тот же.
|
||
|
||
Теперь первый запуск. tcpseg с pos «ноль, midsld» и drop. На проводе в нашем тесте сначала сто пять байт с самого начала. Оригинал, двести пятьдесят байт, на провод не выходит. Потом приходит ещё один пакет: сто сорок пять байт, со смещения сто пять. Эту часть отправила не наша функция: в журнале она проходит как обычный пакет, пропущенный без изменений.
|
||
|
||
Откуда она? Ядро клиента считает, что отправило все двести пятьдесят байт: наш выброс произошёл ниже уровня TCP, и ядро о нём не знает. Сервер подтвердил сто пять, остальное осталось без подтверждения, и ядро отправило его само. Это обычная повторная отправка TCP, по общим сведениям, ядро считает неподтверждённое потерянным. Что запустило её именно в этот момент, мы не выясняли, а время в нашей лаборатории зависит от диска с журналом, поэтому число не называем.
|
||
|
||
![[wiki-anim-video06-s24-drop-first-run.svg|drop и первый запуск tcpseg]]
|
||
|
||
Получатель в нашем тесте отвечает: подтверждено сто пять, жду сто шестой. Потом то же, но с блоком: клиент уже закрыл соединение, это сигнал закрытия, FIN, и он опередил недостающие байты. После остатка: подтверждено всё. Приложение сделало два чтения, сто пять и сто сорок пять байт. Запрос дошёл целиком.
|
||
|
||
Откуда сигнал закрытия? Наш лабораторный клиент после запроса закрывает соединение, и сигнал несёт номер двести пятьдесят один: сразу за последним байтом запроса. Но сервер держит пока только сто пять байт, и этот сигнал для него приходит из будущего. Поэтому он оформлен блоком, а не принят.
|
||
|
||
Но почему остаток не выбросили второй раз, ведь в командной строке есть drop? Он начался с середины приветствия. Проверка типа данных не узнаёт в нём приветствие и пишет «тип неизвестен». В нашем опыте перед обеими функциями стоит фильтр «только приветствие», поэтому обе не запускаются, и пакет идёт без изменений.
|
||
|
||
А без этого фильтра? Умолчание у drop «любые данные», так что по коду она выбросила бы и остаток. Опыта с таким запуском у нас нет. Руководство само советует ограничивать drop только известными данными.
|
||
|
||
Проверь себя. Сервер подтвердил сто пять байт из двухсот пятидесяти. Сколько байт ядро клиента считает неподтверждёнными и с какого номера они начинаются?
|
||
|
||
Сто сорок пять байт, с номера сто шесть. Именно их ядро и отправило. Для ядра это обычная потеря, а не стратегия.
|
||
|
||
![[wiki-anim-video06-s24-receiver-fin-filter.svg|Ответы получателя и фильтр]]
|
||
|
||
Итак, tcpseg с drop заменяет запрос первым куском. Остаток присылает ядро клиента, по общим сведениям, считая его потерянным. Остаток начинается с середины приветствия, при нашем фильтре не распознаётся как приветствие, и стратегия на него не действует.
|
||
|
||
|
||
## Чёрная дыра, seqovl числом и большое приветствие с tcpseg
|
||
|
||
Что будет, если диапазон начинается не с нуля, а с имени сайта?
|
||
|
||
Игрушка, выдуманная. Секретарь клиента не получил ответа и каждый раз сдаёт в отправку то же письмо с начала. Помощник каждый раз вырезает середину и выбрасывает письмо, и начало до адресата не доходит.
|
||
|
||
В нашем тесте так и вышло. Pos равно «host, endhost минус один», то есть ровно пятнадцать байт имени, и вызов drop. На проводе сначала сегмент в пятнадцать байт со смещения девяносто семь. Сервер отвечает: подтверждено ноль байт, жду первый, и показывает блок, потому что начала у него нет.
|
||
|
||
Клиент отправляет приветствие снова, это тоже обычная повторная отправка. Но теперь это полное приветствие в двести пятьдесят байт, и стратегия узнаёт его и снова вырезает те же пятнадцать байт. За время опыта сегмент ушёл шесть раз. Пять раз сервер отвечал, что ждёт первый байт. Потом сервер лаборатории сам закрыл соединение по своему таймауту в две секунды, и шестая отправка получила сброс.
|
||
|
||
Итог в нашем тесте: приложение на сервере не получило ни одного байта, а соединение стало чёрной дырой.
|
||
|
||
Посчитаем дыру. Блок, который держит сервер, это номера с девяносто восьмого по сто двенадцатый, пятнадцать байт имени. Не хватает номеров с первого по девяносто седьмой, девяноста семи байт начала. Они не дойдут, пока drop выбрасывает каждое полное приветствие.
|
||
|
||
Сравните с первым запуском: диапазон шёл от нуля до ста четырёх. Там сервер подтвердил сто пять байт, и ядро повторило один остаток, который не узнаётся как приветствие. Здесь подтверждение осталось на первом байте, ядро каждый раз повторяет приветствие целиком, и стратегия узнаёт его снова.
|
||
|
||
Сама функция работает правильно: вырезает то, что просили. Беда в сочетании диапазона посреди приветствия и повторной отправки, которая каждый раз приносит приветствие целиком. По коду то же будет с pos «host, девятьсот девяносто девять» и drop, но опыта с такой парой у нас нет.
|
||
|
||
Проверь себя. Сервер держит блок с номера девяносто восьмого по сто двенадцатый, а в подтверждении «жду первый». Сколько байт начала ему не хватает и с какого номера ядро будет их досылать?
|
||
|
||
Девяносто семь байт, номера с первого по девяносто седьмой. Досылать ядро будет с первого номера: подтверждение дальше первого не ушло.
|
||
|
||
![[wiki-anim-video06-s25-black-hole-six-sends.svg|Чёрная дыра: шесть отправок]]
|
||
|
||
Теперь два дополнения к tcpseg, уже не про дыру: параметр seqovl и большое приветствие.
|
||
|
||
Игрушка, выдуманная. Лента клеток, и слева прикладывают одну лишнюю клетку. Получатель уже расписался, что получил всё до первой настоящей клетки. Поэтому лишнюю он считает повтором старого и выбрасывает.
|
||
|
||
Параметр seqovl у tcpseg, в отличие от multidisorder, это число, а не метка. В начало сегмента вписывается столько байт накладки, и порядковый номер сдвигается влево на то же число. В нашем тесте pos был «ноль, минус один»: весь запрос, двести пятьдесят байт. Плюс один байт накладки, и вместе с drop ушёл один пакет в двести пятьдесят один байт, начинающийся на байт раньше первого.
|
||
|
||
Получатель ответил: подтверждено двести пятьдесят, жду двести пятьдесят первый, и показал блок из одного байта с номером ноль, на единицу левее первого байта, ниже подтверждения: дубликат. Приложение получило ровно двести пятьдесят байт. Это геометрия «слева от начала диапазона»; в нашем опыте диапазон начинался с нуля.
|
||
|
||
Теперь большое приветствие в двух проигрываемых пакетах. Tcpseg действует только на первом, но на всём собранном сообщении. На втором функция пишет «не действую на следующие проигрываемые пакеты» и больше ничего не делает.
|
||
|
||
Без drop в нашем тесте на проводе сначала сто пять байт, потом оба задержанных оригинала как были: тысяча четыреста сорок восемь байт и тридцать шесть. В этом запуске приложение сделало три чтения: сто пять, тысяча триста сорок три и тридцать шесть; число чтений зависит от моментов чтения.
|
||
|
||
Откуда тысяча триста сорок три? Первый задержанный оригинал, тысяча четыреста сорок восемь байт, начинается с нуля. Первые сто пять байт получатель уже имеет из добавочного сегмента, приложению отдаётся остаток. Вычитаем: тысяча триста сорок три. Проверим обратно: сто пять плюс тысяча триста сорок три дают тысяча четыреста сорок восемь, весь первый пакет ядра.
|
||
|
||
С drop иначе. Он срабатывает на каждом проигрываемом пакете, и в журнале выброшены оба. На проводе в нашем тесте сто пять байт и потом, от повторной отправки клиента, один пакет с остатком. Приложение сделало два чтения: сто пять байт и остаток.
|
||
|
||
Проверь себя. Большое приветствие, tcpseg с drop, pos «ноль, midsld». Сколько байт будет в повторной отправке?
|
||
|
||
Тысяча четыреста восемьдесят четыре минус сто пять, итого тысяча триста семьдесят девять. Ровно такой пакет мы увидели.
|
||
|
||
![[wiki-anim-video06-s25-seqovl-and-big-hello.svg|seqovl числом, большое приветствие]]
|
||
|
||
Итак, диапазон от имени в нашем опыте дал чёрную дыру: каждая повторная отправка снова режется, приложение получило ноль байт; от нуля такого не было. Seqovl у tcpseg это число слева от начала диапазона. На проигрыше tcpseg действует лишь на первом пакете, а drop выбрасывает каждый.
|
||
|
||
|
||
## Ошибки и угловые случаи: проверь себя таблицей
|
||
|
||
Соберём ошибки tcpseg в таблицу. Напомним правило, оно разобрано в выпуске «Lua внутри программы»: ошибка в Lua-функции не останавливает программу. Движок пишет в журнал «ошибка в стратегии, пакет пропускается без изменений», и оригинал уходит как есть.
|
||
|
||
В журнале при ошибке две строки. Сначала текст ошибки с именем файла и номером строки кода, на которой функция упала. Потом строка движка: ошибка, пакет пропущен без изменений. По первой строке видно, что именно не понравилось функции.
|
||
|
||
Игрушка, выдуманная. Приёмщик посылок с карточкой вопросов. Посылка заполнена неправильно, на ней штамп «не принято», и она уходит дальше как была. Ничего не ломается, но и наш приём не сработал.
|
||
|
||
![[wiki-anim-video06-s26-error-two-log-lines.svg|Ошибка: две строки журнала]]
|
||
|
||
Теперь шесть случаев, как вопросы. Каждый раз сначала подумайте, потом ответ. Пятый и шестой мы уже разбирали, повторим.
|
||
|
||
Первый. Вызов tcpseg без параметра pos. Что в журнале?
|
||
|
||
В журнале ошибка «pos не указан», с именем файла и строкой кода. Функция проверяет это раньше, чем берёт данные. Оригинал идёт без изменений.
|
||
|
||
Второй. Pos равно «host, midsld, endhost», три метки.
|
||
|
||
Ошибка «плохой список меток», из функции на Си. Комментарий в коде говорит «используются только первые две», но он неверный: три метки это ошибка.
|
||
|
||
Третий. Pos равно «host», одна метка.
|
||
|
||
Ошибка «нужны две метки»: одна метка диапазона не задаёт. Оригинал идёт как есть.
|
||
|
||
Четвёртый. Pos обычный, а seqovl задан буквами, а не числом.
|
||
|
||
Тоже ошибка, но хитрее: из букв число не получается, функция преобразования в число, tonumber, возвращает пустое значение, и сравнение числа с пустым значением падает. Пакет идёт без изменений.
|
||
|
||
Пятый. Pos равно «midsld, host». Метки в обратном порядке.
|
||
|
||
Здесь не ошибка. Функция пишет «диапазон не удалось определить» и ничего не отправляет. На проводе только оригинал. Отличие от первых четырёх: ошибки Lua нет, просто нет диапазона.
|
||
|
||
Что объединяет все пять. Во всех соединение осталось обычным, оригинал дошёл, и приложение получило запрос целиком. Ошибка в этой стратегии не рвёт связь, она просто отключает стратегию. Это мы видели в нашем тесте для каждой строки таблицы.
|
||
|
||
Все эти опыты шли без drop. А что с drop? По коду, при ошибке движок возвращает «пропустить» для всей цепочки, так что и с drop оригинал ушёл бы как есть. Но такого опыта у нас нет.
|
||
|
||
Как отличить ошибку от тихого случая? В журнале ошибки есть слово «эррор», по-английски «ошибка». У обратного порядка меток такой строки нет, только сообщение, что диапазон не определён.
|
||
|
||
Шестой. Pos равно «host, девятьсот девяносто девять». Это ошибка?
|
||
|
||
Нет. Вторая метка не найдена, и она чинится до последнего байта, как в сцене про мерку диапазона. Диапазон со смещения девяносто семь по смещение двести сорок девять, сто пятьдесят три байта, оригинал следом.
|
||
|
||
![[wiki-anim-video06-s26-six-cases-table.svg|Шесть случаев tcpseg]]
|
||
|
||
Итак, tcpseg требует ровно две метки и число в seqovl. Нарушение даёт ошибку, а оригинал идёт как есть. Обратный порядок меток не ошибка: диапазона нет, ничего не отправляется. Отличить одно от другого можно по слову «эррор» в журнале.
|
||
|
||
|
||
## Игрушечные инспекторы: три вида читателей и одна таблица
|
||
|
||
Теперь самое интересное и самое осторожное. Вопрос такой: кого обманывают наши приёмы? Настоящий ТСПУ мы не видели и не знаем, как он устроен. Поэтому ниже читатели, выдуманные нами, игрушечные инспекторы. По ним нельзя судить о настоящем оборудовании.
|
||
|
||
Первый вид, самый простой: правило в роутере, которое смотрит каждый пакет по отдельности. Оно ищет пятнадцать байт www.example.com на смещении девяносто семь. Без стратегии счётчик правила в нашем тесте равен шести: оригинал и пять повторных отправок, а сервер ничего не получил.
|
||
|
||
С multidisorder, с seqovl, привязанным к host, и с tcpseg и drop счётчик равен нулю, а сервер получил весь запрос.
|
||
|
||
В нашей игрушечной модели такое правило обходится теми нарезками, которые мы пробовали: ему важно, лежит ли имя на его месте, а порядок отправки значения не имеет.
|
||
|
||
Напомним: в выпуске «multisplit: резать на части» над записью работали шесть моделей. Здесь набор другой, подобранный под порядок отправки, и названия у читателей наши собственные. «Пакетный» похож на первую модель оттуда, «склейщик» на четвёртую, а остальные три мы определяем заново.
|
||
|
||
Второй вид: пять читателей, которые разбирают готовый захват. Договоримся: «видит» значит, что имя появилось в собранных данных, и тогда приём не сработал. «Слеп» значит, что читателя приём обошёл.
|
||
|
||
«Пакетный» смотрит каждый пакет отдельно.
|
||
|
||
«Склейщик» склеивает данные в порядке прихода.
|
||
|
||
«Первый записал» собирает по порядковым номерам, и если байты перекрываются, остаётся записанное первым.
|
||
|
||
«Последний записал» тоже собирает по номерам, но остаётся записанное последним.
|
||
|
||
«Строго по порядку» принимает сегмент, только если он начинается не позже ожидаемого байта, а всё из будущего игнорирует.
|
||
|
||
Читаем таблицу по строкам. Первая строка, multisplit: слеп только «пакетный», имя в двух пакетах. Остальным порядок не мешает: куски идут по возрастанию.
|
||
|
||
Multidisorder: слепы «пакетный», «склейщик» и «строго по порядку». «Пакетный» не видит имя целиком, «склейщик» склеивает в порядке прихода, «строго по порядку» игнорирует кусок из будущего. Оба, кто собирает по номерам, имя видят. В нашей игрушечной модели перестановка обманывает тех, кто читает порядок прихода.
|
||
|
||
Seqovl, привязанный к host, добавляет перекрытие. Теперь слепы почти все: имя видит только «последний записал». У «первого записал» на месте начала имени лежат нулевые байты накладки.
|
||
|
||
И tcpseg с drop: слеп только «пакетный», остальные видят, потому что повторная отправка клиента приносит остаток имени.
|
||
|
||
Третий вид самый подробный из наших трёх: второй nfqws2 в роутере, наша программа на Lua. Для каждого соединения она собирает данные клиента по одному из четырёх правил и ищет имя, а когда находит, выбрасывает этот пакет и все дальнейшие пакеты клиента с данными. Результат считаем по числу байт, которые получил сервер.
|
||
|
||
Без стратегии сервер получил ноль. Multisplit против «первый записал» и «строго по порядку»: сто четыре, выброшен кусок, который достраивает имя. Multidisorder против «первый записал» и «последний записал»: ноль, выброшен короткий кусок. Против «строго по порядку» и «склейщика»: двести пятьдесят, имя не собралось.
|
||
|
||
С seqovl против «первый записал»: двести пятьдесят, он видел накладку вместо имени. Против «последний записал»: ноль. С tcpseg и drop против «первый записал»: сто пять байт и обрыв, потому что повторная отправка достраивает имя, и читатель выбрасывает её и всё, что после.
|
||
|
||
Ограничения игрушек. У них нет времени и таймаутов, нет учёта дыр, нет проверки времени жизни и контрольной суммы, нет разбора TLS: это обычный поиск подстроки. Есть только одно соединение, и после первой находки выбрасывается всё.
|
||
|
||
![[wiki-anim-video06-s27-router-rule-and-second-nfqws2.svg|Два читателя в роутере]]
|
||
|
||
Проверь себя. Стратегия multidisorder с pos по умолчанию, то есть два: куски в двести сорок восемь байт и два. Кто из пяти читателей видит имя?
|
||
|
||
Четыре из пяти. Имя целиком лежит в длинном куске, который идёт первым. Слеп только «строго по порядку»: он игнорирует кусок из будущего.
|
||
|
||
![[wiki-anim-video06-s27-five-readers-table.svg|Пять читателей игрушки]]
|
||
|
||
Итак, в нашей игрушечной модели нарезка вместе с перестановкой обманывает читателей, которые смотрят пакеты по одному, склеивают по порядку прихода или игнорируют будущее, и не обманывает собирающих по номерам. Перекрытие дополнительно обманывает «первый записал», но не «последний записал». Tcpseg с drop не помогает против собирающих: повторная отправка доставляет остаток. Какой читатель настоящий, мы не знаем.
|
||
|
||
|
||
## Итог выпуска, что доказано и что нет, и что дальше
|
||
|
||
Подведём итог выпуска. Мы разобрали четыре стратегии, и на экране они стоят рядом.
|
||
|
||
Multisplit, которому посвящён выпуск «multisplit: резать на части», режет сообщение по меткам и отправляет куски по порядку. Multidisorder режет всё сообщение один раз и отправляет с конца, а внутри длинного куска, если он не влезает в пакет, ломтики идут вверх. Параметр seqovl в нём это метка внутри самого сообщения, а позиция «ноль» не режет вообще.
|
||
|
||
Старая функция, multidisorder_legacy, режет каждый проигрываемый пакет отдельно и сохраняет разбивку клиента. Метка seqovl у неё считается внутри текущего пакета ядра. Кусок без разреза она отправляет копией со всеми аргументами подмены полей, параметров nodrop и blob не читает, метка на границе кусков у неё ничего не режет.
|
||
|
||
И tcpseg с drop. Один диапазон, оба конца включены, и он уходит одним сегментом, если помещается в пакет. Оригинал выбрасывает drop, а остаток в нашем тесте присылает ядро клиента. А в нашем опыте, когда диапазон начался с имени, а не с нуля, сервер за всё время опыта так и не получил начало.
|
||
|
||
![[wiki-anim-video06-s28-four-strategies-side-by-side.svg|Четыре стратегии рядом]]
|
||
|
||
Четыре различия, которые легче всего перепутать. Первое: в списке разрезов позиция «ноль» не режет, а «один» режет, зато у tcpseg ноль законное начало диапазона. Второе: seqovl у multidisorder это метка внутри сообщения, а у multisplit и tcpseg число слева от начала диапазона. Третье: у tcpseg конец диапазона входит в диапазон. Четвёртое: «одним сегментом» верно, пока диапазон помещается в пакет, длинный нарежется на ломтики.
|
||
|
||
Теперь честная граница. Что показала лаборатория. Код, который мы читали строка за строкой, и провод: в нашем тесте на одном получателе, на сервере Linux, ядро версии шесть точка восемнадцать, в наших опытах приложение ни разу не получило неверный байт. Перестановка, перекрытие и дубликат у него вызывали только ответы с блоками и подтверждения.
|
||
|
||
Чего лаборатория не показала. Как поведёт себя настоящий инспектор: мы про него ничего не знаем. Как ведут себя другие получатели. В руководстве написано, что перекрытие заменяется настоящими байтами на всех системах кроме Windows: мы проверили только Linux. Не проверяли мы и протокол IP шестой версии, UDP, а также параметры повторов, фрагментации IP и испорченной контрольной суммы с этими функциями. Всё, что мы показали о «срабатывает», касается только наших игрушечных читателей.
|
||
|
||
Что дальше. Приёмы этого выпуска резали, переставляли и перекрывали байты самого запроса. Следующий выпуск, «Фейк и его условия», о другом классе приёмов: о подделках. Программа отправляет рядом лишний пакет, fake, и снабжает его условием, чтобы он дошёл не до всех на пути. Намёк мы уже видели: добавочный сегмент со временем жизни один исчезал на роутере. В нём разберём такие условия по одному: время жизни и другие, и посмотрим на опыты нашей лаборатории.
|
||
|
||
![[wiki-anim-video06-s28-boundary-and-next.svg|Различия, граница, что дальше]]
|
||
|
||
Итак, в этом выпуске стратегии отправляют куски не по порядку и с перекрытием. Когда запрос доходил, приложение получало его целиком, а получатель в нашем тесте отвечал блоками и подтверждениями. Старая функция режет по пакетам и хранит ловушки, tcpseg с drop оставляет остаток ядру клиента. Игрушечные читатели ведут себя по-разному, а настоящий нам неизвестен.
|
||
|
||
## См. также
|
||
|
||
- [[05-multisplit-rezat-na-chasti|multisplit: резать на части]] — предыдущее видео серии
|
||
- [[multidisorder]] — справочная статья о функции
|
||
- [[multidisorder_legacy]] — вариант, совместимый с nfqws1
|
||
- [[ts-and-fooling]] — подмена полей пакета
|
||
|
||
---
|
||
|
||
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
|
||
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: [исходник этой заметки](https://git2.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/Zapret2/video/06-peremeshat-disorder.md) · [скачать весь репозиторий одним zip-архивом](https://git2.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).
|