Форум Flasher.ru
Ближайшие курсы в Школе RealTime
Список интенсивных курсов: [см.]  
  
Специальные предложения: [см.]  
  
 
Регистрация Блоги Правила Справка Пользователи Календарь Поиск рулит! Сообщения за день Все разделы прочитаны
 

Вернуться   Форум Flasher.ru > Блоги > КорДум

Оценить эту запись

Бинарные сокеты в AS3. Часть 2

Запись от КорДум размещена 13.07.2012 в 23:19
Обновил(-а) КорДум 16.07.2012 в 13:48

Часть 1. Часть 3.

В общем, то, о чем я хочу поведать — это бинарный сокет, обменивающийся сообщениями по протоколу TCP. Сообщения представляют собой четко сформированные по каким-то определенным протокольным сигнатурам байтовые массивы. Протоколы для сообщений разрабатываются разработчиками (простите за каламбур) по собственному усмотрению. Но, безусловно, общая основа есть у каждого типа сообщения: это длина + сообщение. Я предпочитаю сигнатуру у сообщений такого вида:
Код:
Length (int) + type (byte) + data (byte[])
Длину можно и short’ом записывать, конечно же. Да и тип поместить в data, а потом считывать. Но это зависит от того, кто разработал протокол и все такое.

Так вот, теперь поговорим о байтах. Вернее о типах и их занимаемость в байтах. Самый простенький тип — это 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 в комментариях предлагает свой алгоритм, в разы проще и немного расстраивает меня ). Потом уже, проанализировав некоторые ситуации, понял, что старый алгоритм много чего не учитывал. Простыню класса разобью на маленькие абзацы и прокомментирую каждый более-менее сложный момент. Итак, погнали.
Код AS3:
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();
		}
Пока ничего нового и все понятно. Разве что начинает фигурировать некий PacketByteArray. Это всего лишь расширенный ByteArray с некоторыми новыми методами (о нем ниже). Справку по самим методам ByteArray я цитировать не буду, но скажу, что там просто дофига сколько недописанного (например, не везде представлены генерируемые экзепшены) и вообще как-то так написано криво. Возможно, это только в русской справке так.
Код AS3:
//---------------------------------------------------------------------------
//
// 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);
}
Тоже ничего трудного, обрабатываем ошибки, посылаем их наверх, подписываемся на события и все такое.
Код AS3:
 //---------------------------------------------------------------------------
//
// 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);
}
Такая вот хреновина. Теперь подробнее об этом алгоритме. Я все сообщения буду называть уже пакетами, не запутайтесь! Все методы чтения байтов из байт арреев генерируют два исключения: EOFError и RangeError. Первый, если мы хотим считать больше, чем нам дали, а второй… да то же самое, по сути. Чем мы и пользуемся: оборачиваем метод декодирования в try-catch (связку можно заменить на один Error, но я это сделал для наглядности) и начинаем разбираться с данными. Первым делом мы считываем длину пакета. Если по каким-то причинам (например, мы считали первый пакет, а второй пакет влез в первую пачку байтов только, допустим, одним байтом. А мы же читаем 4!) этот метод не может прочесть 4 байта, то генерируется исключение, которое мы успешно ловим, обрабатываем и ДОЖИДАЕМСЯ, пока не придут еще данные.

Эти данные приходят, дописываются в наш буфер и отправляются снова на декодирование. Итак, мы считали длину успешно, рекурсивно отправляем массив с байтами на декодирование, считываем тип пакета. Если возникает исключение, то вновь дожидаемся, пока не придут еще данные. То есть точно так же, как с длиной пакета.

Считали тип, начинаем считывать уже само сообщение в пакете (которое data). Ситуация с исключением повторяется. Если выбрасывается исключение, то то, что ниже строчки с чтением байтов из буфера в массив с данными
Код AS3:
_data.writeBytes(buffer, buffer.position, _length);
, просто не выполнится. А если все окей, то перемещаем каретки чтения в нужные позиции, говорим, что пакет пришел полностью, его можно забрать. После проверяем, есть ли еще данные в сокете. И если нет, то очищаем наш буфер и выходим из декодера. Все просто сейчас, но, когда я писал, много чего упустил. Пришлось отлаживать и, кажется, все успешно. То есть никакие ошибки в декодере мне найти не удалось. Тестировал на разных ситуациях.

С декодером все. Оставшиеся методы класса:
Код AS3:
 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; }
writePacket нужен для записи сформированного пакета в сокет для передачи его на сервер. О пакетах чуть дальше и не так полно. И да, я же совсем забыл про PacketByteArray!

Часть 1. Часть 3.
Размещено в net
Комментарии 16 Отправить другу ссылку на эту запись
Всего комментариев 16

Комментарии

Старый 14.07.2012 00:40 ramshteks вне форума
ramshteks
 
Аватар для ramshteks
вы немного ошиблись:
Цитата:
там используют int в диапазоне [-256, 255]
от -128 до 127
Старый 14.07.2012 00:44 ramshteks вне форума
ramshteks
 
Аватар для ramshteks
ну о типах данных так же стоит упомянуть одну хитрую особенность флеша. Он не умеет читать long int, в байт арее нет метода, а если поизучать типы данных, то выясниться, что лонг даже и записать некуда. Это важная информация, так как это стоит учитывать при проектировании протокола
Старый 14.07.2012 00:46 КорДум вне форума
КорДум
 
Аватар для КорДум
Ох! И правда ошибся. Да-да-да. Большое Вам спасибо, правлю пост и делаю ссылку на Ваш комментарий.
Старый 14.07.2012 16:56 КорДум вне форума
КорДум
 
Аватар для КорДум
А longInt развен не эквивалентен int? Есть long long (кажется так), он уже 8 байт. Вы про который говорили?
Старый 14.07.2012 20:33 Furinax вне форума
Furinax
Чем отличается
Код AS3:
var offset:uint = (_streamBuffer.length > 0) ? _streamBuffer.length : 0;
от
Код AS3:
var offset:uint = _streamBuffer.length;
?
Просто интересуюсь потому, что не знаю может ли длина массива байтов быть меньше нуля?
Если не может, то значит она равна либо нулю либо больше нуля, тогда не пойму в чем разница.
Старый 14.07.2012 20:57 КорДум вне форума
КорДум
 
Аватар для КорДум
Правильно. Принимаю поправку.
Старый 15.07.2012 16:08 ramshteks вне форума
ramshteks
 
Аватар для ramshteks
я говорю о 64-битном лонге. инт во флеше 32-битный, а Number хоть якобы и является 64-битным не сможет принять в себя лонг. Так как хранить негде, байтаррей и читать не умеет.

На самом деле лонг, может пригодится в очень редких ситуациях и такое ограничение может быть не очень то и сложно обойдено. я столкнулся с этим когда мне нужно было передать unix-время. Которое в джаве получается именно как лонг. Так же у меня не получилось его восстановить путем записи двух интов, вычитывание его и соединением с помощью битовых сдвигов. Флеш эту операцию будто бы игнорировал, не производя изменений. Точно уже не помню, что было, но я так и не смог найти решения в интернете. Да и встречал инфу о том, что это собственно и невозможно
Старый 15.07.2012 19:35 КорДум вне форума
КорДум
 
Аватар для КорДум
Цитата:
нужно было передать unix-время
Мне и в голову не пришло бы парсить время на клиенте, я на сервере метод написал.
Учту и допишу в статью =)
Старый 15.07.2012 20:43 ramshteks вне форума
ramshteks
 
Аватар для ramshteks
время на клиенте бывает важно для определенных небольших логических действий вроде этот человек пришел в игру раньше меня, поэтому в лог я его не добавляю.
Старый 15.07.2012 21:01 КорДум вне форума
КорДум
 
Аватар для КорДум
Можно извратиться с лонгом, сделав из него double, тупо разделив на 1 000 000 000 или 1 000 000 000 000 000 000
Старый 15.07.2012 21:13 ramshteks вне форума
ramshteks
 
Аватар для ramshteks
да такой вариант тоже был. Теоретически точность больше секунд не интересна, поэтому от этого можно плясать. С даблом могут возникнуть коллизии из-за природы самого дабла, хоть там и двойная точность, но артефакты не исключены. В любом случае это решение не из коробки.

Я решил это посылкой лонга, а при чтении он вычитывается(как инт) как первые и вторые 32 бита лонг числа. Второе число имеет больший приоритет перед вторым, и если они равны то сравниваются вторые части.

Это ограничение в итоге обходится, просто об этом стоит упомянуть в статье, если уж затрагиваются типы данных
Старый 15.07.2012 23:49 Furinax вне форума
Furinax
Еще вопрос. А почему нельзя было сделать так?
Код AS3:
public function writePacket(packet:Packet):void {
	try {
		var data:ByteArray = new ByteArray();
		data.writeObject(packet);
		_socket.writeBytes(data);
		_socket.flush();
	}
	catch(error:IOError) {
		Debugger.append(error.getStackTrace());
	}
}
Ну и естественно Packet implements ISerializable.
Старый 16.07.2012 00:29 КорДум вне форума
КорДум
 
Аватар для КорДум
Я ничего про сериализацию в AMF0/3 не знаю. И тем более, я без понятия, в каком виде пакет запишется и как его парсить на сервере. А протокол у меня уже утверждем.
Если же ISerializable содержит в себе некий метод, который вызывается автоматически (такого интерфейса в доке мне найти не удалось, я полагаю, он кастомный, не из ФП), то я, во-первых, не вижу очевидным этот вызов и, во-вторых, не вижу смысла, ибо это то же самое будет, как если вызвать напрямую метод сериализации, что я и делаю в примере в статье.
Очевидно, Вы знаете про это больше, чем я. Прошу поделиться.
Старый 16.07.2012 10:53 Furinax вне форума
Furinax
Очень извиняюсь, написал не то название интерфейса, правильное IExternalizable
Я тоже на самом деле не так уж много знаю, поэтому пытаюсь разобраться здесь вместе с вами
Судя по документации ByteArray реализует IDataInput, IDataOutput, что дает мне основание предполагать, что параметр в функциях
Код AS3:
public function writeExternal(output:IDataOutput)
и
Код AS3:
public function readExternal(input:IDataInput)
и есть наш поток. А значит на сервер должно уходить то же самое (хотя это надо проверить, я не уверен).
Тут как раз очень удобно должно получиться применять наследование, например
Код AS3:
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;
		}
	}
}
А SomePacket как и задумывалось расширял бы Packet и в нем уже овверрайдились бы методы writeExternal и readExternal с вызовом super.writeExternal и super.readExternal соответственно.
Вот такая вот мысль пришла в голову
Старый 16.07.2012 13:03 КорДум вне форума
КорДум
 
Аватар для КорДум
Сразу же хочу сказать, что это весьма сомнительная плюшка. И начну расписывать, почему:
1. readExternal никогда использоваться не будет, так как сразу в один присест пакет, пришедший по сокету, считать невозможно (если только он не влезает в первую пачку и влезает полностью). Как правило почти все пакеты в реальном приложении раздроблены и склеены.
2. я уже создал наследуемый метод в Packet, который соберет сырые байты в правильном порядке. Этот метод переопределять в наследниках не надо. Метод отдает понятный ByteArray, который можно при должном терпении, оттрейсить (хотя дебаггер FD, почему-то, не показывает байтовое содержимое массива, как я ни пытался его найти и отобразить).

В этом плане я полностью согласен с bav и его упоминанием бритвы Окамы.

И сейчас SomePacket выглядит примерно вот так:
Код AS3:
package client.net.packets {
 
	/**
	 * @author KorDum
	 */
 
	public class SystemPacket extends Packet {
		public static const ENTER_CHAT_SUCCESS:uint = 0x0000;
		public static const DO_NAME_RENAME:uint = 0x0001;
		...
 
		private var _command:uint;
		private var _message:String;
 
		//---------------------------------------------------------------------------
		//
		// CONSTRUCTOR
		//
		//---------------------------------------------------------------------------
 
		public function SystemPacket() {
			type = Packet.SYSTEM;
		}
 
 
		//---------------------------------------------------------------------------
		//
		// PUBLIC STATIC METHODS
		//
		//---------------------------------------------------------------------------
 
		public static function createByData(length:uint, data:PacketByteArray):SystemPacket {
			var packet:SystemPacket = new SystemPacket();
 
			packet.data = data;
			packet.length = length;
			packet.command = data.readCommand();
			packet.message = data.readMessage();
 
			return packet;
		}
 
 
		public static function createByCommand(command:uint):SystemPacket {
			var packet:SystemPacket = new SystemPacket();
 
			var buffer:PacketByteArray = new PacketByteArray();
			buffer.writeCommand(command);
 
			packet.data = buffer;
			packet.length = buffer.length;
			packet.command = command;
 
			return packet;
		}
 
 
		public static function createByCommandMessage(command:uint, message:String):SystemPacket {
			var packet:SystemPacket = new SystemPacket();
 
			var buffer:PacketByteArray = new PacketByteArray();
			buffer.writeCommand(command);
			buffer.writeUTFBytes(message);
 
			packet.data = buffer;
			packet.length = buffer.length;
			packet.command = command;
			packet.message = message;
 
			return packet;
		}
 
 
		//---------------------------------------------------------------------------
		//
		// PUBLIC ACCESSORS
		//
		//---------------------------------------------------------------------------
 
		...
	}
}
А PacketByteArray я где-то в комментариях уже кидал. К третьей части комментарии гляньте.
Старый 16.07.2012 13:48 КорДум вне форума
КорДум
 
Аватар для КорДум
Обновил 1 и 2 часть. В 1 чуть-чуть совсем дописано про XMLSocket. Здесь уже учел все собранные советы.
Большое всем спасибо.
 

 


Часовой пояс GMT +4, время: 08:22.


Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2019, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.