Назад Вверх Вперёд

Глава IV. Организация текстового протокола.

(Просим обратить внимание, что в данной главе _не_ рассматриваются XML, SGML и родственные им протоколы, см. соответствующую главу).

Описанные выше отличия текстового протокола (и формата) от двоичного приводят к существенно другой реализации на всех уровнях. Однако список вопросов, на которые надо ответить при организации протокола, существенно схож с таким списком для двоичного протокола. А именно:

IV.1. Какая будет использоваться кодировка текста, с точностью до ключевых символов, используемых для основных разделителей, команд протокола, прочих синтаксических элементов с постоянными значениями.

На сейчас, для подавляющего большинства протоколов ответ на этот вопрос будет - Unicode, возможно также ASCII (с массой возможных альтернативных имён, включая ISO646, IA5, us-ascii). Кодировки, несовместимые с ASCII, сейчас существуют на ряде устойчиво развивающихся платформ, таких, как IBM S/390 (она же zSeries), но это исключительные случаи по сравнению с общей массой.

При выборе Unicode, следует уточнить транспортный формат; чаще всего в сети используется UTF-8 (для которого ASCII является совместимым частным случаем), но иногда используется UTF-16, тогда такой протокол несовместим с ASCII.

Многие протоколы оговаривают использование ASCII как базы, но при этом практически допускают использование любой восьмибитной кодировки как расширения (в области кодов 128-255), что допускает как UTF-8, так и традиционные восьмибитные кодировки. Однако, современная традиция в основном рекомендует устанавливать в качестве нормы интерпретацию кодов 128-255 в соответствии с UTF-8. (Это имеет ещё одно преимущество в ряде случаев; например, FTP имеет систематические проблемы с пониманием 0xFF, но UTF-8 не использует 0xFF.)

IV.2. Как будут кодироваться отдельные элементы посылок?

IV.2.1. В большинстве практически значимых реализаций текстовых протоколов, основной особенностью кодирования отдельных элементов посылок является их внешняя терминация. То есть, определение того, где заканчивается одна посылка (например, представление числа) и идёт следующая часть сообщения, не определяется заранее по содержанию прочтённой части самой посылки, а определяется следующими за ним символами. Преобладает традиция использования в качестве таких символов-терминаторов тех же символов, что используются в тексте на естественном языке; это пробелы, знаки препинания (запятая, точка с запятой, скобки), переводы строк (CR, LF, CRLF). Обычно, грамматики описывают те символы и определённые комбинации символов (лексемы), из которых может состоять представление посылки, определяя таким образом, что как только следующий символ не может быть добавлен к представлению элемента посылки, представление этого элемента считается законченным, а следующий символ должен относиться к разделяющему элементу грамматики (тоже элементарному элементу посылки).

Здесь мы применили известный термин "грамматика" не определяя его. Вопросы грамматического разбора, теория грамматик и формальных языков не являются целью данного описания и не будут затронуты даже поверхностно. Тем не менее, без их упоминания нельзя обойтись в данном описании.

Типичной чертой грамматик текстовых протоколов является игнорирование пробелов и приравненных к ним символов между элементарными целевыми посылками и разделителями, а также между разделителями и другими разделителями.

IV.2.2. Что делать, если в посылке надо передать содержимое, которое "просто так" (последовательностью символов) не может быть передано согласно действующей в месте её передачи грамматике? (В подавляющем большинстве случаев это проблема только текстовых полей; обычно грамматика рассчитывается на то, что если передаётся число, дата/время или другое нетекстовое содержание, у его формата не было конфликта с допустимыми в этом месте символами.) Меры, применяемые для этого, обычно называются английскими словами escaping и quoting, реже - stuffing; так как адекватного устойчивого переводного термина для выражаемых ими понятий нет, мы будем передавать их как "эскейпинг" и "стаффинг". Термин stuffing чаще используется, когда символы передаваемого не меняются, но вокруг них вставляются специальные символы или последовательности для изменения интерпретации; escaping - когда они меняются на другие, тем самым гарантированно исключаясь из содержимого. Но однозначности в этой терминологии нет.

Пример решения с эскейпингом является метод протокола HTTP (позднее скопированный всюду где используются URL'ы): часть URL или другое поле ограничивается контекстно-зависимым множеством ограничительных символов, никакой из которых не может находиться внутри этого поля. Внутри поля, любой символ из тех, что не допустимы напрямую в этом поле, кодируется как %XY, где XY - две шестнадцатиричные цифры, представляющие код символа. (Разумеется, сам знак процента передаётся как %25, для однозначности парсинга.)

Примером решения со стаффингом является передача произвольных текстовых строк в поле локальной части адреса в RFC822 и потомках, а также текстовых констант в языке Си и ему подобных. Local-part произвольного содержания в RFC822, текстовая константа в Си передаются строкой в кавычках, причём если внутри строки надо передать кавычку, один из вариантов (в Си), единственный вариант (в RFC822) передавать её как \". Это отличается от первого решения (%XY в HTTP и URL) тем, что интерпретация данных на предмет конца посылки в любом месте представления зависит от предшествующего состояния - например, в представлении "\\\\\"\\" хорошо заметно, как одни и те же символы по-разному понимаются в зависимости от того, что было раньше, и не ведя историю парсинга мы не можем сказать, закончилась строка на конкретной кавычке или нет.

При построении нового протокола лучше использовать эскейпинг, чем стаффинг. Причиной этому является значительно более широкий выбор средств разбора в случае эскейпинга: в зависимости от доступных средств можно искать ограничение множеством символов, применять библиотеки регулярных выражений, и так далее. В случае стаффинга, просто множеством символов не обойтись; приходится использовать полную грамматику или сложные регулярные выражения.

Ещё примером решения без стаффинга является передача текстовых строк в SQL и аналогичные подходы, где принято следующее: ограничительный символ (обычно кавычки - ") удваиваются там, где нужно передать такой символ внутри строки; одинарный же символ кавычек означает конец строки. Этот вариант опять же хорош тем, что не надо хранить состояние разбора содержимого строки в процессе её чтения. Разбор синтаксиса немного усложняется, но задача пропустить строку без разбора её содержимого остаётся значительно легче реализуемой, чем для стаффинга; но в отличие от прошлого раза это не мешает стаффингу (при запрете стаффинга для ограничительного символа). Например, можно написать "\x12""a" и это будет соответствовать строке из трёх символов ['\x12', '"', 'a']. Это решение, однако, и не является эскейпингом в описанном выше понимании; для него требуется свой термин.

В спецификациях текстовых протоколов достаточно часто используется термин "квотинг" (quoting), в котором не уточняется, используется эскейпинг или стаффинг, хотя чаще это соответствует стаффингу. Для точного понимания надо смотреть грамматику протокола. Может быть в одной системе правил и эскейпинг, и стаффинг (для строк Си, \\ это стаффинг, а \n это эскейпинг).

История RFC822 является характерным примером провала формата, использующего стаффинг. RFC822 допускает такие случаи, как разделитель строк, на самом деле не разделящий их; последовательность (в представлении текстовой константы Си)


"To: \"X\\\r\nyyy:\" <a@b>\r\n"
(для понятности запишем её же посимвольно:

['T', 'o', ':', ' ', '"', 'X', '\\', '\r', '\n', 'y', 'y', 'y',
 ':', '"', ' ', '<', 'a', '@', 'b', '>', '\r', '\n'])

должна пониматься как одно поле заголовка "To" (с адресом <a@b> и отображаемым именем с экранированным CRLF внутри), но для этого надо производить полный грамматический разбор. Но практически всё реальное ПО этого не делает, выполняя разбор заголовка на поля по строкам раньше разбора полной грамматики, в результате чего эта последовательность будет понята как два поля заголовка - "To" и "yyy". RFC2822 устранил эту опасность и нормализовал наиболее диверсионные случаи, но стандартом по-прежнему считается RFC822.

IV.2.3. Кодирование целочисленных данных, как правило, реализуется текстовой передачей десятичного представления числа, возможно, с ведущим знаком ('-' обязательно для отрицательного числа, '+' необязательно или обязательно для положительного). Более редкий, но известный вариант - использование шестнадцатиричной или восьмеричной системы счисления (локальные условия могут налагать или не налагать ограничения на регистр букв A-F).

Однако многие средства текстового представления, использующие оптимизацию внешнего вида в соответствии с настройками локализации, могут давать неожиданные эффекты. Например, для США принято улучшать читаемость больших чисел разделением запятыми на группы по 3 цифры начиная справа (123456 будет записано "123,456"). Для многих стран Европы с этой же целью вставляются пробелы вместо запятых. Если грамматика не предполагает запятых или пробелов в числе, чтение представления будет сбито. Такое форматирование следует отключать, или делать не конфликтующим с грамматикой (например, в качестве разделителя применять '_'). В общем случае следует для сетевых протоколов, как средств межмашинного общения, устранять всякую локализацию - представление должно быть однозначным и максимально близким к понимаемому стандартными библиотеками и утилитами. (См. ниже то же самое для отдельных видов данных.) О проблемах и неожиданностях такого представления есть замечательная статья "Does Your Code Pass The Turkey Test? ", которая описывает широкий набор проблем, которые могут встретиться на этом пути.

IV.2.4. Кодирование числовых данных с плавающей точкой, как правильно, реализуется в формате, соответствующем представлениям "%e", "%f", "%g" форматных средств языка Си (для краткости опишем это представление именно так). Выбор конкретного варианта из этих (фиксированная точка, плавающая точка, смешанное) определяется задачей и значением кодируемого числа.

Такие же проблемы локального представления, как в случае целых чисел, имеют место и для плавающей точки. Нормы ряда стран требуют тут запятую вместо точки. Традиционным общепринятым, всё же, является представление с точкой. Для сравнения, десятичное представление BER вообще устраняет этот разделитель (например, 1.23 представляется как "123" с порядком -2).

IV.2.5. Кодирование даты, времени, интервалов подчиняется в основном тем же правилам, что были рассмотрены в случае двоичного протокола. Однако текстовая передача накладывает свои особенности, не связанные напрямую с текстом, но отражающие типичные ошибки:

1. Проблема неожиданных представлений при использовании штатных средств генерации текстового формата. Например, вызвав mktime() и получив в ответ "нд 7 вер 2008 09:52:28 EEST" (украинская локаль), вы собьёте с толку всё, что не умеет читать дату по-украински. Если почему-то в формате установлена текстовая дата подобного стиля (или похожего, как arpadate) - надо позаботиться, чтобы локаль не сбивала представление. Возможно, лучше даже обойти стандартные средства или выбрать из них только однозначное; так, strftime() с форматом "%Y-%m-%d.%H-%M-%S" даёт одновременно человеко- и машинно-читаемое представление даты/времени без проблем делокализации. Как уже говорилось, для практики лучше использовать один из стандартных форматов, на сейчас лучшим выбором видится ISO8601.

2. Даже если вы пытаетесь представить дату более стандартно, может оказаться, что кто-то не знает, как это читать правильно. Например, представлять дату как "дд.мм.гг" (традиционный для русского языка вариант) может сбить с толку американцев, привыкших к тому, что месяц идёт раньше ("мм/дд/гг"); частично это можно опознать по другим разделителям, но иногда и разделители не помогают (против тех, кто наивно предполагает свой формат). Представление "гггг-мм-дд" является достаточно надёжной защитой против таких эффектов - оно удобно для текстовой сортировки и одинаково чуждо всем. В случае необходимости также передавать время, можно использовать, например, формат "гггг.мм.дд.ЧЧ.ММ.СС" (для strftime - "%Y.%m.%d.%H.%M.%S"), но мы опять-таки будем рекомендовать ISO8601 и аналоги.

IV.3. Вопрос размеров посылок, их частей и конечных полей данных для текстового протокола, как правило, не является существенным. Ограничения могут накладываться способностями средств используемых платформ (например, где-то не умеют разбирать 64-битные целые числа), но реально подложка и конечная задача накладывают больше ограничений. В текстовых протоколах размеры отдельных элементов часто не ограничиваются в принципе (как в SIP) или ограничиваются непропорционально большим числом символов.

Однако, сочетание безразмерного протокола с ограниченным по возможностям транспортом может приводить к конфликтам. Например, в текстовом SIP при превышении посылкой размера 1300 байт рекомендуется переходить с UDP на TCP. Аналогичные проблемы с RADIUS вызвали появление TCP транспорта у DIAMETER (RADIUS двоичный, но проблемы те же). Ниже это обсуждается более подробно.

IV.4. Как будет устроено опознание и согласование применяемых версий протокола?

Особенности текстовых протоколов в виде внешней терминации существенно смягчают проблемы расширения, опознания и согласования версий. Тривиально построить двоичный протокол так, чтобы его нельзя было расширить или даже узнать версию - заняв все возможные значения посылок и не оставив резервов на расширение. Аналогично, для системы команд процессора достаточно не ввести команду опознания версии, чтобы опознание стало проблемой (например, для x86 линии, прежде чем просто проверить право вызвать CPUID битом EFlags, надо убедиться, что процессор минимум 32-разрядный; Intel гарантирует работу своего метода такого опознания только для реального режима; заметим, что потеря бита в EFlags - тоже грубый костыль, вызванный изначальной недоработкой). При внешней терминации, наоборот, крайне сложно добиться такого эффекта - даже если команды обычно состоят из 2-х алфавитно-цифровых символов, все 36*36==1296 кодов не будут заняты. Но возможность такого согласования должна быть адекватно поддержана:
1) Другая сторона не должна рвать соединение или производить другие деструктивные действия по неизвестной команде, а вместо этого должна сообщить о неизвестной команде. Соответственно, такая функциональность должна быть изначально заложена в протокол.
2) Команды, связанные с версиями и возможностями, должны одинаково быть определены у всех (а не так, что у одних version, у других GetVer, а у третьих EHLO). То же касается и правил задания параметров в командах и ответных данных.

IV.5. Как будет выглядеть адаптация обмена данными к применяемой подложке?

IV.5.1. Аналогично двоичному протоколу, заметной проблемой является ограничение размера элементарной посылки в датаграммном транспорте и поверх канала связи, и влияние посылок чрезмерного размера на потоковый транспорт. Для текстовых протоколов эта проблема стоит острее - за счёт большего размера посылки при том же смысле содержания одной посылки протокола. В некоторых случаях разница в размерах может достигать 20-50 раз (например, если сравнить PER и XER; впрочем, обычно разница не столь велика).

Фактически, идея использования текстового формата посылок или даже отдельных элементов существенного размера (от десятков байт) поверх транспортных подложек типа 2 (датаграммы) уже имеет существенные проблемы (решение которых приводит к аналогу 4-го класса или напрямую, или поверх аналога TCP); типа 3 - более реален за счёт "ручного выпиливания" кадров достаточного размера, но только до тех пор, пока не начинаются частые перепосылки из-за зашумлённости линии и сбоев в данных (например, если кадр размером 1000 байт оказывается "битым" в 10% случаев, то при том же уровне шума для 2000 байт - уже 19%, для 5000 байт - 41%). За редкими исключениями, промышленно значимые решения применяют готовые сетевые стеки или свои аналоги, используя как подложку транспорт 4-го класса в нашей нумерации (SCTP, SPX, свой фрейминг поверх TCP) или 1-го (TCP). Пример SIP является частичным исключением - за счёт того, что в большинстве случаев посылка вмещается в стандартный IP пакет без фрагментации (до 1400 байт).

IV.5.2. Для потокового транспорта (как TCP), существенным является адекватный выбор метода разграничения отдельных посылок. Применяемые для двоичного протокола методы, такие, как передача длины посылки (неважно, в двоичном или текстовом виде) неадекватны для текстового протокола: при генерации сообщения человек не в состоянии адекватно подсчитать его длину, особенно если сообщение превышает треть экрана. Поэтому необходимо использовать другие методы, основанные на внешней терминации посылки.

Пример 1. Одним из наиболее старых, но используемых, является метод строки из точки, применяемый в SMTP, POP3, NNTP. Этот метод можно определить следующим образом:
1) посылка передаётся как последовательность текстовых строк (что означает, что она обязана завершиться последовательностью конца строки, обычно это CRLF или LF;
2) строка полезной нагрузки посылки, начинающаяся с точки ('.'), передаётся с удвоением этой точки;
3) после посылки идёт строка, состоящая из одной точки.

В спецификациях протоколов принято описывать такое завершение строки последовательностью последних символов (например, RFC1939: Hence a multi-line response is terminated with the five octets "CRLF.CRLF").

Приёмная сторона получает строки посылки и, как только получена целая строка, проверяет, начинается ли она с точки. Если строка состоит из одной точки (не считая завершителя строки, CRLF или LF), это означает конец посылки; если строка начинается с двух точек, одна точка удаляется, а остальное считается строкой полезной нагрузки; наконец, если строка не начинается с точки, она целиком считается строкой полезной нагрузки. Или, во многих случаях дешевле, первая точка проверяется сразу в начале строки.

Например, посылка из трёх строчек


abc
.xyz
def
будет передана так:

abc
..xyz
def
.

(Заметим, что данный метод оставляет неприменённым вариант, когда уже по сети передаётся строка, у которой первый символ - точка, а второй - какой-то другой (но не CR или LF); такие последовательности могут использоваться в особых целях, например, для передачи команды отмены или атрибутов передаваемых данных.)

В этом семействе протоколов (SMTP, POP3...) метод завершающий строки из одной точки используется только там, где по контексту должно передаваться неограниченное напрямую и многострочное тело сообщения. С другой стороны, ничего не мешает применить его для всех посылок (включая команды), если это удобно для реализации протокола.

Внутри передаваемого этими протоколами содержимого, как правило, существуют две части - заголовок и тело, разделённые пустой строкой (продолжение письменных традиций англоязычного и не только мира). Таким образом, пустая строка является завершением заголовка. (Это уже пример 2.)

Пример 2. Внутри почтовых сообщений и тел передач по HTTP, при использовании практически стандартного сейчас формата MIME, части сообщения ограничиваются (и сообщение завершается) с помощью сгенерированной случайной последовательности текстовых символов. Например:

Content-Type: multipart/mixed; boundary="zza93487r9ieywyri3q4yyriwqeriewqu"

--zza93487r9ieywyri3q4yyriwqeriewqu
Content-Type: text/plain

hello world
--zza93487r9ieywyri3q4yyriwqeriewqu
Content-Type: text/html

<html><body><em>hello world</em></body></html>
--zza93487r9ieywyri3q4yyriwqeriewqu--

XXX ещё примеров

Какой вывод можно получить из этих примеров? Во-первых, мы имеем всё ту же внешнюю терминацию (ничего нового), но для которой элементарными частями являются уже не байты, а строки. Во-вторых, добавление точки в начало строки является новой формой стаффинга (старой была бы передача отдельной строки со значением "следующую строку не рассматривать как терминатор, а принимать буквально"), если рассматривать на уровне строк, и обычным стаффингом, если рассматривать на уровне байтов. Тем не менее такой вариант стаффинга прост и эффективен именно на уровне строк. Разумеется, можно было бы применить любой другой вариант стаффинга; но этот достаточно удобен, чтобы не думать о других без особых на то причин. Формально, вариант MIME, хотя более удобен в примитивной реализации, потенциально конфликтен - его надёжность обеспечивается только за счёт гарантии случайности каждого сгенерированного значения границы (boundary) и соответственно его несовпадения с предыдущим значением; зато, если сравнивать с рассмотренными подходами для двоичных форматов, он является стаффингом, но за счёт уникальности строки границы работает как эскейпинг - не нужно анализировать каждую строку на признак границы. (Реально, насколько нам известно, ещё не было случаев совпадения значений boundary, так что эту проблему следует считать нереальной.) Для практического применения годятся оба подхода.

XXX


Рекомендации к разработке, обсуждённые в трактате и сведённые в конце краткими пунктами.

1. На основании изучения главы 1 определите основные требования к протоколу: синхронный или асинхронный,

Copyright (C) 2006-2015 Valentin Nechayev. All rights reserved.
Разрешается полное или частичное копирование, цитирование. При полном или частичном копировании ссылка на оригинал обязательна.

Назад Вверх Вперёд