Вверх | Вперёд |
Крошечное предисловие. Данный текст готовился как материал для курса лекций; лекции не состоялись, я сохранил материал и решил выложить в общий доступ. Отсюда существенно "академичный" и сжатый стиль. Общая форма изложения и широкий охват материала лучше всего соответствуют, на мой взгляд, понятию "трактат" (в современном мире мало популярном). Текст постоянно дорабатывается; следите за изменениями, кому интересно.
0. В данном трактате рассматриваются основные вопросы выбора и проектирования протокола межпроцессного (сетевого) взаимодействия. В узком смысле эти вопросы относятся к взаимодействию по сети в "обычном" её понимании, таком, как Internet и локальные сети поверх TCP/IP; в "широком" смысле, однако, те же самые принципы применимы без принципиальных модификаций к значительно более широкому классу взаимодействий, таких, как взаимодействие компонент одного хоста (компьютера), нескольких программ через общие форматы файлов, и т.п.
Мы надеемся, что читатель уже знаком с "семиуровневой моделью открытых систем" ("моделью OSI") в общераспространённом "народном" варианте, не связанным с конкретными реализациями ISO; если это не так - обратитесь к соответствующей литературе. (Если кому достаточно напомнить основные понятия - вспомните, в какой схеме есть физический, канальный, сетевой, транспортный уровень, уровень сеанса связи, представления и приложения.) Мы считаем это существенным для данного изложения, независимо от его конкретного применения, потому что принципы построения модели сетевого взаимодействия сейчас используются даже во взаимодействии компонент одного компьютера. Во всём последующем изложении слова "модель OSI" будут означать именно "народную" интерпретацию модели, во всех её вариантах. Также мы считаем, что читающий хотя бы первично знаком с понятиями "сокет", "порт", "хост", "маршрутизатор" и другими базовыми понятиями сетевого взаимодействия, а также знает, чем кодирование отличается от шифрования; знает, хотя бы интуитивно, что такое грамматика и синтаксис. Все перечисленные знакомства необходимы для глубокого понимания; но основные понятия и принципы можно почерпнуть и не имея таких знаний.
Из терминологии, часто требующей определения, упомянем использование слова "посылка" в роли элементарной порции данных, какими обмениваются участвующие стороны, строя из них нужные им взаимодействия; в английском этому соответствует формальный термин PDU (protocol data unit).
В трактате не производится строгого последовательного разделения понятий собственно протокола, то есть правил, какие посылки должны передаваться/приниматься, в каком порядке, при каких условиях и как их содержание должно отрабатываться, и формата данных, то есть что внутри посылок (какие данные и как они закодированы). В некоторых местах "протокол" используется вместо "формата". Но, изучая вопрос построения протокола, следует понимать, что те же принципы годятся и не только в контексте межпроцессного (сетевого) взаимодействия, но и при хранении данных или передаче их без явного протокола.
Глава I. Начальное рассмотрение требований.
I.0. На момент начала рассмотрения мы предполагаем, что кроме общего знакомства с терминологией и моделью OSI Вы представляете себе хотя бы в общих чертах свои потребности и условия их реализации. Начнём с неформального рамочного уточнения их:
I.1. Определение стиля трафика: какие общие требования к таким характеристикам взаимодействия сторон, как скорость, надёжность и экономность передачи?
В любой физически реальной среде передачи действует принцип, аналогичный известному лозунгу: "Мы делаем быстро, качественно и дёшево - выберите любые два". (Это действует как для сетей с коммутацией пакетов, так и для сетей с коммутацией каналов, хотя мы здесь обсуждаем только первые.) Аналогами этих вариантов являются соответственно:
Три комбинации двух вариантов из трёх дают три стиля трафика:
1. Наливной (англ. bulk). Надёжность и экономность передачи достигаются за счёт снижения скорости при проблемах передачи: неподтверждённые данные пересылаются до достижения результата или определения невозможности связи. Яркие примеры применения - скачивание фильма, доступ к базе данных для выписки товара. Яркие примеры реализации - TCP, HTTP, ODBC, SOAP, IMAP4...
2. Синхронный (и его подвид - изохронный). Скорость и экономность важнее, допускается определённая потеря данных при передаче (которая для большинства целей применения такого типа трафика лучше, чем задержка вследствие попыток перепосылки). Яркие примеры применения - voice-over-IP, потоковое видео... Яркий пример реализации - RTP.
3. Управляющий. Важнее достучаться вовремя до получателя и получить подтверждение об этом, чем экономия ресурсов (хотя практически, безусловно, реализации не стараются поглощать всё - и дизайн протокола должен не допускать неконтролируемый рост объёмов и скоростей потоков данных). Примеры реализации - STP, OSPF, сигнальный уровень SIP.
В пределах одного протокола или применения возможно сочетание разных стилей трафика - например, во всех реализациях VoIP есть управляющий трафик и синхронный трафик.
Стиль трафика налагает существенные ограничения на выбор подложки, это будет рассмотрено немного ниже.
I.2. Какая среда взаимодействия доступна? Это может быть, например:
Как правило, среда взаимодействия определена условиями задачи и не может быть произвольно изменена. Однако, часто поверх среды взаимодействия реализованы более высокие уровни, которыми можно непосредственно воспользоваться для своих целей и выбор между которыми более доступен; так, в TCP/IP есть TCP, UDP и SCTP, в ATM можно применить AAL5 для транспорта пакетов переменной длины и другие уровни адаптации (AAL) для транспорта пакетов постоянной длины. Для дальнейшего изложения мы введём термин "подложка", обозначающий среду взаимодействия вместе с выбранным в пределах её возможностей протоколом реализации более верхнего уровня. Таких подложек существует много, но большинство может быть сгруппировано в несколько характерных классов (упорядочённых здесь в порядке убывания важности для изложения):
Класс 1 - неструктурированный потоковый транспорт. Подложка такого типа соответствует транспортному (4-му) уровню модели OSI. К этому классу относятся: протокол TCP в стеке TCP/IP; потоковые локальные сокеты (типа SOCK_STREAM), именованные и неименованные пайпы в Unix, аналогичные средства других ОС.
В таком виде транспорта стороны обмениваются неструктурированным потоком единиц фиксированного размера (называемых байтами, в общем случае, или октетами, при уточнении что в такой единице ровно 8 бит), подложка обеспечивает то, что если данные дошли до другой стороны, то они дошли без изменения, перестановки, выпадения или добавления байтов (октетов). Это иногда называется "гарантией доставки", но следует понимать, что гарантия здесь очень условная, зависит от качества доставки на нижележащих уровнях и работает только за счёт перепосылок. В случае связи в пределах одного хоста (локальные сокеты, пайпы) есть реальная гарантия, если принимающая сторона не забывает забирать данные. Более корректно, чем "гарантия доставки", следовало бы говорить "обеспечение надёжности доставки".
Взаимодействие подобного рода возможно только между двумя сторонами. Схемы взаимодействия между бо́льшим количеством сторон фактически оказываются состоящими из нескольких подобных транспортов.
Оборотной стороной обеспечения надёжности доставки является непредсказуемое время доставки. Увеличению времени доставки способствуют заторы, потери и искажения данных на всех участках пути от отправителя к получателю (считая структуры и буфера в приложении, в ядре и сетевом стеке на любом из конечных хостов, в сетевых адаптерах, в маршрутизаторах и мультиплексорах между конечными хостами). При потерях и искажениях данных необходима их повторная передача. Кроме того, в случае TCP и потокового режима SCTP используется неотключаемый режим "доброжелательности" по отношению к другим участникам сети (о нём ниже), сознательно замедляющий перепосылки недошедших данных; это может ещё больше увеличивать время доставки. В случае, если необходимо выдерживать чёткое время доставки данных (синхронные потоки, например, передача аудио- и видеоданных в реальном времени; "биение сердца" в кластерах, etc.) транспорт подобного вида неприменим. Поэтому, этот вид подложки оптимален для "наливного" применения и слабо пригоден для остальных.
Если взаимодействующие стороны обмениваются "сообщениями" нефиксированного размера (это подавляющее большинство применений) - необходимо обеспечить выделение границ сообщений в протоколе. Может использоваться один из нескольких механизмов - предварительно переданная длина, стаффинг, разбиение на части с предварительной передачей длины каждой части, внешняя терминация; они будут рассмотрены далее.
Типичный API (здесь и далее в изложении мы ориентируемся в основном на язык C и API, аналогичные традиционным для C) для подобной подложки состоит из:
1. Функций создания объекта, который служит интерфейсом между
приложением и реализацией подложки; наиболее часто он называется сокетом
(socket, дословно - гнездо, розетка), иногда используется endpoint (в
частности, для интерфейсов TLI/XTI); pipe - для локальных пайпов.
2. Функции передачи данных; она принимает буфер байтов (октетов), длину
передаваемого и возвращает длину переданного и/или код ошибки. Поскольку
интерфейс потоковый, возможна передача за один вызов только части
данных; тогда надо повторить передачу для оставшейся части. В Unix это
write(), writev(), send(), sendmsg(); в Win32 это send(), WSASend(),
WriteFile().
3. Функции приёма данных; она принимает буфер и его размер и пытается
принять данные. Поскольку интерфейс потоковый, возможен приём за один
вызов не того количества данных, которое соответствует одной посылке
(часть посылки, или часть следующей посылки), может быть принято как
больше, так и меньше, чем одна посылка; поэтому для многих реализаций
необходимо использовать накопительный буфер. В Unix это read(), readv(),
recv(), recvmsg(); в Win32 это recv(), WSARecv().
4. Другие функции (закрытие сокета, получение адреса удалённой стороны,
установление связи, частичный и полный разрыв связи...)
Пример реализации серверной стороны на Питоне. Программа принимает одну порцию данных размером до 20 байт и отдаёт её обратно.
from socket import *
if __name__ == '__main__':
s = socket(AF_INET, SOCK_STREAM, 0)
s.bind(('', 5409))
s.listen(1)
while True:
s2 = s.accept()[0] ## we need only new socket object
data = s2.recv(20)
s2.send(data)
s2.close()
del s2
Аналогичная клиентская сторона:
from socket import *
import sys
if __name__ == '__main__':
s = socket(AF_INET, SOCK_STREAM, 0)
s.connect((sys.argv[1], 5409)) ## port is fixed, host is program argument
s.send("Hello world! 1234567")
print 'received:', s.recv(20)
Подложка такого рода максимально подходит для передачи наливного трафика, хуже - для управляющего и совсем плохо - для синхронного: малейшие проблемы доставки вызывают торможение передачи.
Класс 2 - транспорт, реализующий передачу отдельных относительно коротких порций данных от отправителя получателю без всякой защиты данных от потери данных или перестановки порядка доставки порций данных; также от дублирования данных и иногда даже от их искажения внутри порции (см. ниже в деталях). (В некоторых случаях есть защита от перестановки и/или дублирования, например, в Frame Relay virtual circuit статической настройки, или для локальных сокетов Unix.) К этому классу относятся: транспорты кадров на 2-м уровне (Frame Relay, ATM/AAL5, PPP, HDLC и ему подобные); "голый" Ethernet; IPX в стеке протоколов IPX; UDP, SCTP в датаграммном режиме и собственно IP в стеке протоколов TCP/IP; локальные сокеты типа SOCK_DGRAM в Unix. Для дальнейшего изложения мы будем употреблять по отношению к посылке данных этого класса термин "датаграмма" (datagram), независимо от уровня, хотя обычно датаграмма - это 4-й уровень.
Данные для такого транспорта собираются порциями-"датаграммами". Единственными гарантиями, предоставляемыми сетью в отношении таких данных, являются 1) обеспечение (в большинстве случаев) целостности данных путём использования контрольных сумм, 2) отсутствие произвольных задержек, каждый промежуточный участник старается передать данные как можно быстрее в сторону получателя. Не гарантируются: сам факт доставки; порядок доставки (он может быть нарушен, например, при прохождении двух датаграмм разными путями с разными задержками); отсутствие дублирования датаграмм. Формат датаграмм может быть произволен (UDP), а может быть ограничен заданием фиксированного зависящего от специфики подложки локального префикса и/или суффикса (для Ethernet - заголовок кадра, для IP - IP заголовок), но всегда есть место для размещения своих произвольных данных. Размер произвольной части датаграммы обычно составляет не менее 1000 октетов.
Подложка такого класса более примитивна по сравнению с первым классом; более того, подложка первого класса может быть реализована через второй, но не наоборот (мешает обеспечение доставки). Преимуществом второго класса является возможность реализации над ней практически любого протокола, хоть и местами с заметными усилиями.
Реализации протоколов и транспортов обычно существенно зависят от отсутствия произвольных задержек датаграмм (пакетов и кадров - на более низких уровнях), от того, что передача на всех промежуточных участниках производится максимально быстро (может быть, с поправкой на небольшие выходные очереди, рассчитанные на смягчение неравномерности поступления входных данных, но не более того). Выбор между перепосылкой и потерей данных в такой подложке всегда производится в сторону потери. Только поверх такой подложки может быть реализован синхронный стиль трафика (такой, как аудио- и видеоданные в реальном времени). Реализация TCP также зависит от того, что IP теряет пакеты, но не задерживает их на неопределённое время; если где-то это не так (например, туннель, реализованный поверх SSH) - могут возникать неприятные побочные эффекты. Кроме того, управляющий стиль трафика также удобнее всего реализовывать через такой класс подложки.
В отличие от подложки первого класса, данный класс позволяет использовать многоадресную передачу данных (multicasting), если это поддерживается средой реализации (ethernet-сети, IP сети...) Реализация подтверждений доставки, если нужна, делается на более высоких уровнях.
API для такой подложки сходно с тем, что мы видели для класса 1. Основное принципиальное отличие - в том, что отсутствие понятия "соединение" заставляет указывать адрес при каждой передаче и получать его при каждом приёме. (В BSD sockets это можно ограничить формальным "коннектом", но это только облегчение работы на уровне API.) Разумеется, API должно сохранять границы посылок, поэтому типичная функция получает или отправляет одну посылку за раз.
[[XXX Нарисовать пример клиента на Питоне]]
Тестовый приёмник:
[python]
from socket import *
import sys
if __name__ == '__main__':
s = socket(AF_INET, SOCK_DGRAM, 0)
s.bind((sys.argv[1], 5409)) ## port is fixed, host is program argument
while True:
data, address = s.recvfrom(65536)
print 'got data %r from address %r' % (data, address)
[/python]
В качестве курьёза отметим, что UDP позволяет посылать и принимать датаграммы нулевого размера. Они могут использоваться, например, для поддержания NAT сессии (более подробное изложение этого выходит за пределы данного трактата).
Прямая передача наливного трафика по такой подложке невозможна - требуется реализация уровня обеспечения доставки (TCP или аналог). Так как такая реализация в полном виде является очень серьёзной задачей (желающие могут поискать в Сети задачи и реализацию NewReno для TCP), лучше воспользоваться транспортом 1-го класса. Наоборот, лучше всего ложится на такую подложку синхронный трафик. Для управляющего трафика требуется контроль доставки и перепосылки по отсутствию ответа; это требует своего уровня реализации, но обычно он достаточно прост (значительно проще, чем TCP), достаточно организовать перепосылку с правильными интервалами (см. ниже) и подтверждение доставки.
Класс 3 - транспорт, реализующий передачу отдельных элементарных порций данных фиксированного размера (биты, байты, октеты) постоянным или непостоянным потоком между двумя крайними точками, обычно на постоянной или редко изменяемой скорости скорости, и не защищённый от искажения данных. К этому классу относятся непосредственно каналы связи (синхронные или асинхронные), включая разнообразные модемные соединения.
Этот класс подложек ещё более примитивен, чем предыдущие, и относится к первому уровню OSI; второй класс подложек (датаграммный) строится поверх данного построением передачи кадров (фреймов). Построение передачи кадров является единственным вариантом реализации второго уровня модели OSI поверх такого носителя. Однако передача кадров имеет смысл и для большинства реализаций какого-то собственного обмена данными. Это даёт подложку 2-го класса (датаграммы).
API этого класса в большинстве случаев представляет собой операции открытия, настройки, чтения, записи, закрытия последовательного устройства "порта". Единица передачи данных - байт (не обязательно, но обычно 8-битный), или бит (но для эффективности на уровне API передача идёт байтами или порциями большего размера).
Класс 4 - транспорт, реализующий последовательную упорядочённую передачу посылок с обеспечением доставки. В TCP/IP это SCTP; в IPX стеке это SPX; существуют аналоги в других стеках; в общем интерфейсе BSD sockets это тип SOCK_SEQPACKET. В целом на сейчас это относительно экзотический класс транспорта (все, кому такое было нужно, реализовывали поверх TCP или аналога с указанием размеров посылок). Преимуществом по сравнению с TCP является возможность многоадресной рассылки. Но постепенно с распространением SCTP появляются реализации, использующие его хотя бы для отказа от усложнения реализации поверх TCP в виде накопительных буферов. Кроме того, SCTP имеет преимущество в виде встроенной поддержки нескольких независимых каналов - можно передавать "наливной" трафик по одному каналу и управляющие сообщения - по другому. К сожалению, SCTP за 10 лет развития (2000-2010) пока что стал действительно качественно реализованным в очень малом количестве систем (в основном в *BSD).
По основным функциональным характеристикам этот класс ближе всего к классу 1 (потоковый), частично к классу 2 (датаграммный). С потоковым его объединяет необходимость организации соединения и автоматические средства обеспечения надёжности доставки посылок; с датаграммным - явные границы посылок.
Класс 5 - HTTP транспорт. Этот класс обладает следующими
особенностями:
1. Взаимодействие осуществляется посылкой запросов и получением ответов.
Стороны чётко разделены по ролям - одна сторона является клиентом,
другая - сервером. Спонтанные проявления активности от сервера
невозможны. (Если это нужно и нужны остальные свойства HTTP, можно
посмотреть на новые веяния вроде SPDY или Websockets.)
2. Вместо адреса серверной стороны в традиционном понимании (например,
хост и порт) имеем понятие ресурса и указателя на него. Это расширяет
возможную гибкость построения серверной стороны.
3. Возможно проксирование запросов штатной инфраструктурой реализации,
что приводит к сокращению трафика и других эксплуатационных затрат.
Из других преимуществ - HTTP легче многих других транспортов проходит через файрволлы и прокси. (Это будет обсуждаться в соответствующем разделе.)
Класс 6 - файловый транспорт и удалённое исполнение заданий. Представителями такого класса подложек являются, например, UUCP и FTN, а также отдельно используемые протоколы такого назначения. Особо отметим ZModem со всеми потомками (Janus, ZedZap, DirZap, и непрямой потомок - Hydra), как реализацию такого поверх 3-го класса подложек - последовательного канала.
Возможны и другие, более экзотические, классы подложек; но на сейчас ограничимся перечисленным.
Продолжаем уточнение требований:
I.3. Предполагается взаимодействие двух или большего числа сторон?
Взаимодействие двух сторон является относительно тривиальной задачей по сравнению с многосторонним взаимодействием. Усложнения, вносимые многосторонним взаимодействием, весьма разнообразны и сильно зависят от специфики задачи, могут быть следующие варианты реализации:
1. Один участник выделенный (сервер), взаимодействует со всеми остальными
(клиентами) одноадресными посылками, а клиенты - с ним.
2. Один участник выделенный (сервер), взаимодействует с остальными
(клиентами) в основном многоадресными посылками, добавляя по
необходимости одноадресные; клиенты отвечают одноадресными
посылками.
3. Все участники напрямую взаимодействуют друг с другом (полносвязная
схема, "full mesh").
4. Часть участников (сервера) взаимодействует напрямую с клиентами,
обмениваясь между собой данными (подварианты: полносвязность,
нециклический граф (остовное дерево), граф с возможными циклами).
Нетрудно заметить, что тут обобщены понятия достаточно разных уровней; в реальности построение будет разделять тонкости реализации одного сообщения или потока сообщений, с одной стороны, и логику работы с сообщениями, с другой стороны. Но для данного уровня рассмотрения обязателен целостный взгляд на требования и реализацию.
I.4. Протокол должен быть синхронным, асинхронным, смешанным?
В синхронном протоколе стороны обмениваются посылками, ожидая ответы на запросы и не посылая новых запросов до получения ответов и посылок, не являющихся ответами на запросы. В асинхронном протоколе запросы могут поступать в произвольном количестве и порядке, до получения ответов на предыдущие запросы, ответы тоже могут приходить достаточно произвольно. В некоторых применениях при этом также сложно разделить понятия запроса и ответа. Возможно также смешанное применение, когда часть посылок посылается синхронно, часть - асинхронно.
Показательными примерами асинхронных сетевых протоколов (но с разной
"степенью" асинхронности) являются:
1. Потоковый режим: pipelining mode ESMTP, streaming mode в NNTP. Для
обоих базовых протоколов, расширения pipelining и streaming позволяют
посылать несколько команд не дожидаясь ответа на них и получать ответы в
том же порядке, в каком были посланы запросы (команды).
2. В IMAP4, клиент может посылать несколько команд, указывая к каждой
тег, который будет идентифицировать ответы; кроме того, сервер может
посылать сообщения (например, нотификацию о поступлении новых писем в
почтовый ящик), которые не были запрошены клиентом и поэтому имеют
специальный тег, недопустимый для команд клиента. Ответы на команды
клиента могут приходить в относительно произвольном порядке и не
обязательно в том, в котором были отправлены команды.
3. В DNS, простые клиенты (библиотеки-резолверы) синхронные (посылают
запрос и ожидают ответа), сложные клиенты (другие DNS сервера и
библиотеки-резолверы асинхронного режима) асинхронные - после посылки
запроса могут заниматься другой работой. Рекурсивные сервера DNS всегда
асинхронные и способны отрабатывать много запросов одновременно, для
нерекурсивных более типична синхронная работа. Однако, для DNS само
понятие "не дожидаясь ответа" частично не имеет смысла, потому что
серверная сторона в случае транспорта поверх UDP не ведёт учёт состояния
клиента между запросами. В случае транспорта поверх TCP, протокол DNS
синхронный, новый запрос до получения ответа на предыдущий запрос по
тому же соединению - не допускается, незаказанные данные от сервера тоже
не разрешены.
4. SIP (Session Initiation Protocol) является "предельно" асинхронным
протоколом: две стороны могут устанавливать произвольное количество
транзакций и диалогов, идентифицируемых соответствующими данными (без
явного ограничения длины), и должны быть готовы в любой момент к
получению новых запросов в диалогах и ответов на транзакции, а также
повторения запросов.
В общем случае, асинхронные протоколы сложнее в реализации, но прогрессивнее, потому что имеют преимущества: 1) отсутствие необходимости ждать, когда запрос дойдёт через все промежуточные буфера и каналы, до другой стороны, и тот же путь будет пройден ответом; за это время можно послать следующие запросы, увеличив в результате скорость взаимодействия; 2) возможность одновременного исполнения нескольких запросов противоположной стороной, или оптимизации порядка выполнения. Некоторые применения в принципе невозможно реализовать синхронным протоколом, потому что каждая из сторон может начать передавать новые данные и запросы.
Оборотной стороной является усложнение дизайна участвующего приложения (особенно серверного). Режим типа "принял запрос, отработал ни на что постороннее не обращая внимания, ответил, жду дальше" (для сервера), "послал запрос, жду ответ" (для клиента) в этом случае уже недопустим. При streaming или pipelining режиме, клиент должен быть готов получить ответ даже если он находится в ожидании реализации посылки запроса (send или write, не выполняющийся из-за переполнения выходных буферов и возможно блокирующийся); при тегированных командах, сервер должен быть готов получить новый запрос даже если он имеет команды для выполнения. В обоих случаях это означает переход на какой-то вариант событийно-управляемого или (если не столь сложный протокол) многонитевого построения программы.
При необходимости строить асинхронный протокол желательно об этом позаботиться заранее, особенно если ответы могут приходить в порядке, несовпадающем с порядком запросов. Расширять протокол и менять логику реализации на асинхронную обработку в некоторых случаях может оказаться слишком дорого. При передаче больших сообщений (писем, файлов) также следует позаботиться о том, чтобы передачу сообщений можно было разрывать командами и данными других сообщений. Фактически последнее означает введение мультиплексирования внутри соединения; оно может быть сделано средствами подложки (SCTP) или своими средствами.
I.5. Какие требования на надёжность и повторяемость запрашиваемых действий?
В случае NFS, запрос типа "дайте блок данных файла номер N со смещения P" не создаёт никаких проблем в случае его повторения (кроме увеличения нагрузки на сеть и обе стороны), даже если ответ был не принят: результат не меняется (а если и меняется, то это не проблема протокола). Но уже если данные читаются из последовательного порта, или гипотетическим протоколом, в котором позицию в файле хранит серверная сторона - потеря ответа и повторение операции приведёт к заведомой потере предыдущих данных. Хорошим подспорьем в этом случае оказывается TCP, где он допустим (и считая разрыв TCP соединения достаточной причиной для перезапуска всей сессии взаимодействия). Но в общем случае при построении протокола должны быть рассмотрены меры борьбы с возможными последствиями от повторного приёма одной команды.
Для многих транспортных протоколов, передающих целевые сообщения (например, SMTP, FTN транспорт, NNTP в случае команды POST и отсутствия message-id клиента) есть опасность, что после полной передачи целевого сообщения, принимающая сторона отправит подтверждение приёма (или отказа), но передающая не сможет его принять из-за разрыва связи; такие ситуации приводят к дубликатам сообщений. В случае UUCP, этой проблемы не существует, за счёт того, что передающая сторона всегда отправляет D-файл ранее X-файла, а на приёмной стороне D-файл не отрабатывается, пока не принят соответствующий ему X-файл; ситуация дублирования X-файла приведёт к ошибке типа "нет входных данных", которая легко отделяется от других возможных ошибок. Но подход, как в UUCP, возможен только для заранее сконфигурированных доверенных линков, иначе даётся слишком большой шанс организовать DoS приёмного хранилища данных принятыми, но не обработанными сообщениями (по одному с каждого клиента - уже может дать переполнение ресурсов хранилища). Хорошим промежуточным вариантом может быть организовать хранение на приёмной стороне не задания, а статуса завершения последней передачи; при этом хранится меньше данных, но с другой стороны нужно или использовать неподделываемый идентификатор клиента и не допускать никаких больше идентификаторов (несовместимо с NAT и IPv6), или уничтожать данные от незавершённых передач (увеличивая вероятность дублирования), или допускать вероятность такого же DoS, хоть и при значительно большем количестве клиентов.
В общем случае, при отсутствии постоянно настроенных линков и запоминания прошлого статуса, дублирование сообщений при разрыве в неподходящий момент неустранимо в принципе. Поэтому дублирование должно контролироваться и устраняться внепротокольными методами - например, опорой на уникальные идентификаторы передаваемых сообщений.
I.6. Должен быть текстовый, двоичный формат или промежуточный между ними?
Вкратце, текстовый формат оптимизируется под удобство прямой работы человека с ним в формате, близком к естественным языкам или языкам программирования; двоичный оптимизируется под скорость и простоту работы компьютера или другого электронного устройства. Детальные различия между ними (несколько огрублённые, но действующие для большинства реализаций):
1. В двоичном формате значения кодовых слов (здесь и далее "значение" - то, как оно представлено, в отличие от "смысла", который показывает, что слово означает) достаточно произвольны, часто отражают историю их добавления (с возможной группировкой по какому-то критерию), или удобство нумерации (например, 0 часто выбирается как обозначение несущественного или неизвестного значения признака), или удобство работы со значениями (это особенно характерно для представлений чисел, которые рассчитаны под работу арифметических устройств процессоров). В текстовом формате кодовые слова записываются близко к записи, которая применяется в текстах естественного языка или языка программирования, в частности, количественные данные записываются числами в форме близкой к записи в естественном языке, а качественные - словами на естественном языке или специальном промежуточном жаргоне.
Впрочем, в двоичных форматах для облегчения чтения дампов могут использоваться текстовые символы: например, для однобайтного кода команды могут быть применены 'a' для добавления, 'c' для изменения и 'd' для удаления объекта. В случае ASCII это будут соотвественно коды 97, 99 и 100 (а не 1, 2 и 3, как было бы в случае выдачи последовательных кодов). Пример такого формата - Milter.
2. В двоичном формате любое значение байта может встретиться в любом месте сообщения. В текстовом формате определённые значения (такие, как CR, LF, пробел, управляющие символы) не могут встречаться внутри сообщений или отдельных "слов" в сообщениях. Если нужно их там передать, то формат специфицирует необходимые для этого меры ("квотинг" (quoting), "эскейпинг" (escaping), полная перекодировка в стиле base64). Ряд форматов также ограничивает комбинации кодов (например, требование соответствия всего потока UTF-8, запрет одиночных CR или LF даже в строке данных). Очень редко встречаются управляющие коды, кроме 13 (CR), 10 (LF), 9 (HT).
Исключением из этого правила для двоичных форматов являются описанные далее меры по эскейпингу в форматах поверх подложки 3-го типа (физический канал).
3. В двоичном формате значение могут нести части байтов, вплоть до отдельных битов. В текстовом формате это недопустимо и минимальная самостоятельно участвующая часть посылки - символ (байт или несколько байтов).
4. В двоичном формате части посылок, которые могут быть переменной длины, обычно самотерминирующиеся (в русскоязычной терминологии часто используется термин "префиксный код" по отношению к такой системе кодирования), каждая такая элементарная часть соответствует условию Фано - "Никакое кодовое слово не может быть началом другого кодового слова". (Для частей фиксированной длины это тем более выполняется.) В текстовом формате обычно используется внешняя терминация, то есть часть посылки или посылка целиком завершается байтом (байтами) с кодами, не допустимыми в этой части посылки; например, строка заканчивается последовательностью CRLF, слово в строке - пробелом, значение параметра - точкой с запятой, запятой или CRLF, при этом ни один из таких терминаторов не входит в терминируемую им часть посылки. Например, если команда должна быть завершена внешним терминатором, то допустимы команда "X" и команда "X2". В случае самотерминации, если наличие одного "X" означает завершённое кодовое слово, последующее "2" не должно пытаться интерпретироваться и будет отнесено к другому кодовому слову.
5. Двоичный формат очень часто имеет фиксированный формат с неизменяемыми позициями, размерами и кодированиями отдельных частей сообщения. Пример - фиксированные части заголовков IP, TCP, UDP, Ethernet... Возможна переменная часть, но основная часть сделана с фиксацией позиции, размера и кодирования для предельно быстрой работы с данными. Текстовый формат, наоборот, обычно использует части сообщений нефиксированного размера, требуя разбор синтаксиса сообщения прежде, чем можно будет добраться до отдельных его частей.
Текстовый формат даёт при этом значительно больше свободы в представлении значений; так, в большинстве грамматических элементов большинства форматов не имеет значения размер промежутка между лексемами (один пробел, два или сто), пока не нарушаются некоторые внешние ограничения (такие, как длина строки или размер всей посылки). Кроме того, типичным является игнорирование регистра в словах текстового формата. Мы считаем это плохой практикой - игнорирование регистра требует его конверсии при разборе сообщения и при хранении разобранной формы (например, в SIP нужно держать канонизированный регистр тегов диалога для сравнения, но крайне желательно также держать исходную форму для передачи следующему участнику, потому что есть и реализации, считающие тег зависимым от регистра).
Следствия:
1. Двоичный формат фиксированной структуры (то есть фиксируются расположение, размер и интерпретация полей) значительно быстрее обрабатывается, чем двоичный формат без фиксированной структуры или особенно текстовый формат; чем ниже уровень формата в модели OSI, тем больше это значит практически. Протокол типа IP в случае текстовой реализации (или даже двоичной, но полностью нефиксированной) потребовал бы ресурсов в десятки раз больше, чем сейчас, на каждый пакет. Двоичный формат нефиксированного формата (как BER) всё же обычно обрабатывается быстрее, чем аналогичный текстовый формат, за счёт самотерминации, меньшего размера отдельных элементов посылок и отсутствия потерь ряда реализаций на парсинг грамматики.
2. Наоборот, текстовый формат значительно проще анализируется и синтезируется человеком, за счёт удобных человеку свойств; это даёт потерю производительности, но на высоких уровнях модели OSI она обычно менее существенна, чем лёгкость диагностики и ручного вмешательства.
3. Текстовый формат обычно значительно легче расширяем. В случае двоичного формата требуется принимать специальные меры, чтобы ввести новые возможности, форматы, параметры не нарушив совместимость с прежними; например, если есть поле "тип сообщения", надо специально предусмотреть занятие в первой версии не больше половины пространства возможных значений, чтобы оставить значения для удобного расширения на новые возможности (которые, естественно, заранее предусмотреть нельзя). В случае текстового формата в большинстве случаев эта расширяемость получается сама, за счёт того, что размеры управляющих полей нефиксированы и какое бы слово ни было использовано, можно придумать более длинное:)
Разумеется, разделение на текстовые и двоичные форматы не абсолютно, встречаются переходные формы. Например, в протоколе SSH есть предопределённый комплект шифров, задаваемых значениями целочисленного поля, и есть вариант задания названием - текстовой строкой, длина которой не фиксирована. Таким образом имеем текстовый элемент в двоичном формате посылок протокола. Существует также достаточно большое количество "полутекстовых" форматов с использованием текстовых элементов, но без широких допущений, свойственных настоящим текстовым форматам; например, посылка может быть фиксированного размера с фиксированными позициями и размерами полей, но каждое поле содержит данные в текстовом виде. Мы со своей стороны предполагаем преимущество "полутекстовых" форматов для большинства инженеров и программистов.
I.7. Какие требования к поддержанию функционирования в условиях потенциально или реально противодействующей среды?
I.7.1. Многие протоколы могут реализовывать одновременно постоянное взаимодействие и ряд кратковременных взаимодействий, например, при передаче файлов, однократных порций данных. Такие передачи, если производятся по отдельному соединению подложки, могут встречаться с препятствиями (NAT, файрволл). Здесь мы обозначаем общим термином NAT полный спектр технологий с трансляцией адреса одной стороны, независимо от названия у конкретного поставщика (NAT, PAT, PNAT, маскарадинг).
Классическим примером проблемы является так называемый "активный режим" протокола FTP. При заказе передачи файла соединение, в котором передаётся содержимое файла, открывается клиентской стороной (пассивный режим) или серверной стороной (активный режим). Если клиент находится по отношению к серверу за NAT или файрволлом, допускающим инициацию соединений из внутренней сети, но не из внешней, и этот NAT (файрволл) не содержит специальную поддержку FTP протокола, соединение установлено не будет (потому, что внутренний адрес клиентской стороны не будет доступен серверной стороне и даже неуникален для сети, в которой сервер) и передача не состоится.
В массовом порядке такие же проблемы встречаются с VoIP. Установление прямого соединения между двумя системами, одновременно находящимися за разными NAT шлюзами, или невозможно (если хоть один из этих NAT является симметричным, в терминологии RFC3489, то есть внешний адрес зависит и от внутреннего, и от удалённого), или требует вспомогательного средства типа STUN сервера (если оба NAT конусного типа). Для связи между такими системами требуется промежуточный прокси, передающий поток данных через себя. В случае, если между двумя участниками только NAT в одну сторону, участник за NAT может связаться с участником вне NAT, но при этом он может не знать, какой внешний адрес ему даст NAT, и подтверждение идентичности участника может быть проблемой. Эти проблемы характерны для всех протоколов VoIP с отделёнными потоками синхронных данных (аудио, видео), то есть как минимум для H.323, SIP, MGCP.
Самый простой (но далеко не всегда самый лучший) метод реализовать прохождение дополнительных потоков данных - использовать то же соединение, по которому устанавливалась связь и передавалась управляющая информация. Если вспоминать FTP, то таким изменением является HTTP: вместо создания отдельного соединения - передача данных в теле запроса. В VoIP аналогичным примером является протокол IAX (актуален IAX2), разработанный для Asterisk; весь обмен, включая поток голосовых данных, производится по одному UDP "соединению" (ассоциации между парой адресов). (Этот пример как раз показывает целевую нишу SCTP: в нём можно было бы, при использовании расширений на ограничение попыток или времени передачи одного сообщения, организовать передачу управляющих сообщений и голосового канала данных без влияния второго на первое. Но такая реализация также потребует релеинга данных центральным участником.)
Избавление от одного из источников этой проблемы в виде NAT её не решает полностью. В случае IPv6 предполагается выдача провайдером или корпоративным центральным источником группы адресов, которые видны в остальной части сети без трансляции. Но, во-первых, это не всегда работает - в первую очередь, перенумерация внутренней сети на каждой замене внешнего адреса из-за специфики работы провайдера может оказаться слишком дорогой и неудобной, и тогда всё равно будет использован NAT, даже если и во внешней, и во внутренней сети используется только IPv6. Во-вторых, это не исключает необходимости защиты сети, а базовая политика такой защиты для средней офисной сети реализуется пропуском (под анализатором трафика или без него) исходящих соединений - наружу и запретом явно не заказанных соединений извне - внутрь. В этом случае всё равно требуются дополнительные меры: например, для аудиоканала VoIP обе стороны, согласовав адреса, должны начать посылку тестовых пакетов от себя для создания разрешений в файрволлах, а файрволлы не должны создавать длительные запреты от кратковременных попыток связи, не подтверждённых изнутри.
I.7.2. Связь с потерями.
Приложения должны работать в условиях проблемной связи. Пакеты IP или другой реализации нижних уровней могут теряться с какой-то вероятностью (например, из 100 пакетов теряется в среднем 2, и такая ситуация длится несколько часов), или в какой-то период (например, 2 минуты вообще ни один пакет не проходит), или комбинированным образом (например, долго - 1% потерь, иногда - до 80%). Соединения более высокого уровня от этого могут рваться, передача может становиться слишком медленной. Даже те приложения, что "заточены" на локальные сети, не могут этого избежать - например, любая "локальная" сеть в современном мире может быть "прозрачно" для участников растянута VPN'ами на несколько континентов, из видимых последствий давая только задержки передачи.
Источниками потерь пакетов могут быть:
1. Перегруженные каналы связи (данные поступают быстрее, чем могут быть
переданы; происходит превышение полосы передачи).
2. Перегруженные коммутаторы и маршрутизаторы - происходит превышение
по количеству пакетов, реже по суммарной полосе передачи.
Эти два источника дают частичную потерю пакетов, но в условиях
действующего QoS могут давать полную потерю для низкоприоритетных
потоков.
3. Разрыв канала или нарушение маршрутизации, перестройки маршрутизации.
Этот источник даёт полную потерю пакетов на срок до восстановления
(времена порядка секунд и минут).
4. Административные ошибки. Этот источник чаще всего даёт полную потерю
связи, но иногда приводит к частичным потерям.
5. Ошибки проектирования устройств или программного обеспечения. Этот
источник может давать самые странные и непонятные виды потерь. Например,
автор данного текста сталкивался с коммутатором, который не пропускал
кадры, которые являлись непервыми фрагментами IP-пакета и имели размер
от 200 до 300 байт. Коммутатор был неуправляемым и поэтому проблема
настройками не лечилась. Этот вид проблем самый трудный для диагностики
и обхода, большинство практически известных протоколов не позволяют
обходить такие ошибочные элементы сети.
Ещё с достаточно ранних времён развития Internet были выработаны методы,
которые направлены на защиту локальных участков сетей от перегрузки в
условиях проблем. Наиболее типичным подходом является следующий:
1. В период установления соединения вычисляется RTT - время прохода
пакета до удалённой стороны и обратно. В начальный период (до
установления) берётся некоторое значение времени по умолчанию (для TCP
обычно 3 секунды, SIP - 0.5 секунды).
2. При обнаружении проблем связи первая перепосылка происходит через
время чуть большее RTT, а каждая последующая - после предыдущей
перепосылки через время, в 2 раза большее предыдущего времени.
Например, если RTT было равно 120 мс, перепосылки будут идти со
следующими интервалами: 120, 240, 480, 960, 1920 мс... (каждый интервал
считается от предыдущей перепосылки, а не от начала момента проблемы).
Такому удвоению препятствует только общий таймаут доставки.
Такой подход применяется для наливного и управляющего трафика и считается "щадящим" для всех описанных видов источников потерь пакетов. В TCP и SCTP он заложен во все реализации протокола и не может быть устранён настройками передающей стороны (если не делать полностью свою реализацию). Он не применяется напрямую для синхронного трафика, потому что для последнего перепосылка является бессмысленной. Вместо этого могут применяться меры явного контроля; например, для RTP применяется обмен RTCP пакетами для подтверждения получения, при отсутствии такого подтверждения передача может тормозиться.
Из описанного подхода с удвоением интервала следует, что среднее время восстановления передачи после полной потери пакетов будет равно половине длительности периода полной потери пакетов (а полное время замирания - полутора таким длительностям); не следует ожидать немедленного восстановления передачи после восстановления работы транспорта.
В TCP и SCTP, кроме собственно увеличения времени передачи, регулируется размер передаваемой за один цикл порции данных; при потерях он сокращается вплоть до одного пакета. Восстановление полной скорости передачи может происходить не быстро (единицы и десятки RTT).
Синхронный транспорт сам по себе не является гарантией успешной передачи. Если каждый пакет несёт независимую порцию данных (типично для VoIP), даже заметный процент потерь может испортить звук, но не передачу в целом. Сложнее с видео. В моих опытах с рядом технологий видео, незначительный процент потерь мог испортить передачу; с одной из них (название не называю, потому что ошибка общая) 0.1% потерь приводил к регулярным рывкам, 0.2% - к замираниям изображения, 0.3% - к срыву передачи (без восстановления) за пару минут. Это очень слабая устойчивость, потому что в Internet случайные потери порядка 1% - распространённое явление. Частая ошибка проектирования видеопотока - использование "ключевого кадра", который может быть принят только в целом виде; для передачи в реальном времени часть полного обновления экрана должна поступать часто и не зависеть от других пакетов. Это не соблюдается в протоколах, которые созданы просто "растягиванием" по сети формата хранения видео на диске.
Достаточно тяжёлым для автоматического обхода случаем является непрохождение пакетов по превышению MTU (Maximal Transmission Unit) канала связи. В локальных сетях на основе Ethernet обычное значение MTU - 1500, иногда существенно больше (для ряда применений рекомендуется разрешать jumbo frames, позволяющие до 9000 байт кадра уровня 2). В то же время минимум, установленный для IPv4 - 68 байт, для IPv6 - 1280. Многие реализации технологий связи между клиентом и провайдером или организации частных VPN соединений, такие, как PPPoE, PPTP, "отбирают" до 40 байт от предела MTU. Большинство реализаций сетевых протоколов не рассчитаны сами на урезание размера передаваемого IP пакета в случае отсутствия подтверждения прохождения, и полагаются на штатные меры IP стека, такие, как path MTU discovery (PMTUD или MTUPD). MTU полагается на получение сообщений ICMP о превышении MTU канала, но если сообщения не генерируются (типичный вариант, к сожалению, для сложных условий вроде передачи пакета внутрь тоннеля), или не пропускаются из-за некорректной настройки файрволлов, недостаточно реализованного NAT, то связь может нарушаться по тяжело диагностируемым причинам. Переход на IPv6 ухудшает ситуацию за счёт того, что промежуточные маршрутизаторы обязаны не фрагментировать пакет, не проходящий в канал в целом виде и имеющий (как в IPv4) разрешение на фрагментацию, а уничтожать его с отправкой ICMP отправителю; поэтому подложки типа UDP получают дополнительный источник потерь пакетов (а TCP, SCTP - задержек доставки). В целом, проблема MTU является "тёмным углом" текущих реализаций IP сетей и в первую очередь Internet.
I.7.3. Связь с порчей пакетов.
(В данном разделе речь только про IP. Впрочем, аналогичные выводы можно сделать и для других подложек.)
Другим видом проблем, которые влияют на протоколы, является порча пакетов. Формально это нереальная ситуация, потому что содержимое пакетов защищено как минимум тремя контрольными суммами:
Однако, первая сумма играет роль только внутри каналов связи; вторая - пересчитывается в IPv4 (но не в IPv6) в каждом маршрутизаторе и не защищает данные. Третья - контрольная сумма целевого протокола - зависит от заголовка протокола 4-го уровня (TCP, UDP) и может пересчитываться, если вообще возможно изменение этого заголовка - в маршрутизаторах с "интеллектуальными" возможностями. На практике оказывалось, что при передаче нескольких гигабайт данных по одному соединению несколько байт могли быть "битыми" и это было вызвано сбоями памяти маршрутизаторов. Также, эти контрольные суммы слабо защищают от некоторых видов проблем (например, в случае IP, TCP, UDP это модификация в любом месте соседних пар байтов с одновременным увеличением и уменьшением).
Реализации стеков могут также вносить свои специфические особенности, не связанные напрямую с порчей данных в памяти. Например, в ранних ядрах Linux 2.2.* была ошибка, приводящая к вставлению 4 нулевых байт в поток на каждые 4 переданных гигабайта данных. Данная ошибка была очень долго незамечена из-за редкости таких видов взаимодействия (передача порции данных более 4GB за раз была в конце 1990-х редчайшей ситуацией) и всплыла после распространения innfeed, который держал активные соединения постоянно, вместо периодического innxmit.
По сказанному, если придаётся значение целостности передаваемых данных, нужно проверять посылки своими контрольными суммами (уровня не менее CRC32, а лучше SHA1;)) Для подложек стиля TCP и отсутствии фрейминга пакетов придётся в случае ошибки разорвать соединение и установить заново, для подложек с собственной пакетизацией (UDP, SCTP) можно обойтись перепосылкой отдельных данных.
I.7.4. -- XXX динамические адреса
Ряд Интернет-провайдеров не заботится о сохранении IP адреса за клиентом, выдавая его каждый раз заново из доступного пула. В случае обрыва и установления связи заново, требуется перезапуск сессии и (если нужно) восстановление параметров клиента из сохранённого состояния предыдущей сессии (с другим адресом).
XXX чрезмерная буферизация
XXX Передвинуть детализацию общих проблем в главу 2.
Copyright (C) 2006-2013 Valentin Nechayev. All rights reserved.
Разрешается полное или частичное копирование, цитирование.
При полном или частичном копировании ссылка на оригинал
обязательна.
Вверх | Вперёд |