суббота, 29 ноября 2008 г.

Часть 2. Старт с Cairngorm. Использование Model Locator для управления Видами.

Куски кода съедены парсером, советую обратиться к первоисточникам (перевод с англ.:Getting Started with Cairngorm – Part 2. Using a ModelLocator to Manage the View )

Резюме: В первой части я рассказывал о базовом устройстве и использовании паттерна Model Locator. Этот паттерн один из многих, используемых в микроархитектуре Cairngorm. Он будет использоваться и во второй части, поэтому предполагается что вы знакомы с ним, или обратиться к 1 части для ознакомления. На данный момент мы еще не работали с "полным" Cairngorm (это произойдет в 3 части).

Как и все статьи этой серии, урок состоит из двух частей. В этой статье вы узнаете всю теорию этой темы, а потом на видео вы сможете со мной попрограммировать. Эта статья содержит инструкции, как настроить проект для нашего совместного творчества.

На предыдущем уроке мы узнали о преимуществах использования ModelLocator для управления данными в рамках приложения. Однако преимущество паттерна этим не исчерпывается. Он может управлять Видами в приложении. Чтобы понять, как это происходит, вам нужно создать создать новый проект "ViewManager" с главным файлом "Main.mxml". Также будет нужно добавить Cairngorn.swc к проекту и создать новые папки - view и model. Когда вы закончите, проект должен будет выглядеть следующим образом:



Теперь, вам необходимо взять код ModelLocator из предыдущего урока и разместить внутри нашего приложения. Я размещаю этот код здесь для вашего удобства

Actionscript:
package model {
import com.adobe.cairngorm.model.IModelLocator;
[Bindable]
public class ViewModelLocator implements IModelLocator {
// Single Instance of Our ModelLocator
private static var instance:ViewModelLocator;
public function ViewModelLocator(enforcer:SingletonEnforcer) {
if (enforcer == null) {
throw new Error( "You Can Only Have One ViewModelLocator" );
}
}
// Returns the Single Instance
public static function getInstance() : ViewModelLocator {
if (instance == null) {
instance = new ViewModelLocator( new SingletonEnforcer );
}
return instance;
}
//DEFINE YOUR VARIABLES HERE
}
}
// Utility Class to Deny Access to Constructor
class SingletonEnforcer {}

Если вам нужна подробная информация о ModelLocator, вернитесь к 1 части руководства.

Единственно, что должно быть исправлено в ModelLocator - это определение пэкаджа. ModelLocator этого проекта мы будем размещать в папке "model", соответсвенно в объявлении пэкаджа должно быть слово "model" (это уже исправлено в листинге выше). Также необходимо добавить еще одну публичную переменную "workflowState" типа "uint". Объявление будет выглядеть следующим образом:

Actionscript:
public var workflowState:uint = 0;

Эта переменная будет использоваться для "управления" вида вашего приложения. Наиболее распространенным способом для этого является использование ViewStack. Если вы с ним не знакомы, почитайте о нем здесь. В частности о свойстве selectedIndex компонента ViewStack. Это числовое значения определяет видимого во ViewStack потомка. Рассмотрим следующий код:

mxml:












По умолчанию selectedIndex равен 0 и отображается box1. Если вы исполните команду:

ActionScript:
myViewStack.selectedIndex = 1;

то будет виден box2. Если применить ModelLocator, можно использовать переменную workflowState для установки selectedIndex. Используя значение workflowState для selectedIndex, можно получить полный контроль за тем, что отображается во ViewStack вашего ModelLocator.

mxml:

...


Определение констант для улучшения кода

Конечно с помощью этого метода легко управлять приложением, однако он может привести к путанице в коде. Например предположим что у нас есть следующее:
1. ViewStack с двумя потомками: LoginScreen и WelcomeScreen
2. Свойство selectedIndex у ViewStack'a управляется свойством workflowState
3. Кнопка "Login" определяет действие myViewStack.selectedIndex = 1;

Может показаться что это будет работать корректно и нет нужды в каких-либо изменениях. Попробуйте добавить во ViewStack еще одного потомка и он может нарушить порядок. Иногда бывает необходимо вручную установить свойство selectedIndex. Для этого просто нужно определить константы внутри ModelLocator.

ActionScript:
//DEFINE YOUR VARIABLES HERE
public var workflowState:uint = 0;
// DEFINE VIEW CONSTANTS
public static const LOGIN_SCREEN = 0;
public static const WELCOME_SCREEN = 1;

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

ActionScript:
myViewStack.selectedIndex = ViewModelLocator.WELCOME_SCREEN;

Вы не только защитили код от будущих изменений, но также сделали его более логичным. Другим разработчикам будет легче понять процесс без просмотра потомков ViewStack.

Видеопример
http://www.davidtucker.net/swf/flv/cairngorm2.flv

Исходники урока
http://www.davidtucker.net/airtips/ViewManager.zip

Flex Cairngorm. Часть 1. Старт с использованием Model Locator

(перевод с англ.: Getting Started with Cairngorm – Part 1)

В Cairngorn используется паттерн Model Locator, но им одним Cairngorn не исчерпывается. Сначала мы рассмотрим преимущества использования данного паттерна.

Model Locator - это централизованный репозиторий для всех данных, имеющихся в вашем приложенн. Ваши данные располагаются в Синглтоне (см. паттерн Одиночка). Этот "класс" может иметь только один экземпляр. Почему это так важно? Покажем это на примере.

У нас есть замечательный мини-блокнот для работы, Я в него записываю все данные в течении рабочего дня. В определенный момент я потерял блокнот и купил себе новый, а потом и нашел старый. После я записывал в оба блокнота. Мне действительно тяжело найти запись, которую я сделал допустим неделю назад. Это простой пример - но представим что у меня 20 блокнотов? Это похоже на сумашествие.

Таким образом, мы получаем "класс", который будет инстанцирован 20 раз в вашем приложении. Самый простой способ обойти эту проблему - использовать паттерн Одиночка. Одиночка - это класс который никогда не создается традиционным способом. Его главное правило - не может существовать более одного экземпляра данного класса. Я покажу на примере, как его сделать.

ActionScript:
package net.davidtucker.CairngormSample.model {
import com.adobe.cairngorm.model.IModelLocator;
[Bindable]
public class ModelLocator implements IModelLocator {
// Single Instance of Our ModelLocator
private static var instance:ModelLocator;
public function ModelLocator(enforcer:SingletonEnforcer) {
if (enforcer == null) {
throw new Error( "You Can Only Have One ModelLocator" );
}
}
// Returns the Single Instance
public static function getInstance() : ModelLocator {
if (instance == null) {
instance = new ModelLocator( new SingletonEnforcer );
}
return instance;
}
//DEFINE YOUR VARIABLES HERE
}
}
// Utility Class to Deny Access to Constructor
class SingletonEnforcer {}

Код может показаться в начале сложным, но поверьте мне, что это не так сложно как кажется. Сначала идет определение пэкаджа и нескольких классов. Далее определение интерфейса IModelLocator, которое нам понадобится в дальнейшем. IModelLocator располагается в SWC Cairngorm которым можно скачать отсюда: http://labs.adobe.com/wiki/index.php/Cairngorm:Cairngorm2.2:Download. Стоит обратить внимание, что можно использовать Model Locator без Cairngon. Я это тоже делаю для небольших проектов. Для этого необходимо будет удалить строчки 'implements IModelLocator' и 'import com.adobe.cairngorm.model.IModelLocator' из кода паттерна Model Locator.

ActionScript:
[Bindable]
public class ModelLocator implements IModelLocator {
// Single Instance of Our ModelLocator
private static var instance:ModelLocator;

Теперь у нас есть определение класса. Важно использовать метатэг [Bindable] над определение класса. Это позволит использовать все определенные внутри класса переменные. Идем дальше. Мы создаем переменную instance, которая будет иметь тип Model Locator. В ней будет храниться экземпляр нашего класса. Он будет объявлен статическим. Вы не уверены, является ли данная переменная статической? На самом деле это нормально, мы обсудим это на следующем уроке.

ActionScript:
public function ModelLocator(enforcer:SingletonEnforcer) {
if (enforcer == null) {
throw new Error( "You Can Only Have One ModelLocator" );
}
}

После этого идет конструктор. В качестве аргумента enforcer типа SingletonEnforcer. Данный класс (SingletonEnforcer) определен после нашего класса. Это делается, потому что:

- когда вы пишете класс в файле ActionScript ниже основного класса, он доступен только для этого основного класса
- конструктор требует данный аргумент, но только наш класс может его создать, т.к. мы не имеем доступа к классу SingletonEnforcer. Только основной класс
- мы не можем создать класс директивой "new", потому что мы не можем выполнить конструктор. (Я покажу вам, как мы будем это делать).

В конструкторе несколько строк кода, которые гарантируют нормальное состояние. В секции IF проверяется валидность объекта "enforcer". Если он неправильный, генерируется сообщение об ошибке.

ActionScript:
// Returns the Single Instance
public static function getInstance() : ModelLocator {
if (instance == null) {
instance = new ModelLocator( new SingletonEnforcer );
}
return instance;
}

Функция "getInstance" дает доступ из приложения к ModelLocator. Эта функция просто берет единственный экземпляр класса, а если он не создан - создает его. Мы можем получить ModelLocator используя такой код:

ActionScript:
var model:ModelLocator = ModelLocator.getInstance();

Видеопример - Создание Адресной Книги в формате FLV:
http://www.davidtucker.net/swf/flv/cairngorm1.flv
Проигрыватель для FLV:
http://www.wimpyplayer.com/products/wimpy_standalone_flv_player.html

Исходники
http://www.davidtucker.net/airtips/ModelLocatorSample.zip

суббота, 22 ноября 2008 г.

PHP сокет сервер и чат-шлюз для флеш-клиентов

(перевод с англ., источник: PHP Socket server and Chat Gateway for Flash clients)

Первоначально, я не планировал писать пошаговое руководство "PHP сокет сервер и чат шлюз для флеш клиентов за 10 минут", или что-то похожее.

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

Я не включил флеш-часть в свой документ, т.к. ее делал мой коллега и друг. Я не могу опубликовать этот код.

Для общих сведений, как мы это делали, Вы можете почитать руководство kirupa.com - PHP 5 Sockets with Flash 8.

Таким образом, в этой статье я покажу вам наши решения чат-сервера на PHP 5, флеш-клиентах, эмулятора веб-сервера, использующего кросс-доменную политику из файла crossdomain.xml, отдаваемому по запросу, и общение с сервером, базирующемуся на xml-сообщениях. Этот пример только показывает, как создать многопользовательский чат с приватным общением. Т.е. вы можете разговаривать сразу с несколькими людьми, но не в чат-комнатах (каналах).

Будем использовать PHP из командной строки, т.к. в полноценном веб-сервере нет необходимости.

Для начала, нам необходимо создать демон (фоновый процесс в Windows), без временного лимита на исполнения. Скрипт в данном случае выполняется до конца света, или до первой перезагрузки :). Также мы установим IP адрес и порт для прослушивания.

#!/usr/bin/php -q set_time_limit(0); ob_implicit_flush(); $address = '127.0.0.1'; $port = 80; Давайте мы созданим массив для входящих подключений (если вы используете его для чата, в нем можно будет хранить ники). Далее создаем сокет. Все echo сообщения в нашем примере идут в лог-файл. $_sockets = array(); if (($master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0) { echo "socket_create() failed, reason: " . socket_strerror($master) . "\n"; } socket_set_option($master, SOL_SOCKET,SO_REUSEADDR, 1); if (($ret = socket_bind($master, $address, $port)) < 0) { echo "socket_bind() failed, reason: " . socket_strerror($ret) . "\n"; } if (($ret = socket_listen($master, 5)) < 0) { echo "socket_listen() failed, reason: " . socket_strerror($ret) . "\n"; } else { $started=time(); echo "[".date('Y-m-d H:i:s')."] SERVER CREATED ( MAXCONN:".SOMAXCONN." ) \n"; echo "[".date('Y-m-d H:i:s')."] Listening on ".$address.":".$port."\n"; } $read_sockets = array($master);
SOMAXCONN является переменной ядра. Она устанавливает количество подключений, которые может обработать ваш сервер. В Unix она может быть установлена на уровне ядра и скорректирована командой sysctl.

После этого мы создаем бесконечный цикл для обработки запросов

while (true) {
$changed_sockets = $read_sockets;
$num_changed_sockets = socket_select($changed_sockets, $write = NULL, $except = NULL, NULL);

foreach($changed_sockets as $socket) {
if ($socket == $master) {
if (($client = socket_accept($master)) < 0) {
echo "socket_accept() failed: reason: " . socket_strerror($msgsock) . "\n";
continue;
} else {
array_push($read_sockets, $client);
echo "[".date('Y-m-d H:i:s')."] CONNECTED "."(".count($read_sockets)."/".SOMAXCONN.")\n";
}
}
else
{
$bytes = @socket_recv($socket, $buffer, 2048, 0);
/*

Here comes the core... ;)

*/
}
}

Это основа. До данного момента, этот код такой же, как в публикации Raymond Fain в socketShell.php, которая упоминалась выше.
Как я уже говорил, мы будем править еще часть кода.

Я буду цитировать каждый кусок кода в следующей части. Мы начнем с определения функций, которые описываются вне бесконечного цикла.

Сначала мы сделаем эмулятор веб-сервера. Зачем нам это нужно?

Суть системы состоит в том, чтобы общаться по 80-му порту, т.к. файрволлы не будут блокировать сообщения а нем. Но проблема заключается в том что, как вы знаете, флеш по умолчанию общается по 1024 порту. Что неприемлемо из-за политик безопастности.

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

Политики хранятся в XML-формате, который выглядит в нашем случае так:





Я храню ее в переменной. Мой демон, обслуживающий сокеты, получает запрос от флеш.
В ответ мы отдаем файл кроссдоменной политики, что означает, что мы разрешаем подключение по 80 порту.

Если вы хотите подробно почитать о кроссдоменных политиках, зайдите на http://www.crossdomainxml.org/

Ваш код после socket_recv будет выглядеть так:

if (preg_match("/policy-file-request/i", $buffer) || preg_match("/crossdomain/i", $buffer)) {
echo "[".date('Y-m-d H:i:s')."] CROSSDOMAIN.XML REQUEST\n";
$contents='';

socket_write($socket,$contents);
$contents="";

$index = array_search($socket, $read_sockets);
unset($read_sockets[$index]);
socket_shutdown($socket, 2);
socket_close($socket);
}

Мы закрываем сокет, поскольку это флеш-запрос на файл кроссдоменной политики. Получив данный файл, флеш-приложение закрывает соединение и переподключается на "правильном" сокете.

После обработчика для флеш-клиентов, создадим подробные сообщения о состоянии сервера (состоянии демона и /server-status). Они могут быть просмотрены, если набрать в браузере IP адрес (и порт, если он отличный от 80). Набрав http://127.0.0.1/server-status, мы будем получать такую информацию:

OK
Clients: 48/128
Created: 2007-07-10 10:54:02
Uptime: 16 days

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

И последнее. Я эмулирую веб-серверную часть, чтобы перенаправить все GET и POST запросы с HTTP заголовками, которые идут не через сокет.
Мы также сделаем запись в журнале запросов и создадим переадресацию HTTP.

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

elseif (( preg_match("/GET/", $buffer) || preg_match("/POST/", $buffer)) && preg_match("/HTTP/", $buffer))
{
if (preg_match("//server-status/i", $buffer))
{
$uptime = floor((time()-$started)/86400);

socket_write($socket,"OK\n");
socket_write($socket,"Clients: ".count($read_sockets)."/".SOMAXCONN."\n");
socket_write($socket,"Created: ".date('Y-m-d H:i:s',$started)."\n");
socket_write($socket,"Uptime: ".$uptime." days\n");
echo "[".date('Y-m-d H:i:s')."] STATUS REQUEST\n";
}
elseif (preg_match("/favicon.ico/i", $buffer))
{
//ignore :)
}
else
{
// fake web server
socket_write($socket,"HTTP/1.1 301 Moved Permanently\n");
socket_write($socket,"Server: PHP Chat Server by DjZoNe - http://djz.hu/\n");
socket_write($socket,"Date: ".date("d, j M Y G:i:s T\n"));
socket_write($socket,"Last-Modified: ".date("d, j M Y G:i:s T\n"));
socket_write($socket,"Location: http://djz.hu/\n");
}
$index = array_search($socket, $read_sockets);
unset($read_sockets[$index]);
@socket_shutdown($socket, 2);
@socket_close($socket);
}


Пока что мы сделали часть, стартующую сокет-сервер и перенаправляющую HTTP-запросы. В следующей части мы напишем обработчик ситуации, когда сокет закрывается и пользователь отключается.

if (strlen($buffer) == 0) {
// мы берем уникальный ID пользователя из базы данных
$id=$_sockets[intval($socket)]['nick'];

$index = array_search($socket, $read_sockets);

unset($read_sockets[$index]); // we clean up
unset($_sockets[intval($socket)]); // we clean up our own data
// cleaning up is essential when creating a daemon
// we can't leave junk in the memory
@socket_shutdown($socket, 2);
@socket_close($socket);

$allclients = $read_sockets; // перезагружаем активных клиентов

// $socket is now pointing to a dead resource id
// but the send_Message() function will need it, I'll explain later

send_Message($allclients, "");
echo "[".date('Y-m-d H:i:s')."] QUIT ".$id."\n";
}

И вот сейчас реальная сокет-коммуникация:

else {
$allclients = $read_sockets;
array_shift($allclients);

$piece = explode(" ",trim($buffer)); // we strip out all unwanted data
$cmd = strtoupper($piece[0]);
}

Мы используем несколько команд IRC-протокола.
MSG сообщение
IDENTIFY ник пароль
LIST

Мы разбиваем сообщение на куски, и склеиваем его после того, как только определим первые несколько аргументов.

if (!empty($piece[1])) $content = $piece[1];

switch ($cmd) {
case "IDENTIFY":
$id = trim($piece[1]);
$passwd = trim($piece[2]);
send_Identify($allclients, $socket, $id, $passwd);
break;

case "MSG":
$id = trim($piece[1]);
$msg="";
foreach ($piece as $key=>$val)
{
if ($key > "1") $msg.=$val." ";
}
$msg = trim($msg);
send_Msg($allclients, $socket, $id, $msg);
break;

case "LIST":
list_Users($allclients, $socket);
break;
}

Мы сделали вызовы команд.
До сих пор мы создавали цикл.
Сейчас мы будем создавать функции. Снаружи.

Я хочу рассказать вам историю.... Просто шутка ;)
Мы сделали socket_write и все. Мы не сможем получить через сокет информацию, пока не поместим символ ASCII 0 в конец

буфера. Т.е. мы вводим ноль ASCII после каждого socket_write. Думаю, для XML-сокет коммуникации через Flash это просто.

Если мы посмотрим назад в "эмулятор веб-сервера", мы просто добавим одну строчку.

Вот вам несколько функций, которые мы используем для авторизации, отправки сообщений и т.д.

function send_Identify($allclients, $socket, $id, $passwd)
{
global $_sockets;
$nicks = array();

$dbconf = new DATABASE_CONFIG;

$db_host = $dbconf->host;
$db_base = $dbconf->database;
$db_login = $dbconf->login;
$db_password = $dbconf->password;

foreach ($_sockets as $_socket)
{
foreach ($_socket as $key=>$val)
{
if (empty($nicks[$val])) $nicks[$val]=1;
else $nicks[$val]=$nicks[$val]+1;
}
}

if (empty($nicks[$id]))
{
$s=1;
// Here will be a simple authentication.

$link = mysql_connect($db_host, $db_login, $db_password);
if (!$link) die("Could not connect:" . mysql_error() . "\n");

$db_selected = mysql_select_db($db_base, $link);
if (!$db_selected) die("Can't use $db_base :" . mysql_error() . "\n");

$result = mysql_query("SELECT nick FROM members WHERE id='".intval($id)."' AND password='".crypt($passwd)."' AND

active='1' LIMIT 1");
$data = mysql_fetch_array($result);
$name = $data['name'];
$_sockets[intval($socket)]=array('id'=>$id, 'nick'=>$name);

mysql_free_result($result);
mysql_close($link);

После использования SQL-соединения его необходимо закрыть. Это важно, т.к. соединение будет закрываться по тайм-ауту и

демон умрет.

}
else $s=0;

// We'll answer to the flash in XML form.
// But we receive in plain text format.

if ($s == 1)
{
$out = "";
send_Message($allclients, "");
// this goes to all active, identified clients
echo "[".date('Y-m-d H:i:s')."] LOGIN ".$id."(".count($allclients)."/".SOMAXCONN.")\n";
}
else $out = "";

socket_write($socket, $out.chr(0)); // write back to the client
}



function send_Msg($allclients,$socket,$id,$msg)
{
global $_sockets;

if (!empty($_sockets[intval($socket)]))
{
$nicks = array(); //amig fut a parancs ebben vannak a nickek.

foreach ($_sockets as $_socket)
{
foreach ($_socket as $key=>$val)
{
// this check's the onliners
if (empty($nicks[$val])) $nicks[$val]=1;
else $nicks[$val]=$nicks[$val]+1; // we shouldn't have duplicated nicks, but what if...
}
}

foreach($allclients as $client)
{
if (!empty($_sockets[$client]['nick']) && ($_sockets[$client]['nick'] == $id))
{
$_client = $client;
$out = "
from=\"".$_sockets[$client]['nick']."\" />";
}
elseif(empty($nicks[$id]))
//not online or something similar
{
//backto the sender
$_client = $socket;
$out = "";
}
}
}
else
{
//backto the sender
$_client = $socket;
$out = "";
}
if (!empty($out))
{
socket_write($socket, $out.chr(0)); //send to back ourself. we have to handle it in flash
socket_write($_client, $out.chr(0)); //send to the recipient
}
}

Сейчас мы создали функцию, которая посылает сообщение всем присоединенным клиентам. Далее покажем, как получить список всех пользователей.

function send_Message($allclients, $socket, $buf) {
global $_sockets;

foreach($allclients as $client) {
@socket_write($client, $buf.chr(0));
}
}

function list_Users($allclients,$socket) {
global $_sockets;
$out = "";
foreach($allclients as $client) {
if (!empty($_sockets[$client]['nick']) && ($_sockets[$client]['nick'] != "")) {
$out .= "";
}
}
$out .= "
";
socket_write($socket, $out.chr(0));
}
?>

В данный момент наш демон обрабатывает три основные команды - идентификацию, получение списка пользователей и отправку

сообщений. Это то, что я обещал вам в самом начале. Вы можете доработать код. Например, мы не имеем функции для смена ника (команды /nick в IRC) и т.д.

Это все. Исходник может быть загружен с здесь.

А в конце у меня для Вас подарок. Это маленький BASH скрипт для запуска демона:

#!/bin/sh
if [ "X$1" = "Xstart" ] ; then
chmod +x /var/www/chat/phpircgateway.php
/var/www/chat/phpircgateway.php >> /var/log/chat/chat.log &
echo "Starting chat"
fi