Бинарные сокеты в AS3. Часть 2
Часть 1. Часть 3.
В общем, то, о чем я хочу поведать — это бинарный сокет, обменивающийся сообщениями по протоколу TCP. Сообщения представляют собой четко сформированные по каким-то определенным протокольным сигнатурам байтовые массивы. Протоколы для сообщений разрабатываются разработчиками (простите за каламбур) по собственному усмотрению. Но, безусловно, общая основа есть у каждого типа сообщения: это длина + сообщение. Я предпочитаю сигнатуру у сообщений такого вида:
Код:
Length (int) + type (byte) + data (byte[])
Так вот, теперь поговорим о байтах. Вернее о типах и их занимаемость в байтах. Самый простенький тип — это byte. В AS3 такого типа нет, там используют int в диапазоне [-128, 127] или беззнаковый byte — аналог uint в диапазоне [0, 255]. Не составляет труда представить эти числа в шестнадцатеричной системе: [0x00, 0xFF].
Далее идет short. Он занимает 2 байта и вмещает себя максимальное число в десятичной — 35535. Думаю, смысл беззнаковых-знаковых понятен, диапазон для short тоже легко просматривается, если провести аналогию с byte.
Есть float. Он 4 байта, так как точность у него одинарная. И есть int. Он тоже 4 байта. Точные максимальные значения: 3.4 * 10^38 и 2 147 483 647 у float и int соответственно.
Следует отметить (спасибо ramshteks), что в AS3 нет longInt (8 байт) вообще. Его нельзя поместить даже в Number, который, хоть и заявлен как 64-битный. И нельзя записать в ByteArray. Это ограничение можно обойти, сделав из этого числа double, который все равно чуть-чуть да может изменить число из-за своей "природы". ramshteks предложил весьма интересный способ, основанный на записи лонга как 2 интовых числа и сравнения по частям (подробности в комментариях).
Итак, зачем же это все нам надо. Мы можем в сообщение записать последовательно кучу int чисел через сепаратор, допустим, “|”. А можем вшить кучу int чисел, а потом эту кучу int чисел в цикле или еще как считать последовательно. И никакие сепараторы нам не нужны. В таком случае сообщение будет выглядеть примерно так: 0x04FB0000 0x00500006 0x000000A0 … (пробелы просто для наглядности). И мы по 4 байта каждый раз можем считывать из буфера эти числа. В общем, пока, наверное, не так все понятно и радужно. Но. Это экономит трафик при частой пересылке по сокету. Да и просто удобнее. Точно-точно.
А теперь вернемся к сигнатуре сообщения, что я дал выше. Сначала мы вшиваем int, затем byte, затем массив байтов (ByteArray) самого сообщения с какой-то информацией. Зачем нам нужна длина и тип? Тип — для того, чтобы разделить сообщения на…хм… пусть будет категории. SystemPacket, MessagePacket, GamePacket и так далее. Этот тип – просто число в диапазоне [0x00, 0x79]. Можно использовать и неучтенную мною часть [-0x80, -0x01], но ну их нафиг. Числа некрасивые. А не будет хватать — переделать на беззнаковый байт можно легко. Разве что изменения должны коснуться сервера и клиента. Учтите это при разработке протоколов или даче совета недальновидному сервернику. А пока, 128 различных типов. Думаю, больше, чем достаточно. Если Вам мало — используем беззнаковый тип. Если и этого мало, увеличиваем его до short.
Длина нужна для того, чтобы правильно считать сообщение из общего потока байтов. Смысл таков: мы сначала считываем длину, затем тип. Затем смотрим, сколько в сообщении была длина, и считываем уже столько байтов, сколько указана в длине. Считали — все, сообщение полностью готово, можно отдавать на разбор-чтение куда-то там наверх по логике приложения. Такая штука. Сложно? Думаю, пока что да, если Вы встречаетесь с этим впервые.
Теперь самое главное, из-за чего затевалась статья: предлагаю свой самописный велосипедный алгоритм корректного считывания байтов из потока и формирования сообщения. Подглядел я его в java сетевом фреймворке netty, очень понравился. Настолько понравился, что даже удалил 100 строк своего парсера данных и заменил на более простенькое и симпатичненькое (а после статьи bav в комментариях предлагает свой алгоритм, в разы проще и немного расстраивает меня ). Потом уже, проанализировав некоторые ситуации, понял, что старый алгоритм много чего не учитывал. Простыню класса разобью на маленькие абзацы и прокомментирую каждый более-менее сложный момент. Итак, погнали.
package client.net { import client.events.ServerEvent; import client.net.packets.Packet; import client.net.packets.PacketByteArray; import flash.errors.EOFError; import flash.errors.IOError; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.net.Socket; import flash.utils.ByteArray; /** * @author KorDum */ [Event(name="getPacket", type="client.events.ServerEvent")] public class ClientSocket extends EventDispatcher { private const LENGTH:uint = 0; private const TYPE:uint = 1; private const DATA:uint = 2; private var _socket:Socket; private var _host:String; private var _port:uint; private var _stateDecode:uint; private var _type:uint; private var _length:uint; private var _data:PacketByteArray; private var _streamBuffer:PacketByteArray; //--------------------------------------------------------------------------- // // CONSTRUCTOR // //--------------------------------------------------------------------------- public function ClientSocket() { _socket = new Socket(); _data = new PacketByteArray(); _streamBuffer = new PacketByteArray(); }
//--------------------------------------------------------------------------- // // PUBLIC METHODS // //--------------------------------------------------------------------------- public function connect(host:String, port:uint):void { _host = host; _port = port; _socket.connect(host, port); _socket.addEventListener(Event.CLOSE, dispatchEvent); _socket.addEventListener(Event.CONNECT, dispatchEvent); _socket.addEventListener(IOErrorEvent.IO_ERROR, dispatchEvent); _socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, dispatchEvent); _socket.addEventListener(ProgressEvent.SOCKET_DATA, socket_socketDataHandler); } public function disconnect():void { if (_socket.connected) { _socket.close(); } _socket.removeEventListener(Event.CLOSE, dispatchEvent); _socket.removeEventListener(Event.CONNECT, dispatchEvent); _socket.removeEventListener(IOErrorEvent.IO_ERROR, dispatchEvent); _socket.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, dispatchEvent); _socket.removeEventListener(ProgressEvent.SOCKET_DATA, socket_socketDataHandler); }
//--------------------------------------------------------------------------- // // PRIVATE HANDLERS // //--------------------------------------------------------------------------- private function socket_socketDataHandler(event:ProgressEvent):void { // offset – для смещения «каретки» записывания для склейки нескольких байтовых // массивов в один большой буфер var offset:uint = _streamBuffer.length; // считываем из сокета данные в массив, начиная со смещения _socket.readBytes(_streamBuffer, offset, _socket.bytesAvailable); try { // отправляем буфер в декодирование decode(_streamBuffer); } catch (error:EOFError) { } catch (error:RangeError) { } } //--------------------------------------------------------------------------- // // PRIVATE METHODS // //--------------------------------------------------------------------------- private function decode(buffer:PacketByteArray):void { if (_stateDecode == LENGTH) { _length = buffer.readLength(); _stateDecode = TYPE; } else if (_stateDecode == TYPE) { _type = buffer.readType(); _stateDecode = DATA; } else if (_stateDecode == DATA) { _data = new PacketByteArray(); _data.writeBytes(buffer, buffer.position, _length); _data.position = 0; buffer.position += _length; _stateDecode = LENGTH; dispatchEvent(new ServerEvent(ServerEvent.GET_PACKET)); if (buffer.bytesAvailable === 0) { buffer.clear(); throw new EOFError(); } } decode(buffer); }
Эти данные приходят, дописываются в наш буфер и отправляются снова на декодирование. Итак, мы считали длину успешно, рекурсивно отправляем массив с байтами на декодирование, считываем тип пакета. Если возникает исключение, то вновь дожидаемся, пока не придут еще данные. То есть точно так же, как с длиной пакета.
Считали тип, начинаем считывать уже само сообщение в пакете (которое data). Ситуация с исключением повторяется. Если выбрасывается исключение, то то, что ниже строчки с чтением байтов из буфера в массив с данными
, просто не выполнится. А если все окей, то перемещаем каретки чтения в нужные позиции, говорим, что пакет пришел полностью, его можно забрать. После проверяем, есть ли еще данные в сокете. И если нет, то очищаем наш буфер и выходим из декодера. Все просто сейчас, но, когда я писал, много чего упустил. Пришлось отлаживать и, кажется, все успешно. То есть никакие ошибки в декодере мне найти не удалось. Тестировал на разных ситуациях.
С декодером все. Оставшиеся методы класса:
public function writePacket(packet:Packet):void { try { var data:ByteArray = packet.getByteArray(); _socket.writeBytes(data); _socket.flush(); } catch(error:IOError) { } } //--------------------------------------------------------------------------- // // PUBLIC ACCESSORS // //--------------------------------------------------------------------------- public function get length():uint { return _length; } public function get type():uint { return _type; } public function get data():PacketByteArray { return _data; }
Часть 1. Часть 3.
Всего комментариев 16
Комментарии
14.07.2012 00:40 | |
вы немного ошиблись:
Цитата:
там используют int в диапазоне [-256, 255]
|
14.07.2012 00:46 | |
Ох! И правда ошибся. Да-да-да. Большое Вам спасибо, правлю пост и делаю ссылку на Ваш комментарий.
|
14.07.2012 16:56 | |
А longInt развен не эквивалентен int? Есть long long (кажется так), он уже 8 байт. Вы про который говорили?
|
14.07.2012 20:57 | |
Правильно. Принимаю поправку.
|
15.07.2012 19:35 | |
Цитата:
нужно было передать unix-время
Учту и допишу в статью =) |
15.07.2012 20:43 | |
время на клиенте бывает важно для определенных небольших логических действий вроде этот человек пришел в игру раньше меня, поэтому в лог я его не добавляю.
|
15.07.2012 21:01 | |
Можно извратиться с лонгом, сделав из него double, тупо разделив на 1 000 000 000 или 1 000 000 000 000 000 000
|
15.07.2012 23:49 | |
Еще вопрос. А почему нельзя было сделать так?
Ну и естественно Packet implements ISerializable. |
16.07.2012 10:53 | |
Очень извиняюсь, написал не то название интерфейса, правильное IExternalizable
Я тоже на самом деле не так уж много знаю, поэтому пытаюсь разобраться здесь вместе с вами Судя по документации ByteArray реализует IDataInput, IDataOutput, что дает мне основание предполагать, что параметр в функциях и и есть наш поток. А значит на сервер должно уходить то же самое (хотя это надо проверить, я не уверен). Тут как раз очень удобно должно получиться применять наследование, например package client.packets.core { import flash.utils.IDataInput; import flash.utils.IDataOutput; import flash.utils.IExternalizable; /** * @author KorDum */ public class Packet implements IExternalizable { static public const SOME_PACKET:uint = 0x01; private var _length:uint; // int private var _type:uint; // byte //--------------------------------------------------------------------------- // // CONSTRUCTOR // //--------------------------------------------------------------------------- public function Packet() { } //--------------------------------------------------------------------------- // // PUBLIC METHODS // //--------------------------------------------------------------------------- public function writeExternal(output:IDataOutput) { output.writeInt(length); output.writeByte(type); } public function readExternal(input:IDataInput) { length = input.readInt(); type = input.readByte(); } //--------------------------------------------------------------------------- // // PUBLIC ACCESSORS // //--------------------------------------------------------------------------- public function get type():uint { return _type; } public function set type(value:uint):void { _type = value; } public function get length():uint { return _length; } public function set length(value:uint):void { _length = value; } } } Вот такая вот мысль пришла в голову |
16.07.2012 13:48 | |
Обновил 1 и 2 часть. В 1 чуть-чуть совсем дописано про XMLSocket. Здесь уже учел все собранные советы.
Большое всем спасибо. |
Последние записи от КорДум
- Basic authentication и GET/POST запросы (20.03.2013)
- SOAP и Flash (19.12.2012)
- Бинарные сокеты в AS3. Часть 3 (13.07.2012)
- Бинарные сокеты в AS3. Часть 2 (13.07.2012)
- Бинарные сокеты в AS3. Часть 1 (13.07.2012)