Как работают Send Window и Receive Window в TCP

Как работают Send Window и Receive Window в TCP


Скользящее окно TCP состоит из окна отправки и окна приёма. Давайте начнём с более сложной части - окна отправки.

Окно отправки (aka Send Window)

Диаграмма выше - это снимок с точки зрения отправителя. Мы можем разделить данные на 4 категории:

  1. Байты, которые были отправлены и подтверждены (синие)
  2. Байты, которые были отправлены, но ещё не подтверждены (жёлтые)
  3. Байты, которые ещё не отправлены, но приёмник готов их получить (зелёные)
  4. Байты, которые ещё не отправлены, и приёмник пока не готов их принять (серые)

Категория 3 также называется usable window - это та часть окна, которую отправитель может прямо сейчас использовать.

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

Usable window может быть пустым, если, например, отправитель передал байты 21-25 и полностью использовал все доступные байты в usable window. При этом само окно отправки остаётся на месте.

Когда отправитель получает ACK за байты 16-19, окно отправки сдвигается вправо на 4 байта. Появляется обновлённое usable window, и следующие байты в очереди могут быть отправлены.

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

  • SND.WND - это Send Window, окно отправки
  • SND.UNA - Send Unacknowledged pointer, указывает на первый байт в окне отправки, который ещё не подтверждён
  • SND.NXT - Send Next pointer, указывает на первый байт, который можно отправить - то есть на начало usable window

Опираясь на эти определения, мы можем выразить размер usable window с помощью формулы:

Окно приёма (aka Receive Window)

Окно приёма делится на три категории:

  1. Байты, которые уже получены и подтверждены
  2. Байты, которые ещё не получены, но передатчику разрешено их отправить
  3. Байты, которые ещё не получены, и передатчику не разрешено их отправлять

Категория 2 - это и есть Receive Window, также обозначается как RCV.WND.

Аналогично окну отправки, здесь тоже есть указатель - RCV.NXT (Receive Next pointer), который указывает на первый байт, ожидаемый приёмником, то есть на начало окна приёма.

Окно приёма - штука не статичная. Если сервер работает эффективно, это окно может расширяться. В противном случае - сужаться.

Приёмник сообщает отправителю о своём текущем receive window через поле Window в заголовке TCP-сегмента. Когда отправитель получает этот сегмент, указанное значение становится usable window - тем, что он может реально использовать для отправки.

Но между отправкой и получением сегментов проходит время. Поэтому в конкретный момент времени receive window ≠ usable window - они могут отличаться.

Упрощённый пример

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

Чтобы упростить расчёты, сделаем два допущения:

  1. Мы игнорируем MSS (Maximum Segment Size). MSS может меняться в зависимости от маршрута в сети, так что для простоты его просто опустим.
  2. Будем считать, что окно приёма всегда равно usable window и не меняется в процессе. То есть оно постоянно и предсказуемо.

На диаграмме выше показан пример из 10 шагов.

Клиент запрашивает ресурс, а сервер отвечает на этот запрос тремя сегментами:

  • Заголовок на 50 байт
  • Первая часть тела на 80 байт
  • Вторая часть тела на 100 байт

Обе стороны - и клиент, и сервер - одновременно являются и отправителями, и приёмниками.

Предположим, что у клиента окно отправки (SND.WND) - 300 байт, а окно приёма (RCV.WND) - 150 байт. Тогда у сервера наоборот: SND.WND = 150 байт, а RCV.WND = 300 байт.

Вот начальное состояние клиента.

Предположим, что он уже ранее получил 300 байт от сервера, поэтому указатель RCV.NXT указывает на байт 301 - это следующий ожидаемый байт.

Так как клиент ещё ничего не отправлял, оба указателя - SND.UNA и SND.NXT - указывают на 1, то есть на самый первый байт, который ещё не был отправлен.

Согласно формуле, usable window клиента рассчитывается так:1 + 300 - 1 = 300

Вот начальное состояние сервера, которое зеркально отражает состояние клиента.

Поскольку сервер уже отправил 300 байт, оба указателя - SND.UNA и SND.NXT - указывают на 301.

А RCV.NXT указывает на 1, так как клиент пока ничего не отправлял- запрос ещё не поступил.

Тогда usable window сервера считается так: 301 + 150 - 301 = 150

Теперь начинается шаг 1.

Клиент отправляет свой первый запрос размером 100 байт. В этот момент состояние окон немного меняется:

  • Эти 100 байт были отправлены, но ещё не подтверждены, поэтому SND.NXT сдвигается на 100 байт вправо - теперь он указывает на 101.
  • Остальные указатели остаются на месте: SND.UNA всё ещё равен 1, так как ACK пока не пришёл.

Теперь usable window клиента рассчитывается так: 1 + 300 - 101 = 200

На шаге 2 переходим к серверу:

  • Когда сервер получает 100-байтный запрос от клиента, он сдвигает RCV.NXT на 100 байт вперёд - теперь он указывает на 101.
  • Затем сервер отправляет ответ: 50 байт, в том числе ACK. Эти байты пока не подтверждены, так что SND.NXT сдвигается на 50 байт - теперь он указывает на 351.
  • SND.UNA остаётся без изменений, всё ещё равен 301, потому что подтверждений за новые 50 байт ещё не было.

Теперь usable window сервера считается так: 301 + 150 - 351 = 100

Теперь снова переходим к клиенту:

  • Когда клиент получает 50-байтный ответ от сервера, он сдвигает RCV.NXT на 50 байт вперёд - теперь он указывает на 351.
  • Одновременно с этим клиент получает ACK за ранее отправленные 100 байт, поэтому SND.UNA сдвигается вперёд и теперь указывает на 101.
  • SND.NXT не меняется, так как новых данных клиент пока не отправлял - он просто принял ответ.

Теперь usable window клиента считается так: 101 + 300 - 101 = 300

Снова переходим на сторону сервера.

Сейчас usable window сервера составляет 100 байт - этого достаточно, чтобы отправить следующий сегмент размером 80 байт.

  • После отправки SND.NXT сдвигается на 80 байт вперёд - теперь он указывает на 431.
  • SND.UNA остаётся на месте (301), потому что первые 50 байт, отправленные ранее, всё ещё не были подтверждены.
  • RCV.NXT тоже не меняется, так как сервер пока не получал новых данных от клиента.

Теперь рассчитываем usable window: 301 + 150 - 431 = 20

Клиент получает первую часть файла - 80 байт - и сразу же отправляет ACK в ответ.

  • При получении этих данных RCV.NXT сдвигается на 80 байт вперёд.
  • Остальные указатели остаются без изменений: SND.UNA и SND.NXT не меняются, так как клиент ничего нового не отправлял, только подтвердил приём.

Поскольку размер окна приёма у клиента постоянен (по допущению), usable window остаётся равным: 101 + 300 - 101 = 300

В этот момент сервер получает ACK за те 50 байт, которые он отправил на шаге 2.

  • SND.UNA сдвигается на 50 байт вперёд - теперь он указывает на 351.
  • Остальные указатели не меняются: SND.NXT остаётся на 431 (после отправки 80 байт ранее), RCV.NXT тоже без изменений - новых данных от клиента не поступало.

Теперь пересчитаем usable window сервера: 351 + 150 - 431 = 70

Сервер получает ещё один ACK - на этот раз за 80 байт, отправленных на шаге 4 (первая часть файла).

  • SND.UNA сдвигается вперёд на 80 байт - теперь он указывает на 431.
  • Остальные указатели не меняются: SND.NXT уже был на 431 (сервер ещё ничего нового не отправлял), RCV.NXT тоже без изменений.

Теперь пересчитываем usable window: 431 + 150 - 431 = 150

На шаге 8 сервер отправляет вторую часть файла - 100 байт.

  • SND.NXT сдвигается на 100 байт вперёд - теперь он указывает на 531.
  • Остальные указатели остаются без изменений: SND.UNA всё ещё на 431 (ожидает подтверждение за эти 100 байт), RCV.NXT не менялся - новых данных от клиента не было.

Теперь пересчитаем usable window: 431 + 150 - 531 = 50

Теперь очередь клиента:

  • Когда клиент получает 100 байт данных (вторая часть файла), RCV.NXT сдвигается на 100 байт вперёд.
  • Остальные указатели не меняются: SND.UNA и SND.NXT остаются на своих местах, так как клиент ничего не отправлял (только получил данные).

Поскольку, по нашему допущению, окно приёма у клиента фиксировано и всегда равно usable window, usable window остаётся прежним - 300 байт.

Наконец, сервер получает ACK за предыдущие 100 байт ответа.

  • SND.UNA сдвигается на 100 байт вперёд - теперь он указывает на 531.
  • Остальные указатели остаются на месте: SND.NXT уже на 531, новых данных не отправлялось; RCV.NXT без изменений.

Теперь считаем usable window: 531 + 150 - 531 = 150

Когда окно меняется

Ранее мы предположили, что окно отправки и окно приёма остаются неизменными. В реальности это на самом деле не так.

Байты, участвующие в обоих окнах, лежат в буферах операционной системы, и размер этих буферов может динамически меняться. Например, если приложение читает данные из буфера слишком медленно, свободное место в нём уменьшается.

Давайте теперь рассмотрим ситуацию, когда окно приёма изменяется, и посмотрим, как это влияет на usable window.

Упростим ситуацию, чтобы сосредоточиться на usable window у клиента.

В этом примере клиент всегда выступает в роли отправителя, а сервер - приёмника. То есть теперь нас интересует только поведение окна у клиента, который отправляет данные.

Когда сервер отправляет ACK, он также включает в него обновлённый размер окна.

В начале клиент отправляет первый запрос размером 150 байт.

  • Эти 150 байт отправлены, но ещё не подтверждены.
  • Usable window сокращается до 150 байт.
  • Окно отправки остаётся равным 300 байтам.

Когда сервер получает запрос, приложение читает первые 50 байт, а 100 байт остаются в буфере, занимая 100 байт пространства в окне приёма. Поэтому окно приёма сжимается до 200 байт.

Затем сервер отправляет ACK с обновлённым окном приёма на 200 байт.

Клиент получает ACK и обновляет размер своего окна отправки до 200 байт.

В этот момент usable window равен send window, потому что все 150 байт были подтверждены.

Клиент снова отправляет запрос размером 200 байт, полностью используя доступное пространство в usable window.

После того как сервер получает 200 байт, приложение всё ещё работает медленно - в сумме оно прочитало только 70 байт, оставив 280 байт в буфере.

Это приводит к очередному сжатию окна приёма. Теперь в нём осталось всего 20 байт свободного места.

В ACK-сообщении сервер сообщает клиенту обновлённый размер окна.

Клиент снова обновляет своё окно отправки до 20 байт после получения ACK. Usable window тоже становится равным 20 байтам.

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

Подозреваю, у многих на этом моменте возник вопрос:

"Застрянем ли мы с usable window в 20 байт, если от сервера больше не придёт ни одного сообщения?"

Нет, не застрянем. Чтобы избежать такой ситуации, TCP на стороне клиента периодически проверяет размер окна приёма.

Как только освобождается место, usable window расширяется, и можно отправлять больше данных.

Главное из всего выше

  • Правильный расчёт usable window — ключ к пониманию механизма скользящего окна TCP.
  • Чтобы научиться его считать, нужно разобраться в трёх указателях: SND.UNA, SND.NXT и RCV.NXT.
  • Допущение, что размер окна не меняется, помогает лучше понять общий процесс.


Report Page