Пост будет достаточно далёкий от Flex но зато на модную тему: будем использовать Google Protobuf.
На вопрос: “зачем?” ответ прост – мы хотим иметь возможность быстро подстраивать наше приложение (клиентскую часть сложной системы с бекендом на Java) под изменения протокола и не хотим при этом каждый раз плотно замарачиваться. Поэтому мы замарочимся один единственный раз – разберёмся что к чему, настроим всё, запустим, сгенерируем и научимся использовать.
Основная идея – иметь единый файл с описанием протокола (объектов передаваемых по нему) и генерироть согласно ему код и для бекенда и для фронтенда. Корпорация добра обещает, что если мы всё сделаем правильно то у нас магическим образом объекты будут передаваться по низкоуровнему протоколу (у меня их даже два – TCP и http/WebSockets) будучи обёрнутыми в наш кастомный протокол протобафом.
Для начала можно походить по интернету почитать что к чему, сходить на http://code.google.com/p/protobuf/downloads/list скачать оттуда, посмотреть версию на джаве. Понять что всё просто: создать файл с описанием объёктов которые пересылаем по нашему протоколу и сгенерировать согласно этому описанию код. Добавить его в джава-проект, немного уличной магии и всё работает. Проникнуться и захотеть уметь генерировать код на AS3 так же легко и просто как это делается для джавы. Всё что нам для этого нужно – сходить на http://code.google.com/p/protoc-gen-as3/downloads/detail?name=protoc-gen-as3-1.0.0-rc5-bin.tar.gz&can=2&q= и скачать архив. В нём всё что нам нужно для того чтобы сгенерировать код на AS3 кроме, естественно, файла с описанием объектов. Но раз мы уже генерироли код для джавы он у нас уже есть. Тут я приведу сильно упрощённую версию моего файла trans.proto:
package ru.codeflex.test.protobuf.protocol;
option java_package = "ru.codeflex.test.protobuf.protocol";
option java_outer_classname = "Message";
/**
Frame - is a Protocol's transport packet, which is
used by Server and Client for network communication.
It consists of required Command code - command
and optional payload (multiple fields).
See available Commands description below for complete
information about associated payload messages.
Protobuf binary format does not provide build-in delimiters
for detection of message boundaries. It is responsibility
of target protocol - to delimit messages sent over the wire.
In our protocol Frames are of variable size, so for determination
of frame boundaries every Frame will have its length prepended
as first uint32 value. So complete data structure will be:
[uint32][Frame][uint32][Frame]...
uint32 - is a base 128 Varint. Variable lenth integer.
To learn more about Varint types visit official page
http://code.google.com/apis/protocolbuffers/docs/encoding.html#varints
*/
message Frame {
required Command command = 1;
// payload
optional Join join_message = 2;
optional JoinAcknowledge join_acknowledge_message = 3;
optional Error error_message = 7;
}
enum Command {
JOIN = 5; // Send by the client who wants to join an existing (valid session_id) session.
// If given session_id does not exist this message will be dropped silently.
// The first joining client will become the host.
// Payload: -> Join
JOIN_ACK = 3; // Send by the server to joining client in case of success.
// If new client is being connected to an existing session
// Payload: <- JoinAcknowledge
ERROR = 99; // Send by the server in case of errors detected on server.
// May be sent in response to any Client's command.
// When triggered by the clients the message is dropped silently.
// Payload: <- Error
}
// Client's role
enum Role {
PLAYER = 92;
// may be extended with additional roles in future
}
/**
In order to successfully join the session, client must provide valid data:
session_id - id of existing (game) session provided by gface|engine
gface_token - valid token, given by gface|engine after successful authentication.
Server will do additional check if provided token is enlisted as participant for given session_id
and session has free slots. Otherwise it will reject Join request from client.
*/
message Join {
optional Role role = 1 [default = PLAYER];
required uint64 session_id = 2;
required string gface_token = 3;
}
/**
Send by the server after a player has joined.
isHost is "true" if this player is a host, "false" otherwise.
The slot is the player slot which is a zero based index
and lower than the max number of players (0 <= slot < maxNoPlayers).
*/
message JoinAcknowledge {
required uint32 slot = 1;
optional bool is_host = 2 [default = false];
}
/**
Error detected on server.
code - see error codes list for possible error codes.
msg - arbitrary message, describing occurred error.
*/
message Error {
optional uint32 code = 1;
optional string msg = 2;
}
uint32, uint64 – конвенции, при генерации по этому описанию кода в Джава uint64 например даёт Long, при генерации кода в AS3 почему-то мапится не на Number а на написанный разработчиками protoc-gen-as3 класс Uint64. Много странного вокруг нас ![]()
Для того чтобы получить по этому описанию код на Java нужно всего лишь запустить экзешник (для виндовой версии) или .sh файл скормив ему путь на наш trans.proto. Для того чтобы получить код на AS3 нужно запустить его же но с указанием дополнительных параметров плагина для генирации –plugin=protoc-gen-as3=protoc-gen-as3.bat и выходной папки для этого плагина –as3_out=as3. Я сделала generate_as3code.bat файл для того чтобы упростить всё это дело. К этому посту приаттачен архив со всем необходимым (generate_Javacode.bat для java) для генерации. Сорцы протобафа на Джаве можно взять со ссылки на гуглкод что я привела выше или позволить Мавену сделать это за вас, сорцы com.netease.protobuf.* я так же положила в архив к этому посту.
Итак, будем считать что у нас всё есть – как же нам добавить этот код в наш проект на AS3 и использовать его? Пример: имеем флеш-клиент общающийся с сервером по TCP. Есть некий кастомный сокет ProtoBufSocket extends Socket в конструкторе которого добавлен листенер addEventListener(ProgressEvent.SOCKET_DATA, onData); onData – функция в которой мы читаем из сокета, разбираем прочитанное и как-то реагируем – выглядит так:
private function onData(_event:ProgressEvent):void
{
var data:ByteArray = new ByteArray();
if(this.bytesAvailable > 0)readBytes(data);
var mess:Frame = new Frame();
mess.mergeDelimitedFrom(data);//length included!
if (mess.command == Command.GF_JOIN_ACK)//joined ok, accept
{
//process somehow mess.joinAcknowledgeMessage
return;
}
else if (mess.command == Command.GF_ERROR)
{
//process somehow mess.errorMessage.msg
return;
}
else{
//throw new Error("Undefined protocol command:" + command);
return;
}
}
В этой функции мы преобразовываем массив байтов в протобафовую “единицу” Frame. Для того чтобы записать в сокет нам нужно произвести обратную операцию – разобрать Frame на байты и послать по сокету. Это делаем в функции writeFrameToSocket:
public function writeFrameToSocket(frame:Frame):void{
if(!this.connected) return;
var buf:WritingBuffer = new WritingBuffer();
frame.writeToBuffer(buf);
var t:ByteArray = new ByteArray();
buf.toNormal(t)
flushLoggedWW(t);
}
private function flushLoggedWW(_buf:ByteArray):void{
var logBuff:WritingBuffer = new WritingBuffer();
WriteUtils.write$TYPE_UINT32(logBuff, _buf.length);
logBuff.writeBytes(_buf);
logBuff.position=0;
writeBytes(logBuff);
flush();
}
Пример того как можно сформировать и послать по сокету сообщение в инстанце-родителе:
private var serverConnection:ProtoBufSocket;
...
serverConnection = new ProtoBufSocket(_serverAddress, _serverPort, this);
...
var mess:Frame = new Frame();
mess.command = Command.GF_JOIN;
mess.joinMessage = new Join();
mess.joinMessage.sessionId = UInt64.parseUInt64(gameSessionId.toString());
mess.joinMessage.gfaceToken = "here-some-token";
serverConnection.writeFrameToSocket(mess);
На примере приведённого тут очень прстого протокола сложно оценить все преимущества использования протофа но поверьте, в условиях сложного протокола в большом и сложном проекте протобаф оправдан целиком и полностью.
P.S. Огромное спасибо сеньёру Алексею Макаренко за то что правильно пнул меня в направлении Google Protobuf.
Обещанный архив можно скачать отсюда.












