Рис. 6.30. Четыре 512-байтовых сегмента, посланные как отдельные IP-дейтаграммы (а);
2048 байт данных, доставленные приложению с помощью одного вызова процедуры READ (б)
Файлы в системе UNIX также обладают этим свойством. Программа, читающая файл, не может определить, как был записан этот файл: поблочно, побайтно или сразу целиком. Как и в случае с файлами системы UNIX, TCP-программы не имеют представления о назначении байтов и не интересуются этим. Байт для них — просто байт.
Получив данные от приложения, протокол TCP может послать их сразу или поместить в буфер, чтобы послать большую порцию данных, по своему усмотрению. Однако иногда приложению бывает необходимо, чтобы данные были посланы немедленно. Допустим, например, что пользователь интерактивной игры хочет отправить поток обновлений. Важно, чтобы обновления передавались сразу же, а не сохранялись в буфере до появления других обновлений. Чтобы можно было вынудить передачу данных, в TCP существует флаг PUSH (протолкнуть). Изначально предполагалось, что с помощью этого флага приложения будут сообщать TCP о том, что не нужно задерживать передачу пакета. Однако приложения не могут устанавливать такие флаги при отправке данных. Вместо этого в различных операционных системах существуют специальные параметры, позволяющие ускорить передачу данных (например, TCP_NONDELAY в Windows и Linux).
Для тех, кто интересуется доисторическими методами Интернета, мы расскажем об интересной особенности службы TCP, которая все еще входит в состав протокола, но используется редко. Речь пойдет о срочных данных (urgent data). Когда часть данных обладает высоким приоритетом, то есть должна обрабатываться сразу же, — например, если пользователь, взаимодействующий с программой в интерактивном режиме, нажимает Ctrl-C, чтобы прервать начавшийся удаленный процесс, — посылающее приложение может поместить в выходной поток данных управляющую информацию и передать ее TCP-службе вместе с флагом URGENT (срочно). Этот флаг заставляет TCP-подсистему прекратить накопление данных и без промедления передать в сеть все, что у нее есть для данного соединения.
Когда срочные данные прибывают по назначению, получающее приложение прерывается (то есть в терминологии UNIX «получает сигнал»), после чего оно может считать данные из входного потока и найти среди них срочные. Конец срочных данных маркируется, так что приложение может распознать, где они заканчиваются. Начало срочных данных не маркируется. Приложение должно само догадаться.
Такая схема представляет собой грубый сигнальный механизм, оставляя все прочее приложению. Хотя теоретически использование срочных данных выглядит целесообразным, в первое время после своего появления эта схема не была хорошо реализована и поэтому быстро вышла из употребления. Сейчас использовать ее не рекомендуется из-за трудностей реализации, так что приложения вынуждены прибегать к своим собственным системам сигналов. Возможно, в последующих транспортных протоколах эта идея будет реализована лучше.
6.5.3. Протокол TCP
В данном разделе будет рассмотрен протокол TCP в общих чертах. В следующем разделе мы обсудим заголовок протокола, поле за полем.
Ключевым свойством TCP, определяющим всю структуру протокола, является то, что в TCP-соединении у каждого байта есть свой 32-разрядный порядковый номер. В первые годы существования Интернета базовая скорость передачи данных между маршрутизаторами по выделенным линиям составляла 56 Кбит/с. Хосту, постоянно выдающему данные с максимальной скоростью, потребовалось бы больше недели на то, чтобы порядковые номера совершили полный круг. При нынешних скоростях порядковые номера могут кончиться очень быстро, об этом еще будет сказано ниже. Отдельные 32-разрядные порядковые номера используются для указания позиции скользящего окна в одном направлении и подтверждений в обратном направлении, о чем также будет сказано ниже.
Отправляющая и принимающая TCP-подсистемы обмениваются данными в виде сегментов. Сегмент TCP состоит из фиксированного 20-байтового заголовка (плюс необязательная часть), за которым могут следовать байты данных. Размер сегментов определяется программным обеспечением TCP. Оно может объединять в один сегмент данные, полученные в результате нескольких операций записи, или, наоборот, распределять результат одной записи между несколькими сегментами. Размер сегментов ограничен двумя пределами. Во-первых, каждый сегмент, включая TCP-заголовок, должен помещаться в 65 515-байтное поле полезной нагрузки IP-пакета. Во-вторых, в каждом канале есть максимальный размер передаваемого блока (MTU, Maximum Transfer Unit). На отправителе и получателе каждый сегмент должен помещаться в MTU, чтобы он мог передаваться и приниматься в отдельном пакете, не разделенном на фрагменты. На практике максимальный размер передаваемого блока составляет обычно 1500 байт (что соответствует размеру поля полезной нагрузки Ethernet), и таким образом определяется верхний предел размера сегмента.
Тем не менее, если IP-пакет, содержащий TCP-сегменты, проходит по пути со слишком низким MTU, его фрагментация возможна. Но в таком случае снижается производительность, а также возникают другие проблемы (Kent и Mogul, 1987). Вместо этого реализации TCP выполняют обнаружение MTU маршрута. При этом используется метод, описанный в RFC 1191, — о нем мы говорили в разделе 5.5.5. Этот метод вычисляет минимальное значение MTU по всем каналам пути, используя сообщения об ошибках ICMP. На основе этого значения TCP выбирает размер сегмента, позволяющий избежать фрагментации.
Основным протоколом, используемым TCP-подсистемами, является протокол скользящего окна с динамическим размером окна. При передаче сегмента отправитель включает таймер. Когда сегмент прибывает в пункт назначения, принимающая TCP-подсистема посылает обратно сегмент (с данными, если есть, что посылать, иначе — без данных) с номером подтверждения, равным порядковому номеру следующего ожидаемого сегмента, и новым размером окна. Если время ожидания подтверждения истекает, отправитель посылает сегмент еще раз.
Хотя этот протокол кажется простым, в нем имеется несколько деталей, которые следует рассмотреть подробнее. Сегменты могут приходить в неверном порядке. Так, например, возможна ситуация, в которой байты с 3072 по 4095 уже прибыли, но подтверждение для них не может быть выслано, так как байты с 2048 по 3071 еще не получены. К тому же сегменты могут задерживаться в сети так долго, что у отправителя истечет время ожидания, и он передаст их снова. Переданный повторно сегмент может включать в себя уже другие диапазоны фрагментов, поэтому потребуется очень аккуратное администрирование для определения номеров байтов, которые уже были приняты корректно. Тем не менее, поскольку каждый байт в потоке единственным образом определяется по своему сдвигу, эта задача оказывается реальной.
Протокол TCP должен уметь справляться с этими проблемами и решать их эффективно. На оптимизацию производительности TCP-потоков было потрачено много сил. В следующем разделе мы обсудим несколько алгоритмов, используемых в различных реализациях протокола TCP.
6.5.4. Заголовок TCP-сегмента
На рис. 6.31 показана структура заголовка TCP-сегмента. Каждый сегмент начинается с 20-байтного заголовка фиксированного формата. За ним могут следовать дополнительные поля (параметры). После дополнительных полей может располагаться до 65 535 - 20 - 20 = 65 495 байт данных, где первые 20 байт — это IP-заголовок, а вторые — TCP-заголовок. Сегменты могут и не содержать данных. Такие сегменты часто применяются для передачи подтверждений и управляющих сообщений.
Рассмотрим TCP-заголовок поле за полем. Поля Порт получателя и Порт отправителя являются идентификаторами локальных конечных точек соединения. TCP-порт вместе с IP-адресом хоста образуют уникальный 48-битный идентификатор конечной точки. Пара таких идентификаторов, относящихся к источнику и приемнику, однозначно определяет соединение. Этот идентификатор соединения называется кортежем из пяти компонентов (5 tuple), так как он включает пять информационных составляющих: протокол (TCP), IP-адрес источника, порт источника, IP-адрес получателя и порт получателя.