TCP основан на связи «точка-точка» между двумя узлами сети. TCP получает данные от программ и обрабатывает их как поток байтов. Байты группируются в сегменты, которым TCP присваивает последовательные номера, необходимые для правильной сборки сегментов на узле-приемнике.
Чтобы два узла TCP могли обмениваться данными, им нужно сначала установить сеанс связи друг с другом. Сеанс TCP инициализируется с помощью процесса, называемого трехэтапным установлением связи. В этом процессе синхронизируются номера последовательности и передается управляющая информация, необходимая для установления виртуального соединения между узлами.
TCP-сегменты инкапсулируются и передаются в IP-датаграммах, как показано на следующем рисунке.
Заголовок TCP-сегмента содержит значительно больше полей, чем заголовок UDP, что отражает более развитые возможности первого протокола.
Рис. 1. Формат заголовка сегмента TCP.
Поле порт источника (Source Port) занимает 2 байта и идентифицирует процесс-отправитель.
Поле порт получателя (Destination Port) занимает 2 байта и идентифицирует процесс-получатель.
Поля порядковый номер (Sequence Number) (длина 4 байта) и номер подтверждения (acknowledgement number) (длина 4 байта) нумеруют каждый отправленный или полученный байт данных. Реализуются как целые числа без знака, которые сбрасываются, когда достигают максимального значения. Каждая сторона ведет собственную порядковую нумерацию.
Поле длина заголовка занимает 4 бита и представляет собой длину заголовка TCP-сегмента, измеренную в 32-битовых словах. Длина заголовка не фиксирована и может изменяться в зависимости от значений, устанавливаемых в поле параметры.
Поле резерв (Reserved) занимает 6 бит.
Поле флаги (Code Bits) занимает 6 бит и содержит шесть 1-битовых флагов:
Рис. 2. Структура пакета TCP при вычислении контрольной суммы.
Псевдозаголовок формируется исключительно для работы с контрольной суммой и имеет следующую структуру.
Рис. 3. Структура псевдозаголовка пакета TCP.
Поле протокол (длина 1 байт) идентифицирует протокол из заголовка пакета IP. Для TCP это значение равно 6.
Включение псевдозаголовка в контрольную сумму TCP помогает обнаружить неверно доставленные пакеты, хотя это нарушает иерархию протоколов, так как IP-адреса в нем принадлежат IP-уровню, а не TCP-уровню.
Поле указатель на срочные данные (Urgent Pointer) (длина 2 байта) содержит смещение в байтах от текущего порядкового номера байта до места расположения срочных данных. Содержимым срочных данных занимаются вышестоящие уровни.
Поле параметры (options) (длина переменная, кратная 32 битам) содержит дополнительные поля, расширяющие возможности стандартного заголовка.
Рис. 4. Установка TCP-соединения в нормальном случае (а); столкновение вызовов (б).
Если какой-либо процесс прослушивает какой-либо порт, то входящий TCP-сегмент передается этому процессу. Последний может принять соединение или отказаться от него. Если процесс принимает соединение, он отсылает в ответ подтверждение. Последовательность TCP-сегментов, посылаемых в нормальном случае, показана на рис. 4, а. Необходимо обратить внимание на то, что сегмент с установленным битом SYN занимает 1 байт пространства порядковых номеров, что позволяет избежать неоднозначности в их подтверждениях.
Если два хоста одновременно попытаются установить соединение друг с другом, то последовательность происходящих при этом событий будет соответствовать рис. 4, б. В результате будет установлено только одно соединение, а не два, так как пара конечных точек однозначно определяет соединение. То есть если оба соединения пытаются идентифицировать себя с помощью пары (x, y) делается всего одна табличная запись для (x, y).
В результате переговорного процесса модулей TCP с двух сторон соединения определяются параметры соединения. Одни из них остаются постоянными в течение всего сеанса связи, а другие адаптивно изменяются. В частности, в зависимости от загрузки буфера принимающей стороны, а также надежности работы сети динамически изменяется размер окна отправителя. Создание соединения означает также выделение операционной системой на каждой стороне соединения определенных системных ресурсов: для организации буферов, таймеров, счетчиков. Эти ресурсы будут закреплены за соединением с момента создания и до момента разрыва.
Логическое TCP-соединение однозначно идентифицируется парой сокетов.
Каждый сокет одновременно может участвовать в нескольких соединениях. Так, если (IP1, n1), (IP2, n2), (IP3, n3) – сокеты трех различных приложений, где IP1, IP2, IP3 – их IP-адреса, а n1, n2, n3 – номера их TCP-портов, то возможно образование следующих соединений:
Рис. 5. Нумерация байтов в TCP-сегменте.
Когда отправитель посылает TCP-сегмент, он в качестве идентификатора сегмента помещает в поле порядкового номера номер первого байта данного сегмента. Так, на рис. 6 идентификаторами сегментов являются номера 32600, 34060, 36520 и т.д. На основании этих номеров TCP-получатель не только отличает данный сегмент от других, но и позиционирует полученный фрагмент относительно общего потока. Кроме того, он может сделать вывод, что полученный сегмент является дубликатом или что между двумя полученными сегментами пропущены данные и т.д.
Рис. 6. Порядковый номер и номер квитанции.
В качестве квитанции получатель сегмента отсылает ответное сообщение (сегмент), в которое помещает число (номер подтверждения), на единицу превышающее максимальный номер байта в полученном сегменте. Для сегментов, изображенных на рис. 6, квитанцией о получении (номером подтверждения) служат номера последнего байта каждого сегмента +1. Так для первого отправленного сегмента это будет число 34060, для второго – 36520 и т.д. Номер подтверждения часто интерпретируют как номер следующего ожидаемого байта данных. Квитанция (подтверждение) в протоколе TCP посылается только в случае правильного приема данных, отрицательные квитанции не посылаются. Таким образом, отсутствие квитанции означает либо потерю сегмента, либо прием искаженного сегмента, либо потерю квитанции.
В протоколе TCP в одном и том же сегменте могут быть помещены и данные, которые посылает приложение другой стороне, и квитанция, которой модуль TCP подтверждает получение данных.
Протокол TCP является дуплексным, то есть в рамках одного соединения регламентируется процедура обмена данными в обе стороны. Каждая сторона одновременно выступает и как отправитель, и как получатель. У каждой стороны есть пара буферов: один – для хранения принятых сегментов, другой – для сегментов, которые только еще предстоит отправить. Кроме того, имеется буфер для хранения копий сегментов, которые были отправлены, но квитанции о получении которых еще не поступили.
И при установлении соединения, и в ходе передачи обе стороны, выступая в роли получателя, посылают друг другу так называемые окна приема. Каждая из сторон, получив окно приема, «понимает», сколько байтов ей разрешается отправить с момента получения последней квитанции. Другими словами, посылая окна приема, обе стороны пытаются регулировать поток байтов в свою сторону, сообщая своему «визави», какое количество байтов (начиная с номера байта, о котором уже была выслана квитанция) они готовы в настоящий момент принять.
На рис. 7 показан поток байтов, поступающих с верхнего уровня в выходной буфер протокола TCP. Из протокола байтов модуль TCP «нарезает» последовательность сегментов и готовит их для отправки другому сокету. В этом потоке можно указать несколько логических границ. Первая граница отделяет сегменты, которые уже были отправлены и на которые уже пришли квитанции. По другую сторону этой границы располагается окно размером W байт. Часть байтов, входящих в окно, составляют сегменты, которые уже были отправлены, но квитанции на них пока не получены. Оставшаяся часть окна – это сегменты, которые пока не отправлены, но могут быть отправлены, так как входят в пределы окна. И наконец, последняя граница указывает на начало последовательности сегментов, ни один из которых не может быть отправлен до тех пор, пока не придет очередная квитанция и окно не будет сдвинуто вправо.
Если размер окна равен W, а последняя по времени квитанция содержала значение N, то отправитель может посылать новые сегменты до тех пор, пока в очередной сегмент не попадет байт с номером N+W. Этот сегмент выходит за рамки окна, и передачу в таком случае необходимо приостановить до прихода следующей квитанции.
Рис. 7. Особенности реализации алгоритма скользящего окна в протоколе TCP.
Процедура «медленный старт»
Когда в какую-либо сеть поступает больше данных, чем она способна обработать, в сети образуются заторы. Интернет в этом смысле не является исключением. Поэтому здесь мы рассмотрим алгоритм, разработанный для борьбы с перегрузкой сети. Решение, применяемое в Интернете, состоит в признании существования двух потенциальных проблем: низкой пропускной способности сети и низкой емкости получателя – и в раздельном решении обеих проблем. Для этого у каждого отправителя есть два окна: окно, предоставленное получателем, и окно перегрузки. Размер каждого из них соответствует количеству байтов, которое отправитель имеет право передать. Отправитель руководствуется минимальным из этих двух значений. Например, получатель говорит: «Посылайте мне 8 Кбайт», но отправитель знает, что если он пошлет более 4 Кбайт, то в сети образуется затор, поэтому он посылает все же 4 Кбайта. Если же отправитель знает, что сеть способна пропустить и большее количество данных, например 32Кбайта, он передаст столько, сколько просит получатель (то есть 8 Кбайт).
При установке соединения отправитель устанавливает размер окна перегрузки равным размеру максимального используемого в данном соединении сегмента. Если подтверждение получения этого сегмента прибывает прежде, чем истекает период ожидания, к размеру окна добавляется размер сегмента, то есть размер окна перегрузки удваивается, и посылаются уже два сегмента. В ответ на подтверждение получения каждого из сегментов производится расширение окна перегрузки на величину одного максимального сегмента. Допустим, размер окна равен n сегментам. Если подтверждения для всех сегментов приходят вовремя, окно увеличивается на число байтов, соответствующие n сегментам. По сути, подтверждение каждой последовательности сегментов приводит к удвоению окна перегрузки.
Этот процесс экспоненциального роста продолжается до тех пор, пока не будет достигнут размер окна получателя или не будет выработан признак тайм-аута, сигнализирующий о перегрузке сети. Например, если пакеты размером 1024, 2048 и 4096 байт дошли до получателя успешно, а в ответ на передачу пакета размером 8192 байта подтверждение не пришло в установленный срок, окно перегрузки устанавливается равным 4096 байтам. Пока размер окна перегрузки остается равным 4096 байтам, более длинные пакеты не посылаются, независимо от размера окна, предоставляемого получателем. Этот алгоритм называется «медленным стартом».
Рис. 8. Формат заголовка пакета UDP.
Поля порт источника и порт получателя идентифицируют передающий и получающий процессы.
Поле длина UDP содержит длину пакета UDP в байтах.
Поле контрольная сумма содержит контрольную сумму пакета UDP, вычисляемую по всему пакету UDP с добавленным псевдозаголовком.
Рис. 9. Структура пакета UDP при вычислении контрольной суммы.
Рис. 10. Структура псевдозаголовка пакета UDP
Поле протокол (длина 8 бит) идентифицирует протокол из заголовка пакета IP. Для UDP это значение равно 17.
Судя по простоте заголовка, протокол UDP очень сложным не является. Здесь стоит прямо сказать о том, чего UDP не делает. Итак, UDP не занимается контролем потока, контролем ошибок, повторной передачей после приема испорченного сегмента. Его функции сводятся к мультиплексированию и демультиплексированию данных между сетевым и прикладным уровнями. Для процессов, которым хочется управлять потоком, контролировать ошибки и временные интервалы, протокол UDP – это как раз то, что нужно. Одной из областей, где UDP применяется особенно широко, является область клиент-серверных приложений. Зачастую клиент посылает короткий запрос серверу и надеется получить короткий ответ. Если запрос или ответ теряется, клиент по прошествии определенного интервала может попытаться еще раз. Это позволяет не только упростить код, но и уменьшить требуемое количество сообщений по сравнению с протоколами, которым требуется начальная настройка.
Рассмотрим, как протокол UDP решает задачу демультиплексирования. Казалось бы, для этой цели достаточно использовать номера портов. Кадры, несущие UDP-дейтаграммы, прибывают на сетевой интерфейс хоста, последовательно обрабатываются протоколами стека и, наконец, поступают в распоряжение протокола UDP. UDP извлекает из заголовка номер порта назначения и передает данные на соответствующий порт соответствующему приложению, то есть выполняет демультиплексирование.
Это решение выглядит очень логично и просто, однако оно неработоспособно в ситуации, когда на одном конечном узле выполняется насколько копий одного и того же приложения. Пусть, например, на одном хосте запущены два DNS-сервера, причем оба используют для передачи своих сообщений протокол UDP. DNS-сервер имеет UDP-порт 53. В то же время у каждого из DNS-серверов могут быть свои клиенты, собственные базы данных, собственные настройки. Когда на сетевой интерфейс данного компьютера придет запрос от DNS-клиента, в UDP-дейтаграмме будет указан номер порта 53, который в равной степени относится к обоим DNS-серверам – так кому же из них протокол UDP должен передать запрос? Чтобы снять неоднозначность, применяют следующий подход. Разным копиям одного приложения, даже установленным на одном компьютере, присваивают разные IP-адреса. В данном примере DNS-сервер 1 имеет IP-адрес IP1, а DNS-сервер 2 имеет IP-адрес IP2. Таким образом, однозначно определяет прикладной процесс в сети (а тем более в пределах одного компьютера) пара (IP-адрес, номер порта UDP), называемая UDP-сокетом.