Состояние
Описание
CLOSED
Закрыто. Соединение не является активным и не находится в процессе установления
LISTEN
Ожидание. Сервер ожидает входящего запроса
SYN RCVD
Прибыл запрос соединения. Ожидание подтверждения
SYN SENT
Запрос соединения отправлен. Приложение начало открывать соединение
ESTABLISHED
Установлено. Нормальное состояние передачи данных
FIN WAIT 1
Приложение сообщило, что ему больше нечего передавать
FIN WAIT 2
Другая сторона согласна разорвать соединение
TIME WAIT
Ожидание, пока из сети не исчезнут все пакеты
CLOSING
Обе стороны попытались одновременно закрыть соединение
CLOSE WAIT
Другая сторона инициировала разъединение
LAST ACK
Ожидание, пока из сети не исчезнут все пакеты
Илл. 6.38. Состояния конечного автомата, управляющего TCP-соединением
Конечный автомат показан на илл. 6.39. Типичный случай клиента, активно соединяющегося с пассивным сервером, показан жирными линиями — сплошными для клиента и пунктирными для сервера. Тонкие линии обозначают необычные последовательности событий. Каждая линия на илл. 6.39 маркирована парой событие/действие. Событие может представлять собой либо обращение пользователя к системной процедуре (CONNECT, LISTEN, SEND или CLOSE), либо прибытие сегмента (SYN, FIN, ACK или RST), либо, в одном случае, окончание периода ожидания, равного двойному времени жизни пакетов. Действие может состоять в отправке управляющего сегмента (SYN, FIN или RST). Впрочем, может не предприниматься никакого действия, что обозначается прочерком. В скобках приводятся комментарии.
Диаграмму проще понять, если сначала проследовать по пути клиента (сплошная жирная линия), а затем — по пути сервера (жирный пунктир). Когда приложение на устройстве клиента вызывает операцию CONNECT, локальная TCP-подсистема создает запись соединения, помечает его состояние как SYN SENT и отправляет SYN-сегмент. Обратите внимание, что несколько приложений одновременно могут открыть множество соединений, и состояние каждого из них хранится в записи соединения. Когда прибывает сегмент SYN + ACK, TCP-подсистема отправляет последний ACK-сегмент «тройного рукопожатия» и переключается в состояние ESTABLISHED. В этом состоянии можно пересылать и получать данные.
Когда у приложения заканчиваются данные для передачи, оно выполняет операцию CLOSE, заставляющую локальную TCP-подсистему отправить FIN-сегмент и ждать ответного ACK-сегмента (пунктирный прямоугольник с пометкой «активное разъединение»). Когда прибывает подтверждение, происходит переход в состояние FIN WAIT 2, и одно направление соединения закрывается. Когда приходит встречный FIN-сегмент, в ответ на него также высылается подтверждение, после чего закрывается второе направление. Теперь обе стороны соединения закрыты, но TCP-подсистема ожидает в течение удвоенного максимального времени жизни пакета. Таким образом гарантируется, что ни один пакет этого соединения больше не перемещается по сети, даже если подтверждение было потеряно. Когда период ожидания истекает, TCP-подсистема удаляет запись о соединении.
Илл. 6.39. Конечный автомат TCP-соединения. Жирная сплошная линия показывает нормальный путь клиента. Жирным пунктиром показан нормальный путь сервера. Тонкими линиями обозначены необычные события. Для каждого перехода через косую черту указано, какое событие его вызывает и к выполнению какого действия он приводит
Далее рассмотрим управление соединением с точки зрения сервера. Он выполняет LISTEN и переходит в режим ожидания запросов соединения. Когда приходит SYN-сегмент, в ответ на него высылается подтверждение, после чего сервер переходит в состояние SYN RCVD (запрос соединения получен). Когда в ответ на SYN-подтверждение от клиента приходит ACK-сегмент, процедура «тройного рукопожатия» завершается и сервер переходит в состояние ESTABLISHED. Теперь можно передавать данные.
По окончании передачи данных клиент выполняет операцию CLOSE, в результате чего на сервер приходит FIN-сегмент (пунктирный прямоугольник, обозначенный как пассивное разъединение). Получив оповещение, сервер тоже выполняет CLOSE, и клиенту отправляется FIN-сегмент. Когда от клиента прибывает подтверждение, сервер разрывает соединение и удаляет запись о нем.
6.5.8. Раздвижное окно TCP
Как уже было сказано выше, управление окном в TCP решает проблемы подтверждения корректной доставки сегментов и выделения буферов получателя. Предположим, у получателя есть 4096-байтный буфер (илл. 6.40). Если отправитель передает 2048-байтный сегмент, который успешно принимается получателем, то последний подтверждает его получение. Однако при этом у получателя остается всего лишь 2048 байт свободного буферного пространства (пока приложение не заберет какое-то количество данных из буфера), о чем он и сообщает отправителю, указывая соответствующий размер окна (2048) и номер следующего ожидаемого байта.
После этого отправитель отправляет еще 2048 байт, получение которых подтверждается, но размер окна объявляется равным нулю. Отправитель должен прекратить передачу до тех пор, пока получающий хост не освободит место в буфере и не увеличит размер окна.
При нулевом размере окна отправитель не может отправлять сегменты, за исключением двух случаев. Во-первых, разрешается передавать срочные данные, например, чтобы пользователь мог уничтожить процесс, выполняющийся на удаленном компьютере. Во-вторых, отправитель может отправить 1-байтный сегмент, прося получателя повторить информацию о размере окна и ожидаемом следующем байте. Такой пакет называется пробным сегментом (window probe). Стандарт TCP прямо предусматривает эту возможность для предотвращения тупиковых ситуаций в случае потери объявления о размере окна.
Отправители не обязаны передавать данные сразу же, как только они приходят от приложения. Также никто не требует от получателей отправлять подтверждения как можно скорее. Например, было бы абсолютно логично, если бы TCP-подсистема на илл. 6.40, получив от приложения первые 2 Кбайт данных и зная, что размер окна равен 4 Кбайт, сохранила бы полученные данные в буфере до тех пор, пока не придут еще 2 Кбайт, чтобы передать сегмент из 4 Кбайт. Такая свобода действий может улучшить производительность.
Рассмотрим соединение (к примеру, telnet или SSH) с удаленным терминалом, реагирующим на каждое нажатие клавиши. При наихудшем сценарии, когда символ прибывает к передающей TCP-подсистеме, она создает 21-байтный TCP-сегмент и передает его IP-уровню, который, в свою очередь, отправляет 41-байтную IP-дейтаграмму. На принимающей стороне TCP-подсистема немедленно отвечает 40-байтным подтверждением (20 байт TCP-заголовка и 20 байт IP-заголовка). Затем, когда удаленный терминал прочитает этот байт из буфера, TCP-подсистема отправит обновленную информацию о размере буфера, передвинув окно на 1 байт вправо. Размер этого пакета также составляет 40 байт. Наконец, когда удаленный терминал обработает этот символ, он отправит обратно эхо, включенное в 41-байтный пакет. Итого для каждого введенного с клавиатуры символа пересылается четыре пакета общим размером 162 байта. При дефиците пропускной способности линий этот метод работы нежелателен.
Илл. 6.40. Управление окном в TCP
Чтобы улучшить ситуацию, многие реализации TCP используют отложенные подтверждения (delayed acknowledgements). Идея в том, чтобы задерживать подтверждения и обновления размера окна на время до 500 мс в надежде получить дополнительные данные и отправить подтверждение вместе с ними. Если терминал успеет выдать эхо в течение 500 мс, удаленной стороне нужно будет выслать только один 41-байтный пакет, таким образом, нагрузка на сеть снизится вдвое.
Хотя отложенные подтверждения и снижают нагрузку на сеть, тем не менее отправитель, передающий множество маленьких пакетов (к примеру, 41-байтные пакеты с 1 байтом реальных данных), по-прежнему работает неэффективно. Метод, позволяющий повысить эффективность, известен как алгоритм Нейгла (Nagle’s algorithm) (Nagle, 1984). Предложение Нейгла звучит просто: если данные поступают отправителю маленькими порциями, он просто передает первый фрагмент, а остальные помещает в буфер, пока не получит подтверждение приема первого фрагмента. После этого можно переслать все накопленные в буфере данные в виде одного TCP-сегмента и снова начать буферизацию до получения подтверждения о доставке следующего сегмента. Таким образом, в каждый момент времени может передаваться только один небольшой пакет. Если за время прохождения пакета в обе стороны приложение отправляет много порций данных, алгоритм Нейгла объединяет несколько таких порций в один сегмент, и нагрузка на сеть существенно снижается. Кроме того, согласно этому алгоритму, новый пакет должен быть отправлен, если объем данных в буфере превышает максимальный размер сегмента.
Алгоритм Нейгла широко применяется различными реализациями TCP, однако бывают ситуации, в которых его лучше отключить. В частности, интерактивным играм по интернету обычно требуется быстрый поток мелких пакетов с обновлениями. Если буферизировать эти данные для пакетной пересылки, игра будет работать неправильно, что не порадует пользователей. Дополнительный нюанс в том, что иногда при задержке подтверждений использование алгоритма Нейгла приводит к временным тупиковым ситуациям: получатель ждет данные, к которым можно присоединить подтверждение, а источник ждет подтверждение, без которого не будут переданы новые данные. Так, например, может задерживаться загрузка веб-страниц. На этот случай существует возможность отключения алгоритма Нейгла (параметр TCP_NODELAY). Подробнее об этих и других решениях см. работу Могула и Миншалла (Mogul and Minshall, 2001).
Еще одна проблема, способная значительно снизить производительность протокола TCP, известна под названием синдрома глупого окна (silly window syndrome) (Кларк; Clark, 1982). Ее суть в том, что данные пересылаются TCP-подсистемой крупными блоками, но принимающая сторона интерактивного приложения считывает их посимвольно. Чтобы разобраться в этом, рассмотрим илл. 6.41. Начальное состояние таково: TCP-буфер приемной стороны полон (то есть размер его окна равен нулю), и отправителю это известно. Затем интерактивное приложение читает один символ из TCP-потока. Принимающая TCP-подсистема радостно сообщает отправителю, что размер окна увеличился и что он теперь может отправить 1 байт. Отправитель повинуется и передает 1 байт. Буфер снова заполняется, о чем получатель сообщает с помощью подтверждения для 1-байтного сегмента с нулевым размером окна. И так может продолжаться вечно.