Для некоторых приложений неприемлемы транзитные задержки, возникающие вследствие подтверждения получения. Одно из этих приложений — цифровой голосовой трафик (VoIP). Возникающий время от времени небольшой шум на линии не так раздражает пользователей, как зависание разговора в ожидании подтверждений. Аналогично и при видеосвязи: небольшое число неправильно переданных пикселей не представляет собой проблемы, а вот подергивание изображения, когда поток данных останавливается для исправления ошибок, или долгое ожидание поступления идеального видеопотока раздражает пользователей.
Еще один вид служб — запрос/ответ (request-reply). Отправитель передает одну дейтаграмму с запросом и получает дейтаграмму с ответом. С помощью схемы «запрос/ответ» часто реализуется связь в клиент-серверной модели: клиент отправляет запрос, а сервер на него реагирует. Например, клиент с мобильного телефона отправляет запрос картографическому серверу, чтобы получить список ближайших китайских ресторанов, а сервер присылает ему этот список.
На илл. 1.28 приведена краткая сводка представленных выше типов служб.
Илл. 1.28. Шесть различных типов служб
1.5.4. Примитивы служб
Службы формально описываются набором примитивов (primitives), то есть операций, доступных обращающимся к ним пользовательским процессам. С помощью этих примитивов можно указать службе выполнить какое-либо действие или сообщить о действии, которое совершил объект на том же уровне. Если стек протоколов располагается в операционной системе (как это обычно и бывает), примитивы представляют собой системные вызовы. Такой вызов приводит к системному прерыванию в привилегированном режиме. Далее управление компьютером передается операционной системе для отправки нужных пакетов.
Набор доступных примитивов зависит от вида предоставляемой службы. Примитивы для ориентированных на соединения служб отличаются от примитивов служб без установления соединений. В качестве минимального набора примитивов, способного выдавать надежный байтовый поток, рассмотрим примитивы на илл. 1.29. Они хорошо знакомы всем приверженцам интерфейса сокетов Беркли, поскольку представляют собой его упрощенный вариант.
Примитив
Значение
LISTEN (прослушивать)
Блокирующее ожидание входящего соединения
CONNECT (подключиться)
Установка соединения с находящимся в состоянии ожидания одноранговым процессом
ACCEPT (согласиться)
Принятие входящего соединения от однорангового процесса
RECEIVE (принять)
Блокирующее ожидание входящего сообщения
SEND (отправить)
Отправка сообщения одноранговому процессу
DISCONNECT (отключиться)
Завершение соединения
Илл. 1.29. Простая, ориентированная на установление соединения служба с шестью примитивами
Эти примитивы можно использовать для механизма «запрос/ответ» в клиент-серверной среде. Для иллюстрации их работы покажем схему работы простого протокола, реализующего службу получения дейтаграмм с подтверждением.
Прежде всего сервер выполняет примитив LISTEN, чтобы оповестить о готовности к приему входящих соединений. Примитив LISTEN обычно реализуют при помощи блокирующего системного вызова. После выполнения этого примитива серверный процесс блокируется (приостанавливается) до тех пор, пока не появится запрос на соединение.
Далее клиентский процесс выполняет CONNECT, чтобы установить соединение с сервером. В вызове CONNECT должно быть указано, с кем соединяться, поэтому у него может быть параметр для адреса сервера. После этого операционная система отправляет пакет одноуровневому процессу с запросом на соединение, как показано в (1) на илл. 1.30. Клиентский процесс приостанавливается до получения ответа.
Илл. 1.30. Простое взаимодействие типа «клиент-сервер» получения дейтаграмм с подтверждением
Когда пакет прибывает на сервер, операционная система видит, что он запрашивает соединение. Она проверяет наличие прослушивающего процесса и разблокирует его, если он есть. Далее серверный процесс может устанавливать соединение при помощи вызова ACCEPT, в результате которого клиентскому процессу отправляется ответ (2) с согласием на соединение. Поступление этого ответа приводит к снятию блокировки с клиента. И сервер, и клиент в этот момент уже работают, и между ними установлено соединение.
Очевидна аналогия между этим протоколом и звонком покупателя в службу поддержки какой-либо компании. В начале дня менеджер по работе с клиентами сидит возле своего телефона в ожидании звонков. Затем звонит клиент. Когда менеджер поднимает трубку — устанавливается соединение.
Следующий шаг: выполнение сервером операции RECEIVE для подготовки к получению первого запроса. Обычно сервер делает это сразу же после снятия блокировки примитивом LISTEN, прежде чем клиент получит подтверждение. Вызов RECEIVE блокирует сервер.
Далее клиент выполняет примитив SEND для передачи своего запроса (3) с последующим RECEIVE для получения ответа. Поступление пакета с запросом разблокирует сервер, и он может обработать запрос. После завершения необходимых действий сервер отправляет ответ клиенту (4) с помощью примитива SEND. Получение этого пакета разблокирует клиента, который теперь может обработать ответ и отправить дополнительные запросы, если таковые у него есть.
По окончании работы с сервером клиент выполняет DISCONNECT для завершения соединения (5). Обычно первый DISCONNECT представляет собой блокирующий вызов, который приостанавливает клиента, а серверу отправляется пакет с сообщением, что соединение больше не нужно. При получении этого пакета сервер также выполняет свою операцию DISCONNECT, подтверждая клиенту получение, и освобождает соединение (6). При поступлении серверного пакета на компьютер пользователя клиентский процесс освобождается и соединение разрывается. Такова краткая схема работы связи с использованием соединений.
Конечно, на практике не все так просто. Многое может пойти не так. Могут возникать проблемы с очередностью происходящего (например, выполнение CONNECT перед LISTEN), теряться пакеты и многое другое. Эти проблемы будут подробнее рассмотрены позже, а пока что илл. 1.30 вкратце резюмирует взаимодействие «клиент-сервер» для дейтаграмм с подтверждением (мы можем игнорировать потери пакетов).
Учитывая, что для одного цикла этого протокола требуется шесть пакетов, может показаться странным, что взамен него не используется протокол без установления соединения. Конечно, в идеальном мире так бы и было. Понадобились бы только два пакета: один для запроса, а второй для ответа. Впрочем, если учесть возможность передачи сообщений большого размера в обе стороны (например, файла размером в мегабайт), потенциальные ошибки передачи и возможную потерю пакетов, ситуация меняется. Если ответ состоит из сотен пакетов, клиенту необходимо знать, не потерялась ли часть из них при передаче. Как он узнает, что последний полученный пакет был последним отправленным? Или представим, что клиент запросил второй файл. Как он сможет отличить пакет 1 второго файла от потерянного и внезапно найденного пакета 1 из первого файла? Короче говоря, на практике простого протокола запрос/ответ при связи по ненадежной сети обычно недостаточно. В главе 3 мы подробно изучим множество протоколов, позволяющих решить эти и другие проблемы. А пока что отметим только, что надежный упорядоченный байтовый поток между процессами иногда может очень пригодиться.
1.5.5. Службы и протоколы
Службы и протоколы не одно и то же. Различать эти понятия столь важно, что мы еще раз подчеркнем их различия. Службы — это набор примитивов (операций), которые нижележащий уровень может делать для вышележащего. Служба описывает операции, которые уровень может выполнять для своих пользователей, но при этом не упоминается о том, как эти операции реализуются. Служба описывает интерфейс между двумя уровнями, один из которых (расположенный ниже) является поставщиком службы, а другой (расположенный непосредственно над первым) — ее потребителем.
Протокол, напротив, представляет собой набор правил, определяющих формат и смысл пакетов (сообщений), которыми обмениваются сущности внутри одного уровня. Протоколы используются такими сущностями для реализации описаний служб. Они могут менять протоколы, как им заблагорассудится, главное — не менять видимую пользователям службу. Таким образом, служба и протокол совершенно независимы друг от друга. Это ключевая концепция, которую должен хорошо понимать любой архитектор сетей.
Еще раз повторим: службы имеют непосредственное отношение к интерфейсам между уровнями (как показано на илл. 1.31). Протоколы же имеют непосредственное отношение к пакетам, пересылаемым между одноранговыми сущностями на различных компьютерах. Очень важно не путать эти понятия.
Илл. 1.31. Соотношение между службой и протоколом
Уместно будет провести аналогию с языками программирования. Служба подобна абстрактному типу данных или объекту в объектно-ориентированном языке. Она описывает, какие операции можно выполнять над объектом, но не уточняет, как они должны быть реализованы. А протокол относится к реализации службы и сам по себе пользователю службы не виден.
Во многих старых протоколах службы и протоколы не различались. В сущности, типичный уровень мог включать примитив службы SEND PACKET10, а пользователь передавал ссылку на полностью готовый пакет. Такое соглашение означало, что все изменения протокола сразу же становились видимы пользователям. Большинство разработчиков сетей считают подобную архитектуру серьезной ошибкой.
9 Дикий кролик (лат.). — Примеч. ред.
10 «Отправить пакет». — Примеч. пер.
1.6. Эталонные модели
Многоуровневая архитектура протоколов — одна из ключевых абстракций в сетевой архитектуре. Важнейшей задачей при этом является описание функциональности уровней и взаимодействий между ними. Ниже рассматриваются две основные эталонные модели — TCP/IP и OSI, а также модель, которая представляет собой компромисс между ними (ее мы и б