Когда срочные данные приходят по назначению, получающее приложение прерывается (то есть, в терминологии UNIX, «получает сигнал»), затем оно считывает данные из входного потока и ищет среди них срочные. Конец срочных данных маркируется, но их начало приложение должно отыскать самостоятельно.
Эта схема является грубым сигнальным механизмом, который оставляет все прочие задачи приложению. Хотя теоретически использование срочных данных выглядит целесообразным, на заре своего появления эта схема была неудачно реализована и поэтому быстро вышла из употребления. Сейчас использовать ее не рекомендуется из-за различий в имплементации, поэтому приложения вынуждены прибегать к собственным системам сигналов. Возможно, в последующих транспортных протоколах эта идея будет воплощена лучше.
6.5.3. Протокол TCP
В данном разделе мы обсудим TCP в общих чертах, а в следующем — подробно изучим его заголовок.
Ключевым свойством TCP, определяющим всю структуру протокола, является то, что в TCP-соединении у каждого байта есть свой 32-разрядный порядковый номер. В прежние годы, когда типичная скорость выделенных линий между маршрутизаторами составляла 56 Кбит/с, хосту, постоянно работающему на полной скорости, потребовалось бы больше недели на то, чтобы перебрать все порядковые номера. При современных скоростях они могут закончиться пугающе быстро. Отдельные 32-разрядные порядковые номера используются для указания позиции раздвижного окна в одном направлении и для подтверждений в обратном. Все это мы обсудим далее.
Отправляющая и принимающая TCP-подсистемы обмениваются данными в виде сегментов. Сегмент TCP состоит из фиксированного 20-байтного заголовка (плюс необязательная часть), за которым могут следовать байты данных. Размер сегментов определяется программным обеспечением TCP. Оно может объединять в один сегмент данные, полученные в результате нескольких операций записи, или, наоборот, распределять результат одной записи между несколькими сегментами. Их размер ограничен двумя пределами. Во-первых, каждый сегмент, включая TCP-заголовок, должен помещаться в 65 515-байтное поле пользовательских данных IP-пакета. Во-вторых, в каждом канале есть максимальный размер передаваемого блока (Maximum Transfer Unit, MTU). На сторонах отправителя и получателя каждый сегмент должен помещаться в MTU, чтобы он мог передаваться и приниматься в отдельном пакете, не разделенном на фрагменты. На практике MTU обычно составляет 1500 байт (что соответствует размеру поля пользовательских данных Ethernet), и таким образом определяется верхний предел размера сегмента.
Тем не менее фрагментация IP-пакета, содержащего TCP-сегменты, возможна, если на его пути у одного из каналов слишком низкий MTU. Но в таком случае снижается производительность, а также возникают другие проблемы (Кент и Могул; Kent and Mogul, 1987). Вместо этого современные реализации TCP выполняют обнаружение MTU маршрута (path MTU discovery). При этом используется метод, описанный в RFC 1191 (мы говорили о нем в разделе 5.5.6). Этот метод вычисляет минимальное значение MTU по всем каналам пути, используя сообщения об ошибках ICMP. На основе этого значения TCP выбирает размер сегмента, позволяющий избежать фрагментации.
Основной протокол, используемый TCP-подсистемами, — это протокол раздвижного окна с динамическим размером окна. При передаче сегмента отправитель включает таймер. Когда сегмент приходит по назначению, принимающая TCP-подсистема высылает обратно сегмент (с данными, если они есть, или без) с номером подтверждения (он равен порядковому номеру следующего ожидаемого сегмента) и новым размером окна. Если время ожидания подтверждения истекает, отправитель передает сегмент еще раз.
Этот протокол кажется простым, но в нем есть несколько деталей, которые следует рассмотреть подробнее. Сегменты могут приходить в неверном порядке. Например, возможна ситуация, в которой байты с 3072-го по 4095-й уже прибыли, но подтверждение для них не может быть выслано, так как байты с 2048-го по 3071-й еще не получены. К тому же сегменты могут задержаться в сети настолько, что у отправителя истечет время ожидания, и он передаст их снова. Переданный повторно сегмент может включать в себя уже другие диапазоны фрагментов, тогда потребуется очень аккуратное администрирование для определения номеров байтов, которые уже были приняты корректно. Но поскольку каждый байт в потоке имеет свое уникальное смещение, эта задача выполнима.
Протокол TCP должен уметь эффективно решать такие проблемы. На оптимизацию производительности TCP-потоков было потрачено много усилий. В следующем разделе мы обсудим несколько алгоритмов, используемых в различных реализациях TCP.
6.5.4. Заголовок TCP-сегмента
На илл. 6.36 показана структура заголовка TCP-сегмента. Каждый сегмент начинается с 20-байтного заголовка фиксированного формата. За ним могут следовать дополнительные параметры. Далее может располагаться до 65 535 – 20 – 20 == 65 495 байт данных (первые 20 байт это IP-заголовок, а вторые — TCP-заголовок). Сегмент может и не содержать данных. Такие сегменты часто применяются для передачи подтверждений и управляющих сообщений.
Илл. 6.36. Заголовок TCP
Рассмотрим поля TCP-заголовка одно за другим. Поля Source port и Destination port указывают локальные конечные точки соединения. TCP-порт вместе с IP-адресом хоста образуют уникальный 48-битный идентификатор конечной точки. Пара конечных точек получателя и отправителя идентифицируют соединение. Такой идентификатор соединения называется кортежем из пяти компонентов (5 tuple), так как он включает пять информационных составляющих: протокол (TCP), IP-адрес отправителя, порт отправителя, IP-адрес получателя и порт получателя.
Поля Sequence number и Acknowledgement number (Номер подтверждения) выполняют свою обычную функцию. Обратите внимание: поле Acknowledgement number относится к следующему по порядку ожидаемому байту, а не к последнему полученному. Это накопительное подтверждение (cumulative acknowledgement), так как один номер объединяет в себе информацию обо всех полученных данных. Сфера его применения не выходит за рамки потерянных данных. Оба поля 32-разрядные, поскольку в TCP-потоке нумеруется каждый байт данных.
Поле TCP header length (Длина TCP-заголовка) сообщает, сколько 32-разрядных слов содержится в TCP-заголовке. Эта информация необходима, так как поле Options, а вместе с ним и весь заголовок имеет переменную длину. По сути, TCP header length указывает смещение от начала сегмента до поля данных, измеренное в 32-битных словах. Это то же самое, что длина заголовка.
Следом идет неиспользуемое 4-битное поле. Тот факт, что эти биты не используются уже 30 лет (изначально поле было 6-битным и из них были задействованы только 2 бита), свидетельствует о том, насколько хорошо продуман дизайн TCP. Иначе протоколы использовали бы эти биты, чтобы справиться с его недостатками.
Затем следуют восемь 1-битных флагов. CWR и ECE сообщают о перегрузках сети в случае, если используется явное уведомление о перегрузке (см. RFC 3168). Когда TCP-получатель узнает, что сеть перегружена, он с помощью флага ECE передает TCP-отправителю сигнал ECN-Echo (ECN-эхо), предлагая ему снизить скорость отправки. Уменьшив скорость, TCP-отправитель сообщает об этом TCP-получателю с помощью флага CWR с сигналом Congestion Window Reduced (Окно перегрузки уменьшено), после чего получатель перестает передавать сигнал ECN-Echo. Подробнее о роли ECN и CWR при контроле перегрузки в TCP мы поговорим в разделе 6.5.10.
Бит URG устанавливается в 1 в случае использования поля Urgent pointer (Указатель срочности), где указано байтовое смещение от текущего порядкового номера до срочных данных. Таким образом, в TCP реализуются прерывающие сообщения. Как уже упоминалось, этот метод позволяет отправителю передать получателю сигнал, не вовлекая в это TCP; он используется редко.
Если бит ACK установлен в 1, значит, поле Acknowledgement number действует. Это справедливо для большинства пакетов. Если ACK установлен в 0, значит, сегмент не содержит подтверждения, и поле Acknowledgement number игнорируется.
Бит PSH является, по сути, PUSH-флагом, с помощью которого отправитель вежливо просит получателя доставить данные приложению сразу, а не хранить их в буфере, пока тот не наполнится (получатель может это делать в целях эффективности).
Бит RST используется для внезапного сброса состояния соединения, которое из-за сбоя хоста или по другой причине попало в тупиковую ситуацию. Также он применяется для отказа от неверного сегмента или от попытки создать соединение. Если в сегменте установлен бит RST, это означает проблему.
Бит SYN применяется для установки соединения. У запроса соединения бит SYN = 1, а бит ACK = 0, то есть поле подтверждения не задействовано. Но в ответе на этот запрос подтверждение есть, поэтому значения этих битов таковы: SYN = 1, ACK = 1. Таким образом, бит SYN используется для обозначения как сегмента CONNECTION REQUEST, так и CONNECTION ACCEPTED, а бит ACK — чтобы различать их.
Бит FIN используется для разрыва соединения. Он сообщает, что у отправителя больше нет данных для передачи. Однако, даже закрыв соединение, процесс может продолжать получать данные в течение неопределенного времени. У сегментов с битами FIN и SYN есть порядковые номера, что гарантирует правильный порядок их выполнения.
Управление потоком в TCP осуществляется при помощи раздвижного окна переменного размера. Поле Window size (Размер окна) сообщает, сколько байтов может быть отправлено после подтвержденного байта. Нулевое значение Window size означает, что все байты до Acknowledgement number – 1 включительно пришли, но получатель их еще не обработал, и поэтому остальные байты он пока принять не может. Позже получатель может разрешить дальнейшую передачу, отправив сегмент с таким же значением Acknowledgement number и ненулевым значением Window size.
В главе 3 мы изучали протоколы, в которых подтверждения приема фреймов были связаны с разрешениями на продолжение передачи. Это следствие фиксированного размера раздвижного окна в этих протоколах. В ТСР подтверждения отделены от разрешений на передачу. В сущности, получатель может сказать: «Я получил байты вплоть до k-го, пока что достаточно, спасибо». Такое разделение (а если точнее, окно переменного размера) придает протоколу дополнительную гибкость. Далее мы обсудим его более детально.