
Первая часть: Ethernet, ARP, IPv4 и ICMPv4
Вторая часть: основы TCP и Handshake
В прошлом посте мы узнали о заголовках TCP и о том, как устанавливается соединение между двумя сторонами.
В этом посте мы изучим передачу данных по TCP и способ управления ею.
Также мы создадим интерфейс сетевого стека, который приложения смогут использовать для передачи данных по сети. Потом этот Socket API мы применим, чтобы наш пример приложения смог отправить простой HTTP-запрос веб-сайту.
Блок управления передачей
Лучше всего начать обсуждение управления передачей данных по TCP с определения переменных, фиксирующих состояние потока данных.
Если вкратце, TCP должен отслеживать передаваемые им последовательности данных и получаемые подтверждения. Для этого у каждого открытого соединения инициализируется структура данных Transmission Control Block (TCB).
Переменные для исходящей (отправляющей) стороны будут такими:
Переменные последовательности передачи
SND.UNA - передача не подтверждена
SND.NXT - передать следующим
SND.WND - окно передачи
SND.UP - передать указатель срочности
SND.WL1 - порядковый номер сегмента, использованный при последнем обновлении окна
SND.WL2 - порядковый номер подтверждения, использованный при последнем обновлении окна
ISS - начальный порядковый номер для передачи
Для получающей стороны записываются следующие данные:
Переменные последовательности получения
RCV.NXT - принять следующим
RCV.WND - окно приема
RCV.UP - прием указателя срочности
IRS - начальный порядковый номер для приема
Дополнительно определяются следующие переменные текущего обрабатываемого сегмента:
Переменные текущего сегмента
SEG.SEQ - порядковый номер сегмента
SEG.ACK - порядковый номер сегмента подтверждения
SEG.LEN - длина сегмента
SEG.WND - окно сегмента
SEG.UP - указатель срочности сегмента
SEG.PRC - значение предпочтений для сегмента
Совместно эти переменные составляют основную часть логики управления TCP для конкретного соединения.
Передача данных по TCP
После установления соединения начинается явная обработка потока данных. Для базового отслеживания состояния важны три переменные TCB:
SND.NXT — отправитель будет отслеживать в SND.NXT следующий используемый порядковый номер.
RCV.NXT — получатель записывает в RCV.NXT следующий порядковый номер для ожидания.
SND.UNA — отправитель записывает в SND.UNA самый старый неподтверждённый порядковый номер.
Если период времени, в который TCP управляет передачей данных и не происходит передача, будет достаточным, то все три переменные должны оказаться равными.
Например, когда A решает отправить сегмент с данными хосту B, происходит следующее:
TCP A отправляет сегмент и увеличивает SND.NXT в собственных записях (TCB).
TCB B получает сегмент и подтверждает его, увеличивая RCV.NXT и отправляя ACK.
TCB A получает ACK и увеличивает SND.UNA.
Величина инкремента переменных — это длина данных в сегменте.
И это базис логики управления TCP в передаче данных. Давайте при помощи популярной утилиты для перехвата сетевого трафика tcpdump(1) посмотрим, как это выглядит:
[saminiir@localhost level-ip]$ sudo tcpdump -i any port 8000 -nt
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [S], seq 1525252, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [S.], seq 825056904, ack 1525253, win 29200, options [mss 1460], length 0
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 1, win 29200, length 0
Адрес 10.0.0.4 (хост A) инициирует соединение из порта 12000 к хосту 10.0.0.5 (хост B), прослушивающему порт 8000.
После трёхстороннего handshake соединение устанавливается, и их внутреннее состояние сокета TCP принимает значение ESTABLISHED. Изначальный порядковый номер для A — 1525252, а для B — 825056904.
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [P.], seq 1:18, ack 1, win 29200, length 17
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [.], ack 18, win 29200, length 0
Хост A отправляет сегмент с 17 байтами данных, который хост B подтверждает сегментом ACK. Для большей читаемости tcpdump по умолчанию показывает относительные порядковые номера. Таким образом, ack 18 — это на самом деле 1525253 + 17.
Внутри TCP получающего хоста (B) переменная RCV.NXT увеличилась на 17.
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 1, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [P.], seq 1:18, ack 18, win 29200, length 17
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 18, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [P.], seq 18:156, ack 18, win 29200, length 138
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 156, win 29200, length 0
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [P.], seq 156:374, ack 18, win 29200, length 218
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 374, win 29200, length 0
Взаимодействие между отправкой данных и подтверждением продолжается. Как мы видим, у сегментов с длиной 0 задан только флаг ACK, но порядковые номера подтверждения чётко выполняют инкремент на основании ранее полученной длины сегмента.
IP 10.0.0.5.8000 > 10.0.0.4.12000: Flags [F.], seq 374, ack 18, win 29200, length 0
IP 10.0.0.4.12000 > 10.0.0.5.8000: Flags [.], ack 375, win 29200, length 0
Хост B генерирует сегмент FIN, сообщая хосту A, что у него больше нет данных для отправки. Хост A в свою очередь подтверждает это.
Для завершения соединения хост A также должен сигнализировать, что у него больше нет данных для отправки.
Прекращение соединения TCP
Закрытие соединения TCP — это столь же сложная операция; оно может быть завершено принудительно (RST) или по взаимному соглашению (FIN).
Базовый сценарий выглядит так:
Активная закрывающая сторона отправляет сегмент FIN.
Пассивная закрывающая сторона подтверждает это, отправляя сегмент ACK.
Пассивная сторона начинает собственную операцию закрытия (когда у неё больше нет данных для отправки), по сути, становясь активной.
После того, как обе стороны отправят друг другу FIN и подтвердят их с обоих направлений, соединение закрывается.
Очевидно, что для закрытия соединения TCP требуется четыре сегмента, а не три, как при установлении соединения (трёхсторонний handshake).
Кроме того, TCP — двунаправленный протокол, поэтому может случиться так, что другая сторона объявит, что у неё больше нет данных для отправки, но останется онлайн, ожидая входящих данных. Это называется «полузакрытием» (TCP Half-close).
Ненадёжность сетей с пакетной передачей повышает сложность завершения соединения — сегменты FIN могут пропадать или не быть переданы намеренно, из-за чего соединение оказывается во взвешенном состоянии. Например, в Linux параметр ядра tcp_fin_timeout
определяет количество секунд ожидания протоколом TCP окончательного пакета FIN перед принудительным завершением соединения. Это нарушает спецификацию, но необходимо для предотвращения Denial of Service (DoS)1.
Для прекращения соединения нужен сегмент с заданным флагом RST. Сброс может происходить по множеству причин. Самые распространённые:
Соединение запрашивает несуществующий порт или интерфейс
У второй реализации TCP произошёл сбой и она оказалась в рассинхронизированном состоянии соединения
Попытки помещать существующим соединениям2
Таким образом, при удачной передаче данных TCP сегмент RST не используется.
Socket API
Чтобы мы могли использовать сетевой стек, приложениям нужно предоставить некий интерфейс. Самый известный из них — это BSD Socket API, появившийся в релизе 4.2 BSD UNIX 1983 года3. Socket API в Linux совместим с BSD Socket API4.
Сокет резервируется из сетевого стека вызовом socket(2), которому передаются в качестве параметров тип сокета и протокол. Чаще всего используются значения типа AF_INET и домена SOCK_STREAM. Они стандартны для сокета TCP-over-IPv4.
После успешного резервирования сокета TCP из сетевого стека он подключается к удалённой конечной точке. При этом используется connect(2), и его вызов запускает TCP handshake.
В дальнейшем мы можем просто выполнять write(2) и read(2) данных из сокета.
Сетевой стек будет заниматься обработкой очередей, повторной передачей, контролем ошибок и пересборкой данных в потоке TCP. Для приложения внутренняя работа TCP по большей части непрозрачна. Единственное, на что может надеяться приложение — что TCP принял на себя ответственность по отправке и получению потока данных и что он будет сообщать приложению о неожиданном поведении через socket API.
Например, давайте взглянем на системные вызовы, выполняемые простым curl(1):
$ strace -esocket,connect,sendto,recvfrom,close curl -s example.com > /dev/null
...
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("93.184.216.34")}, 16) = -1 EINPROGRESS (Operation now in progress)
sendto(3, "GET / HTTP/1.1\r\nHost: example.co"..., 75, MSG_NOSIGNAL, NULL, 0) = 75
recvfrom(3, "HTTP/1.1 200 OK\r\nCache-Control: "..., 16384, 0, NULL, NULL) = 1448
close(3) = 0
+++ exited with 0 +++
Мы видим, что Socket API вызывается с strace(1) — инструментом для трассировки системных вызовов и сигналов. Процесс состоит из следующих этапов:
socket открывает сокет с указанием типа IPv4/TCP.
connect запускает TCP handshake. Функции передаются адрес назначения и порт.
После успешного установления соединения для записи данных в сокет используется sendto(3) — в данном случае это обычный запрос HTTP GET.
Далее curl считывает входящие данные при помощи recvfrom.
Внимательный читатель заметит, что системные вызовы чтения или записи не использовались. Причина этого заключается в том, что сам socket API не содержит этих функций, но допускается использование и обычных операций ввода-вывода. Цитата из man socket(7)4:
Кроме того, для чтения и записи данных могут использоваться стандартные операции ввода-вывода наподобие write(2), writev(2), sendfile(2), read(2) и readv(2).
Сам Socket API содержит множество функций для простой записи и чтения данных. Это усложняется наличием семейства функций ввода-вывода, которые тоже могут применяться для манипуляций с дескриптором файла сокета.
Тестируем наш Socket API
Теперь, когда наш сетевой стек предоставляет интерфейс сокетов, мы можем писать для него приложения.
Популярный инструмент curl
используется для передачи данных по различным протоколам. Мы можем воссоздать поведение HTTP GET в curl
, написав минимальную реализацию:
$ ./lvl-ip curl example.com 80
...
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is established to be used for illustrative examples in documents. You may use this
domain in examples without prior coordination or asking for permission.</p>
<p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
Отправка запроса HTTP GET лишь минимально загружает наш сетевой стек.
Заключение
По сути, мы реализовали рудиментарный TCP с простым управлением данными и создали интерфейс, которым могут пользоваться приложения.
Однако передача данных по TCP — непростая задача. Пакеты при передаче могут повреждаться, менять порядок или теряться. Кроме того, передача данных может перегружать произвольные элементы сети.
Для учёта этого в процесс передачи данных по TCP необходимо включить более сложную логику. В пятой части мы изучим механизмы управления окнами TCP и таймаута повторной передачи TCP, которые позволяют лучше справляться с более сложными ситуациями.
Источники
Часть 5: повторная передача TCP
Итак, у нас есть стек TCP/IP, способный общаться с другими хостами в Интернете. Пока реализация оставалась довольно простой, но в ней недостаёт важнейшего аспекта: надёжности.
Наш TCP не гарантирует целостности потока данных, передаваемых им приложениям. Даже установка соединения может завершиться сбоем, если пакеты handshake при передаче потеряются.
Наша следующая основная задача при создании с нуля стека TCP/IP — добавление надёжности и возможностей управления.
Автоматический запрос повторной передачи
Базис множества надёжных протоколов — это концепция Automatic Repeat reQuest (ARQ)1.
В ARQ получатель отправляет подтверждения полученных им данных, а отправитель повторно передаёт данные, для которых не получил подтверждения приёма.
Как мы уже говорили, TCP хранит в памяти порядковые номера передаваемых данных и отвечает подтверждениями. Передаваемые данные помещаются в очередь повторной передачи, и запускаются таймеры, связанные с данными. Если до истечения таймера не будет получено подтверждение для последовательности данных, происходит повторная передача.
Очевидно, что надёжность TCP построена на принципах ARQ. Однако подробные реализации ARQ сложны. Трудно отвечать даже на простые вопросы типа «как долго отправитель должен ждать подтверждения?», особенно если требуется максимальная производительность. Расширения TCP наподобие Selective Acknowledgments (SACK)2 частично решают проблемы эффективности, подтверждая данные вне порядка и избегая ненужной передачи туда-обратно.
Повторная передача TCP
Повторная передача в TCP описывается исходной спецификацией3 так:
Когда TCP передаёт содержащий данные сегмент, он помещает их копию в очередь повторной передачи и запускает таймер; при получении подтверждения данных сегмент удаляется из очереди. Если подтверждение не получено до истечения таймера, сегмент передаётся повторно.
Однако исходная формула вычисления таймаута повторной передачи считается неадекватной, поскольку сетевые окружения могут быть очень разными. Современный «стандартный способ»4, описанный Ваном Джейкобсоном5, и актуальную кодифицированную спецификацию можно найти в RFC62986.
Базовый алгоритм относительно прост. С целью вычисления таймаута для конкретного отправителя TCP определяются переменные состояния:
srtt
— сглаженное значение времени передачи туда-обратно для усреднения времени передачи сегмента туда-обратно (round-trip time, RTT)rttvar
содержит колебания времени RTTrto
хранит таймаут повторной передачи, например, в миллисекундах
Если вкратце, то srtt
работает в качестве фильтра низких частот для последовательных RTT. Из-за возможных сильных колебаний RTT rttvar
используется для выявления таких изменений, чтобы они не искажали усредняющую функцию. Кроме того, подразумевается разрешение часов в G секунд.
Согласно RFC62986, вычисления состоят из следующих этапов:
До первого измерения RTT:
rto = 1000ms
При первом замере RTT R:
srtt = R rttvar = R/2 rto = srtt + max(G, 4*rttvar)
При последующих замерах:
alpha = 0.125 beta = 0.25 rttvar = (1 - beta) * rttvar + beta * abs(srtt - r) srtt = (1 - alpha) * srtt + alpha * r rto = srtt + max(g, 4*rttvar)
Если после вычисления
rto
, она оказывается меньше 1 секунды, то происходит округление до 1 секунды. Возможно указание максимальной величины, но она должна быть не менее 60 секунд
Разрешение часов реализаций TCP традиционно было довольно грубым, в интервале от 500 мс до 1 секунды. Однако в современных системах наподобие Linux используется разрешение часов в 1 миллисекунду4.
Стоит отметить, что рекомендуется оставлять RTO всегда равным как минимум 1 секунде. Это нужно для защиты от ложных повторных передач, например, когда сегмент повторно передаётся слишком часто, вызывая перегрузки в сети. На практике многие реализации округляют эту переменную до значений меньше секунды: в Linux используется 200 миллисекунд.
Алгоритм Карна
Алгоритм Карна7 — это обязательно применяемый алгоритм, предотвращающий ложные результаты замеров RTT.
Иными словами, отправитель по TCP отслеживает, был ли отправленный им сегмент передан повторно, и для таких подтверждений пропускает процедуру RTT. Это логично, ведь в противном случае отправитель не мог бы отличить подтверждения оригинала и повторно переданного сегмента.
Однако при использовании опции метки времени TCP значение RTT может замеряться для каждого сегмента ACK. Об опции метки времени TCP мы поговорим в отдельном посте.
Управляем таймером RTO
Управлять таймером повторной передачи относительно просто. RFC6298 рекомендует использовать следующий алгоритм:
Если при отправке сегмента данных таймер RTO не запущен, активировать его со значением таймаута rto
Когда все ожидающие обработки сегменты данных были подтверждены, отключать таймер RTO
При получении ACK для новых данных перезапускать таймер RTO со значением rto
Когда таймер RTO истекает:
Повторно передать самый ранний неподтверждённый сегмент
Увеличить таймер RTO на коэффициент 2, то есть (rto = rto * 2)
Запустить таймер RTO
Дополнительно при увеличении значения RTO и успешном выполнении последующих замеров значение RTO может существенно уменьшиться. При увеличении таймера и ожидании подтверждения реализация TCP может сбросить srtt
и rttvar
6.
Запрос повторной передачи
Обычно TCP не использует таймеры отправителя для исправления утерянных пакетов. Получающая сторона также может сообщить отправителю, что сегменты нужно отправить повторно.
Duplicate acknowledgment — это алгоритм подтверждения сегментов вне порядка, но по порядковому номеру последнего идущего по порядку сегмента. После трёх duplicate acknowledgment отправитель должен осознать, что ему нужно повторно передать сегмент, объявленный в duplicate acknowledgment.
Selective Acknowledgment (SACK) — это более сложная версия duplicate acknowledgment. Это опция TCP, при которой получатель может закодировать полученные последовательности в свои подтверждения. Так отправитель мгновенно замечает все утерянные сегменты и отправляет их заново. Опцию TCP SACK мы обсудим в другом посте.
Проверка
Теперь, когда мы разобрались с концепциями и общими алгоритмами, давайте посмотрим, как повторные передачи TCP выглядят на практике.
Поменяем правила файрвола так, чтобы пакеты отбрасывались после установки соединения, и попытаемся получить главную страницу HackerNews:
$ iptables -I FORWARD --in-interface tap0 \
-m conntrack --ctstate ESTABLISHED \
-j DROP
$ ./tools/level-ip curl news.ycombinator.com
curl: (56) Recv failure: Connection timed out
Наблюдая за трафиком соединения, мы видим, что при повторной передаче HTTP GET интервал увеличивается примерно вдвое:
[saminiir@localhost ~]$ sudo tcpdump -i tap0 host 10.0.0.4 -n -ttttt
00:00:00.000000 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [S], seq 3975419138, win 44477, options [mss 1460], length 0
00:00:00.004318 IP 104.20.44.44.80 > 10.0.0.4.41733: Flags [S.], seq 4164704437, ack 3975419139, win 29200, options [mss 1460], length 0
00:00:00.004534 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [.], ack 1, win 44477, length 0
00:00:00.011039 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:00:01.094237 IP 104.20.44.44.80 > 10.0.0.4.41733: Flags [S.], seq 4164704437, ack 3975419139, win 29200, options [mss 1460], length 0
00:00:01.094479 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [.], ack 1, win 44477, length 0
00:00:01.210787 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:00:03.607225 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:00:08.399056 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:00:18.002415 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:00:37.289491 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:01:15.656151 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
00:02:32.590664 IP 10.0.0.4.41733 > 104.20.44.44.80: Flags [P.], seq 1:85, ack 1, win 44477, length 84: HTTP: GET / HTTP/1.1
Проверять увеличение интервалов повторной передачи и потери ответов от получателя просто, но как насчёт ситуации, когда повторные передачи выполняются только для некоторых сегментов? Для оптимальной производительности алгоритм RTO должен «приходить в норму» после того, как соединение будет признано беспроблемным.
Давайте зададим правило файрвола, блокирующее только шестой пакет из 6000 байтов:
$ iptables -I FORWARD --in-interface tap0 \
-m connbytes --connbytes 6 \
--connbytes-dir original --connbytes-mode packets \
-m quota --quota 6000 -j DROP
Теперь, если мы попытаемся передать данные, то TCP распознает потерю связи и завершит её. Давайте отправим 6009 байтов:
$ ./tools/level-ip curl -X POST http://httpbin.org/post \
-d "payload=$(perl -e "print 'lorem ipsum ' x500")"
Пошагово рассмотрим этапы соединения, момент срабатывания повторной передачи и изменение значений RTO. Ниже представлен изменённый вывод tcpdump output с комментариями о внутреннем состоянии сокета TCP:
00.000000 10.0.0.4.49951 > httpbin.org.80: [S], seq 1, options [mss 1460]
00.120709 httpbin.org.80 > 10.0.0.4.49951: [S.], seq 1, ack 1, options [mss 8961]
00.120951 10.0.0.4.49951 > httpbin.org.80: [.], ack 1
- Соединение установлено, значение RTO TCP для 10.0.0.4:49951 равно 1000 миллисекундам.
00.122686 10.0.0.4.49951 > httpbin.org.80: [P.], seq 1:174, ack 1: POST /post
00.242564 httpbin.org.80 > 10.0.0.4.49951: [.], ack 174
01.141287 10.0.0.4.49951 > httpbin.org.80: [.], seq 174:1634, ack 1: HTTP
01.141386 10.0.0.4.49951 > httpbin.org.80: [.], seq 1634:3094, ack 1: HTTP
01.141460 10.0.0.4.49951 > httpbin.org.80: [.], seq 3094:4554, ack 1: HTTP
01.263301 httpbin.org.80 > 10.0.0.4.49951: [.], ack 1634
01.265995 httpbin.org.80 > 10.0.0.4.49951: [.], ack 3094
- Пока всё хорошо, удалённый хост подтвердил наш HTTP POST
и начало полезной нагрузки. Значение RTO равно 336 мс.
01.526797 10.0.0.4.49951 > httpbin.org.80: [.], seq 3094:4554, ack 1: HTTP
02.259425 10.0.0.4.49951 > httpbin.org.80: [.], seq 3094:4554, ack 1: HTTP
03.735553 10.0.0.4.49951 > httpbin.org.80: [.], seq 3094:4554, ack 1: HTTP
- Началась утеря связи, вызванная нашим правилом iptables.
TCP должен многократно повторно передавать сегмент.
Значение RTO сокета увеличивается:
01.526797: 618ms
02.259425: 1236ms
03.735553: 2472ms
06.692867 10.0.0.4.49951 > httpbin.org.80: [.], seq 3094:4554, ack 1: HTTP
06.819115 httpbin.org.80 > 10.0.0.4.49951: [.], ack 4554
- Наконец удалённый хост отвечает. Значение RTO увеличилось до 4944 мс.
Здесь приводится в действие алгоритм Карна: новое значение RTO не может быть замерено
с повторно переданным сегментом, так что мы пропускаем его.
06.819356 10.0.0.4.49951 > httpbin.org.80: [.], seq 4554:6014, ack 1: HTTP
06.819442 10.0.0.4.49951 > httpbin.org.80: [P.], seq 6014:6182, ack 1: HTTP
06.948678 httpbin.org.80 > 10.0.0.4.49951: [.], ack 6014
06.948917 httpbin.org.80 > 10.0.0.4.49951: [.], ack 6182
- Теперь мы получаем ACK с первой попытки, и сеть относительно надёжна.
Алгоритм Карна позволяет нам замерить новое RTO:
06.948678: 309ms
06.948917: 309ms
06.948942 httpbin.org.80 > 10.0.0.4.49951: [P.], seq 1:26, ack 6182: HTTP 100 Continue
06.949014 httpbin.org.80 > 10.0.0.4.49951: [.], seq 26:1486, ack 6182: HTTP/1.1 200 OK
06.949145 10.0.0.4.49951 > httpbin.org.80: [.], ack 26
06.949816 httpbin.org.80 > 10.0.0.4.49951: [.], seq 1486:2946, ack 6182: HTTP
06.949894 httpbin.org.80 > 10.0.0.4.49951: [.], seq 2946:4406, ack 6182: HTTP
06.950029 10.0.0.4.49951 > httpbin.org.80: [.], ack 2946
06.950030 httpbin.org.80 > 10.0.0.4.49951: [.], seq 4406:5866, ack 6182: HTTP
06.950161 httpbin.org.80 > 10.0.0.4.49951: [P.], seq 5866:6829, ack 6182: HTTP
06.950287 10.0.0.4.49951 > httpbin.org.80: [.], ack 5866
06.950435 10.0.0.4.49951 > httpbin.org.80: [.], ack 6829
06.958155 10.0.0.4.49951 > httpbin.org.80: [F.], seq 6182, ack 6829
07.082998 httpbin.org.80 > 10.0.0.4.49951: [F.], seq 6829, ack 6183
07.083253 10.0.0.4.49951 > httpbin.org.80: [.], ack 6830
- Обмен данными и соединение завершены.
В замерах RTO не происходит существенных изменений.
Заключение
Повторная передача — неотъемлемая часть надёжной реализации TCP. Протокол TCP должен быть устойчивым и производительным в меняющихся условиях сети, в которой, например, могут резко возрасти задержки или произойти кратковременная блокировка сетевого пути.
Исходный код для этого проекта выложен на GitHub.