Пацаны, гоу Вконтакте?
Сегодня речь пойдёт о ВконтактеАПИ.
Сперва хотелось бы сказать, что программирую я не первый день. И вот за всю свою карьеру мне до сих пор непонятно - КАК можно было сделать так, что возвращалась бы "Неизвестная ошибка"? Как-то ведь можно было понять что произошла ошибка - ну и выплюнь ты результат, может по нему причину ликвидировать можно было бы. А н-нет. Будем плеваться Unknown Error без объяснений - не барское это дело.
На самом деле лабуды у контакта много, вроде всяких там секретов (хотя в 3 версии серверного АПИ должны убрать) и сигнатур - наверное чтобы хоть как-то защищать переменные всякие, что у них на серверах хранятся. Хотя я не представляю для них другой задачи, кроме как хранения запоминалки настроек - типа звук "вкл-выкл", если настройки не найдены в SharedObject. Так что на кой там защита - тоже для меня загадка.
Но мы не такие. Мы будем делать всё правильно, по пацански. Чтобы конкуренты нам завидовали.
Я вам расскажу сегодня как нужно делать взаимодействие клиент-контакт. Более менее сознательно с контактом я работаю после выхода 3 версии их клиентского API, поэтому если что-то нереализуемо со старыми версиями - уж простите. Постараюсь идти по пунктам.
1) Создали VkontakteConnector? Выбросьте на помойку, причем сразу. О чем это я? Дело в том, что никогда нельзя верить заказчикам. Сегодня одно, завтра другое. Поэтому я придерживаюсь MVC насколько могу, а что касается этого момента... Нужно создавать SocialConnector. Уверяю вас, в один прекрасный день начальник скажет - "а теперь в фейсбук!" и ваша седина сменится лысиной.
Как это делать? У SocialConnector мы создаём поле instance с типом ISocial. ISocial - интерфейс, в котором перечислены все-все методы, которые вы используете. Например, getProfiles(...), getUserApps() и всякие такие. VkontakteConnector реализует ISocial и сам отправляет нужные запросы. В итоге, чтобы узнать информацию о юзере нужно будет сделать вот так:
Но лично мне instance не нравится. Поэтому мой SocialConnector тоже реализует ISocial, где вся реализация методов заключается в
Вообще реализация выглядит как то так:
private var _instance:ISocial; ... public function SocialConnector(social:String, flashVars:Object){ super(); switch (social){ case SocialConnector.VKONTAKTE: _instance = new VkontakteConnector(flashVars); break; case SocialConnector.FACEBOOK: _instance=new FacebookConnector(flashVars); break; default: throw new ArgumentError("Unknown social"); } }
О доставке от SocialConnector желающим думаете сами - я просто делаю dispatchEvent, а подписчики уже парсят сообщение.
Далее речь пойдёт сугубо о контакте, однако к другим соцсетям оное тоже применимо.
2) Кэш, кэш, кэш!
Любая картинка извне, что я загружаю - грузится у меня отдельным классом, который по загрузке считывает bytes, а при обращении в следующий раз к этому адресу - эти bytes скармливает Loader#loadBytes. То есть я не гружу загруженные картинки 2 раза.
С контактом всё куда сложнее. Например, getProfiles. Есть много разных полей у этого getProfiles - имя, фамилия, аватарка, день рождения. И всё это надо кешировать. Да-да, я знаю что этим мало кто занимается. Но надо, очень надо.
Реализация: я бы создал класс VkontakteUser, в котором перечислил вообще все возможные поля от getProfiles. Метод getProfiles VkontakteConnector`а принимает массив uid`ов пользователей и fields - поля, что нужно от юзеров знать. Перед отпрвкой мега-супер-кеш-класс бежит по всем uid`ам и смотрит, есть ли такие в кеше, а если и есть - то все ли поля загружены. Если не всё загружено - он грузит это дело с контакта, запоминает в кеше и выплёвывает из кеша. Почему я рекомендую выплёвывать уже из кеша после загрузки: например, у пользователя известно имя, но не известна фамилия. Спросили только фамилию... и теперь нужно склеивать имя с фамилией и выплёвывать клиенту что-то такое, что он ожидает. Когда таких параметров много - это тяжело. Другое дело, сохранив это в кеш и взяв это дело "начистую оттуда". По мне это гораздо проще.
Однако, говоря "взяв из кеша" я имею ввиду что всё это дело происходит прозрачно для клиента. Клиент не подозревает о существовании кеша как такового - если его убрать, то клиент не должен что-нибудь почувствовать. Под клиентом я, конечно, подразумеваю флешку.
По поводу getProfiles и аватарок: можно, конечно, выплевывать сразу bytes для аватарки, но я так не делаю. Я выплевываю адрес, а кэш картинок смотрит, есть ли по такому адресу уже загруженная картинка. Вконтакте кэш и картинко-кэш ничего друг о друге не знают. Это кэши разного уровня.
Со всякими getBalance чтобы узнать у юзера баланс тоже нужно делать кэш - а как же! Однако вот незадача: он может изменится, скажете вы. Да, но изменившись вы получите сообщение, по типу сообщений onWindowFocus, onWindowBlur и подобных. Вот тогда кэш можно сбрасывать.
Пользуясь любыми командами - кешируйте их донельзя. А почему?
3) Да потому что можно сделать всего лишь 3 запроса к вконтакту. Вы создаёте очередь запросов, чтобы не отправлять их слишком много? А то отправятся 4 запроса - и пиши пропало. Ошибка будет, что слишком много запросов. Поэтому каждый запрос нужно отправлять скрипя зубами - мало того, что это юзера беспокоит. Ему ведь надо ждать, деньги за трафик отдавать, да ещё и медленнее это дело будет чем из кэша брать. А там мало ли что-то не то, да всё полетит... ой-йо... Но чтобы прокешировать что-то, сперва нужно это получить. Так что без очереди запросов тут не обойтись.
Однако, проблема есть. Отправляя запросы каждые 334 мс мы рассчитываем на идеальную связь вконтакт-клиент. А её не будет. Какой нибудь запрос возьмет и задержится. А задержавшийся ждать не будет - и уйдет пачка запросов вконтакт сразу, а мы так не играем.
Реализация: будет подстраиваться под реалии сурового мира. Отправляя запрос мы должны дождаться его ответа. Дождавшись смотрим время "полёта" запроса. Если оно больше или равно 334 - значит, запрос прошел. Если меньше - выжидаем это время перед тем как заявить, что "касса свободна". Конкретней реализацию можно представить так: запрос кладётся в ассоциативный массив (в хэш (в Object/Dictionary, ну)), где он является ключом. А значение у этого запроса - текущий getTimer. По возвращению или взводим setTimeout/Timer/да что угодно, или уменьшаем на единичку число "ушедших запросов". Можно запрос в виде класса сделать, с геттером free : Boolean... Да как угодно. Как вам удобней. Кстати, не советую выставлять между запросами время в 334 мс. Погрешность таймера всё таки может сыграть своё место. Поэтому я бы поставил 400.
Однако, мы говорили "положить текущий запрос". Очевидно, что текущий запрос положить сложно, нужно положить какой-то его идентификатор. Идентификатор "подпишет" запрос и по возвращению запроса мы всё таки сможем понять, что же он значил.
4) Только скорее эта подпись является не продолжением пункта 3, а совершенно отдельным. Например, вам нужно узнать у кого установлено приложение и своё имя-фамилию. Это 2 разных запроса. Можно, конечно, ждать ответа от первого, потом отправлять второй... Но это будет куда дольше, чем отправить 2 параллельно.
А при пришествии запроса просто посмотреть на его идентификатор и сказать - "баа! Да это же инфа обо мне!". Ляпота, почти свой сервер и клиент, идентифицирующий пакеты.
Реализация: я человек ленивый достаточно, хоть с ленью и борюсь. Но не в этом случае. Я просто скачал пример того, что даём нам сам контакт - простенькое приложение с кнопочками. Я не зря говорил о лени - ищите этот пример сами.
Но нужно его и прокачать. Глядя в код видим кучу замыканий и некрасивый, нечитабельный код. Но на каждого парня с яйцами найдется каждый парень с ножницами, поэтому прикусив нижнюю губу работаем с тем, что есть. Файл DataProvider.as:
Сигнатура sendRequest:
Неплохо было бы этот options использовать. Я буду передавать ему поле customParameters, где будет хранится что-то там, идентифицирующее мой запрос. Чаще всего - строка, но чем черт не шутит

Ловким движением руки превращаем кусок их кода в кусок моего:
var loader:URLLoader = new URLLoader(); loader.dataFormat = URLLoaderDataFormat.TEXT; if (options.onError) { loader.addEventListener(IOErrorEvent.IO_ERROR, function():void { options.onError("Connection error occured", options.customParameters); }); loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function():void { options.onError("Security error occured", options.customParameters); }); } loader.addEventListener(Event.COMPLETE, function(e:Event):void{ var loader:URLLoader = URLLoader(e.target); //trace(loader.data); var data: Object = JSON.decode(loader.data); if (data.error) { options.onError(data.error, options.customParameters); } else if (options.onComplete) { options.onComplete(data.response, options.customParameters); } });
В APIConnection есть метод api, который мы и дёргаем. Логично было бы туда передавать сразу customParameters.
public function api(method: String, params: Object, onComplete:Function = null, onError:Function = null, customParameters:Object = null):void { var options: Object = new Object(); options['params'] = params; options['onComplete'] = onComplete; options['onError'] = onError; options['customParameters'] = customParameters; dp.request(method, options); }
Всё, теперь дело происходит так APIConnection.api -> DataProvider.request -> DataProvider._sendRequest -> onComplete/onError.
И в этом onComplete/onError я имею мой аргументик. Прелестно.
А ещё мне не нравится что onComplete/onError передаётся чистый коллбек, без наворотов вроде нашего любимого event flow (читай - addEventListener/dispatchEvent). Я переделал это дело тоже, только уже внутри VkontakteConnector. Примерно так:
private function onError(event:VkontakteEvent):void { var e:SocialEvent = new SocialEvent(SocialEvent.ERROR); e.customArguments = event.customArguments; e.response = event.response; _dispatcher.errorHandler(e); }
В итоге я подписываюсь банально на Последний презерватив сделан ради того, чтобы подписывался я как написано выше, а не вот так:
Однако, какой типа коллбеков использовать - личное дело каждого.
А вот теперь давайте подумаем. Предположим что злой сисадмин провайдер пользователя годами мечтал играть в игры вконтакте, но ему нельзя! Потому что за ним следит великий начальник, который запрещает. И вот он творит всякую злость, черную магию, бьет в бубен... Ну и приключилось так, что произошла ошибка у запроса. Из за зависти черного системного администратора. Ошибка попадёт в onError. А в onSuccess никто не попадёт! А ерунда в том, что именно в onSuccess мы ждали мессию, и сказали пользователю - жди! Сейчас контакт тебе ответит. А он не ответил. Короче, понимаете к чему я клоню? Если происходит такая фигня - нужно запрос тут же отправить заново! Причем прозрачно для всей программы.
5) Редиспатч запроса при ошибке. Однако следует предусмотреть некую границу редиспатчей. Если вдруг запрос 15 раз не прошел, наверное, он не пройдёт и на 16. И стоит записать на сервер что нам отвечает контакт, что мы хотели спросить, значения разных там ключевых переменных... И показать юзеру ошибку. Сказать, что виноват Дуров.
Реализация: банально по идентификаторам храним и сам запрос. При удачном проходе - удаляем. При ошибке - находим его в хранилище отправленных - и пробуем ещё раз. Ну, и ещё одно хранилище для числа текущих неудачных запросов. Запрос, получается, должен иметь:
1) Имя (идентификатор).
2) "Внутренность запроса" - то есть сам запрос, что уходит вконтакт
3) Время, когда он был отправлен
4) Количество запросов, неудачно отправленных.
Неплохо было бы оформить это дело в класс с этими 4 полями. Типизация, всё такое.
Ну всё, теперь нам не страшно, если вдруг запрос "провалится". А значит мы можем слегка ослабить политику к числу запросов. Например, с политикой "враг не пройдёт" запросы будут отсылаться каждые 700 мс. А можно бы отсылать их с интервалом в 500 мс без потерь. Если черный сисадмин позволит. Приходим к мысли, что можно найти
6) Среднее время запроса. Если честно, это уже граничит с турбонадувом в чайнике. Сам никогда не делал, но идея весьма интересная, поэтому поделюсь. Суть в том, что вконтакту достаточно получать запрос каждые 334 мс. Если запрос идёт до контакта 350 мс, и обратно за 350 то в "плотном" режиме запрос отправляется каждые 700 мс. 3.5 секнды будет отправлено 7 запросов, а если отправлять их с интервалом 350 мс - то 10.
Реализация: время выполнения запроса запоминается и корректирует стартовую величину. Например, начальная величина 1000 мс. Запрос прошел за 200 мс (туда-обратно), значит до контакта он доходит за 100. Но раз на раз не приходится, поэтому 1000 и 100 как то взаимодействуют, получая "среднее время запроса".
Да хоть (1000 + 100) / 2 = 550. Следующий запрос в 100 мс сократит время до (550 + 100) / 2 = 325. Ну и так далее. Интервал между запросами - это средний интервал между запросами, но он не может быть меньше 334.
В итоге запросы выполняются с максимальной скоростью, какой только возможно. Конечно, при реализации следует применять этот интервал только в случае если все запросы заняты (можно параллельно отправить 2 запроса сразу, не выжидая паузу). В случае если происходит ошибка по причине "Слишком много запросов" - среднее время резко увеличивается, чтобы потом снова быть подкорректировано. А если такая ошибка повторяется слишком часто - система вообще переходит в состояние, описанное в пункте 3.
Но это просто мысли. Не думаю, что кому то действительно потребуется такой турбонадув.
7) Оформление в единый фреймворк. Всё сделали? Умницы! Теперь оглянитесь назад и поймите, что вы не хотите переживать снова весь этот кошмар. Сделайте фреймворк, задокументируйте и перекреститесь. А на следующий день встаньте, улыбнитесь солнышку и... продолжайте делать адаптер, например, под Мой Мир.
Приведу небольшой овервью сказанного:
1) Сделайте адаптер первым делом. Даже под одну соцсеть.
2) Кэшируйте всё что можно. Если нельзя - закэшируйте и подумайте ещё раз.
3) Делайте очередь запросов: 3 запроса в секунду это предел для контакта.
4) Подписывайте запросы, чтобы в любой момент времени можно было сказать, зачем этот запрос нужен.
5) Случилась ошибка - попробуй ещё раз!
6) Если личной жизнью не богат - турбонадуву будешь рад!
7) Оформи это в фреймворк и забудь как страшный сон.
Спасибо за внимание.
Всего комментариев 50
Комментарии
![]() ![]() |
|
каша в тексте и в голове
|
![]() ![]() |
|
Цитата:
Уверяю вас, в один прекрасный день начальник скажет - "а теперь в фейсбук!" и ваша седина сменится лысиной.
|
![]() ![]() |
|
mayakwd, покажи где конкретно, я исправлю)
|
![]() ![]() |
|
Цитата:
ОДНО ФЛЭШ ЯДРО, но, обязательно РАЗНЫЕ серверные части
|
![]() ![]() |
|
Зачем кешировать кешируемое? Засоряя при этом оперативную память. Я про картинки.
|
![]() ![]() |
|
Если кэшируются все же ссылки на картинки, тогда непонятно, почему упоминался кэш браузера, да и Loader все равно будет использоваться по мере надобности.
Увидел ПС ![]() С одной картинкой в работе смысла бы не имело заморачиваться с такой системой кэша. |
|
Обновил(-а) andrew911 22.12.2010 в 04:50
|
![]() ![]() |
|
Про ПС — я не имел в виду "одну картинку". Речь идет о том, что картинка уже есть (одна-две-двести). Завести в массиве ссылку на нее под ключом ["URL_картинки"] — вот и все расходы.
|
![]() ![]() |
|
Да, логично - если картинка отображается, то память она итак уже занимает.
|
![]() ![]() |
|
Можно компилить с разными конфигами.
|
![]() ![]() |
|
У нас один. Просто в местах, где клиенты разные - вьюшка спрашивает "мы где?" и в зависимости от социалки меняет нужные места. Их не так много.
|
![]() ![]() |
|
ничего не описался. отображается 4 + 1 эталон, с которого копировалось.
|
![]() ![]() |
|
Цитата:
ничего не описался. отображается 4 + 1 эталон, с которого копировалось.
|
![]() ![]() |
|
Psycho Tiger, так не бывает. эталон всегда BitmapData. поэтому всегда +1.
|
![]() ![]() |
|
Psycho Tiger, ну пускай по твоему они не расходятся. уговорил. лично мне всё равно.
|
Последние записи от Psycho Tiger
- Тонкости и трюки ActionScript`а, которые... бесполезны (10.05.2011)
- Vkontakte: как пользоваться wall.post, нужен ли теперь wall.savePost? (05.03.2011)
- А пятый контер-страйк хорош. (19.01.2011)
- Пацаны, гоу Вконтакте? (21.12.2010)
- Давайте начистоту (18.12.2010)