Беглый обзор AMF с выводами.

Tags:

Доклад для 10-й конференции BAFPUG.

Что такое AMF

AMF (Action Message Format) -- стандарт сериализации/десериализации данных.

Данные в этом формате флэшклиент посылает различным серверам (медиа сервера, BlazeDS, LCDS, ColdFusion и др.), посылает сам себе через LocalConnection, хранит в SharedObject, записывает в ByteArray (writeObject, readObject).

То есть, для флэш-платформы AMF -- это повсеместно используемый стандарт. Реализация его скрыта от разработчика в недрах флэш плеера, так что флэш разработчики используют его, даже не задумываясь о его существовании. И это правильно :)

AMF -- бинарный формат, и он лучше строковых форматов передачи данных JSON или XML, потому что он компактнее (расходуется меньше траффика), и потому что он быстрее сериализуется и десериализуется.

Зачем, вообще, нужно знать об устройстве AMF?

  • просто из любопытства;
  • чтобы более эффективно его использовать;
  • чтобы написать собственную реализацию AMF в собственном сервере :)

Спецификация

Формат AMF открыт, спецификация на него опубликована на сайте Адоби.

И эта спецификация невелика -- pdf документ на 13 страниц. Так что любой любопытствующий может изучить ее за небольшое время.

Проблема в том, что эта спецификация последний раз обновлялась в 2006 году. После этого в формате были некоторые изменения (вернее, добавления). И они не опубликованы. Но, как водится, изучены заинтересованными лицами и реализованы в коде третьих сторон :)

Есть две версии AMF. AMF0 был актуален во времена 6 - 8-го флэш плеера и AS2. AMF3 вышел вместе с AS3, и был существенно улучшен. В частности, улучшена упаковка данных, и, соответственно, уменьшился трафик при передаче.

Спецификация определяет, как кодируются числа, строки, массивы и объекты любой сложности. И сейчас мы слегка затронем некоторые моменты. (Слегка, ибо дублировать спецификацию в этом докладе нет никакого смысла :)

Кодирование чисел

Кодирование типов int и uint осуществляется с помощью Variable Length Unsigned 29-bit Integer Encoding (U29)

Variable length означает, что в зависимости от числа, может быть использовано от 1 до 4-х байтов для его кодирования. Unsinged означает не знаю что, ибо знак числа сохраняется :) 29 бит означает, что для хранения данных используется 29 бит (или меньше). Оставшиеся 3 бита используются как флаги.

Если число меньше 128 (укладывается в 7 бит), то для его хранения используется 7 бит, а 8-й бит используется как маркер того, хватает ли нам одного байта, или следующий байт тоже нужно учитывать.

Из следующего байта опять используются 7 бит для хранения данных, и 8-й бит указывает, нужно ли учитывать третий байт. И так далее :)

Итого, в 4-х байтах (29 битах) можно уместить число до 2^29 - 1.

Интересно, что отрицательные числа всегда используют все 4 байта, даже если это -1.

Числа большего размера, а также тип Number упаковываются в 8 байт по стандарту IEEE-754. Собственно, так они и представлены в самом AS3(Number) и в Java(Double). То есть, они даже не упаковываются, а передаются как есть :)


var so1 : SharedObject = SharedObject.getLocal("so1");
so1.data.val = 123;
/*
76 61 6C  val
04        integer marker
7B        123
 */

Кодирование строк

Строки кодируются просто. Сперва идет header строки. В нем первый бит указывает на то, хранится ли тут сама строка, или только ссылка на нее в reference таблице (об этом позже). Далее указана длинна строки в формате U29 (правда чутка модифицированном, ибо 1 бит уже занят для других целей).

Далее выбираются байты соответственно длине строки (или по ссылке берется строка из таблицы). В этих байтах лежат обычные UTF-8 символы (где один символ может занимать от 1 до 4-х байт).


var so2 : SharedObject = SharedObject.getLocal("so2");
so2.data.val = "123";
/*
76 61 6C  val
06        string marker
07        string header 00000111 (3 

Кодирование не типизированных объектов

Не типизированный объект кодируется так:


var so3 : SharedObject = SharedObject.getLocal("so3");
so3.data.val = {id:123, name:"David"};
/*
76 61 6C        val
0A              object marker
0B              0000|1|01|1 dynamic|not traits-ref, not traits-ext but traits|not ref but value
01              string header for empty string, no class name
09              string header 00001001 (4 

Картинка для наглядности:

Кодирование типизированных объектов

Типизированный объект кодируется так:


package vo
{
import flash.net.registerClassAlias;

registerClassAlias("vo.User", vo.User);

public class User
{
	public var id : uint;
	public var name : String;
}
}

var user : User = new User;
user.id = 123;
user.name = "Bob";
var so4 : SharedObject = SharedObject.getLocal("so4");
so4.data.val = user;
/*
76 61 6C     val
0A           object marker
23           0010|0|01|1  has 2 fields|not-dynamic|traits|value
0F           00001111 (7 << 1) | 1 class name length
76 6F 2E 55 73 65 72  vo.User
05           string header
69 64        id
09           string header
6E 61 6D 65  name
04           integer marker
7B           123
06           string marker
07           string header
42 6F 62     Bob
 */

Reference таблицы

Очень важный момент в AMF, это то, что если в одном пакете данных одинаковая строка повторяется несколько раз, или несколько раз повторяются одинаковые объекты, то сериализуется только один экземпляр строки или объекта, он сохраняется в специальные reference таблицы, и потом в местах вхождения строки(объекта) вместо них самих вставляются ссылки на данные таблицы.

Таких таблиц существует три: для хранения строк (Strings), для хранения сложных объектов (Complex Objects) и для хранения частей объектов (Object Traits).

На первый взгляд выглядит это клево и обещает существенно уменьшить трафик. Однако все не так хорошо. Такие таблицы были бы очень эффективны, если бы работали на протяжении все сессии общения клиента и сервера. Но, увы, это уже за рамками AMF, ибо это не протокол обмена данными между клиентом и сервером, а только формат сериализации данных. И в его рамках каждый отдельный AMF пакет должен быть и является самодостаточным.

Стало быть, каждый отдельный запрос от клиента к серверу (или наоборот) имеет свои reference таблицы. Поэтому одинаковая строка дважды встретиться в одном пакете, то она будет сохранена таким образом. А если в разных пакетах, то нет.

Как работает registerClassAlias

Все мы знаем, что на стороне клиента с помощью функции registerClassAlias можно связать класс на стороне клиента с классом на стороне сервера. Так, что отправив на сервер экземпляр клиентского класса, на сервере волшебным образом получаем экземпляр серверного класса :)

Чуть выше я показывал, как сериализуется типизированый объект. И там видно, как это работает. То, что мы указываем в registerClassAlias -- полное имя серверного класса, передается как строка внутри пакета данных. Так что сервер по этой строке может создать экземпляр соответствующего класса:


Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
instance = clazz.newInstance();

И наоборот, когда сервер отправляет данные на клиент, он тоже записывает в пакете имя серверного класса, а клиент использует это имя как ключ, чтобы найти соответствующий клиентский класс.

Рекомендации

Ну теперь, когда мы кое-что знаем об AMF, можно дать рекомендации по более эффективному его использованию.

Но что, вообще, значит "эффективное использование"? В данном случае мы можем добиваться уменьшения трафика и ускорения сериализации-десериализации.

Что касается ускорения сериализации-десериализации, то на клиенте мы никак не можем повлиять на этот процесс, ибо он скрыт внутри флэш-плеера. На сервере повлиять можем, но только если мы будем писать свой собственный сервер. В большинстве случаем мы все-таки используем готовый сервер, будь то BlazeDS, или FMS или еще что-то.

Так что остается только уменьшение траффика.

Ну и вот рекомендации:

  • там, где можно использовать Boolean, нужно использовать Boolean, а не числа (1,0) и не строки ("1", "0");
  • там, где можно использовать int, нужно использовать int, а не Number. Так же небезынтересно знать, что положительные числа займут меньше места, чем отрицательные;
  • в registerClassAlias использовать короткие имена классов;
  • не забывайте о существовании flash.utils.IExternalizable, с его помощью можно реализовать свою упаковку данных;

О registerClassAlias подробнее:

Если на сервере вы следуете стилю Java и все классы у вас имеют имена вида com.mycompany.project.module.vo.SomeVO, то на клиенте в registerClassAlias вы передаете такую длинную строку: "com.mycompany.project.module.vo.SomeVO". И эта строка присутствует целиком в КАЖДОМ запросе от клиента к серверу и от сервера к клиенту (вот тут бы очень помогли references таблицы на уровне сессии, но их нет, увы).

Отсюда следует, что если вы откажетесь от такого стиля, и назовете классы как-нибудь покороче, типа module.vo.SomeVO, или vo.SomeVO, или просто SomeVO (в дефолтном пакете), то таким образом вы заметно сэкономите трафик.

Впрочем, соблюдение стандартов может считаться более важным, чем экономия трафика :)

Реализации

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

Легко доступны исходные коды Red5 и BlazeDS. К сожалению, в Red5 реализация AMF тесно спутана с реализацией RTMP.

Еще есть ряд реализаций на Java, .NET, PHP, Python, Ruby, Erlang и т.д. Ссылки на них можно найти, к примеру, в википедии.

Comments

Для питона рекомендую реализацию PyAmf
http://www.pyamf.org/index.html

Для просмотра содержимого amf файла в человекочитаемом варианте)):
http://amfview.org

непонятно нужно ли разрывать коннекшн после запроса каждого либо поддерживать его всегда и возобновлять при разрыве.

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
question for bots )
Image CAPTCHA
Enter the characters shown in the image.