Создание полноценного чата на Flash

altИтак, решил я изучить Flash, нашел книжку по 5-ому флешу просмотрел ее по диагонали, кое что в голове отложилось, но в целом все очень туманно… Поискал в инете учебники по флешу… Открыл в первый раз Macromedia Flash MX 2004 потыкался туда-сюда, нарисовал шарик, сделал чтобы он бегал за курсором мыши и понял, придется делать все как обычно: разбираться самостоятельно с нуля.
Во Flash меня в первую очередь интересует программирование, а не рисование мультиков, потому и решил начать свое обучение с написания чата. Посмотрел, что есть в инете на тему «чат на Flash», нашел одну примитивную заметку, где все реализовывалось через refresh с подчитыванием 50-ти последних строк чата и вывода их в виде обычного текста. Попытался найти работающие чаты на Flash — ситуация хуже, чем я предполагал… Если это более-менее нормальный чат, то сам вывод текста реализован в HTML с подгрузкой последних строк через refresh, а на Flash реализованы только строка для ввода текста и смены настроек… Если же это чат полностью на Flash, то функционал на уровне детского сада, обычный текст, без смайликов и возможности кликать по ссылкам в тексте или по нику отправителя, чтобы написать ему ответ… За две недели поиска информации по этому вопросу, мне попался только один достойный чат на Flash постоянный коннект, удобный интерфейс, продуманный протокол с малым трафиком, стильно оформлен… но нет возможности вставлять смайлики в основном окне общения.
Поэтому, идея создания нормального чата мне еще больше понравилась. А так как я пишу его, не имея навыков работы во Flash, то подробное описание процесса со всеми сделанными ошибками и открытиями будет очень хорошим пособием для начинающих изучать Flash.
1. Постановка задачи
Что же в итоге должно получиться:
Клиентская часть чата должна быть реализована на Flash, серверная на Perl.
Я хорошо знаю Perl поэтому мне будет проще и быстрее на нем писать, а по большому счету на чем реализовывать серверную часть роли не играет, от этого будет только зависеть сколько народу в online выдержит сервер.
Чат должен держать постоянный коннект с сервером.
В этом есть свои плюсы и минусы. Плюсы: меньше трафик, больше динамика чата, т.к. нет пауз из-за рефреша, однозначное определение есть человек в online или у него разорвалась связь. Минусы: на сервере необходимо запустить своего демона на выделенном порту (это можно сделать, только имея рутовый доступ), если у юзера на машине стоит FireWall ему надо будет открыть доступ по этому порту, если юзер выходит в инет через прокси, может не получиться соединиться с сервером (не знаю как во Flash решены эти проблемы). И все же, чаты с технологией refresh, на мой взгляд, дело прошлое, и надо идти в ногу со временем, поэтому делаем постоянное соединение.
В чате должны быть смайлики. Ники кликабельны.
Возможность выбрать цвет своих сообщений.
Подсветка сообщений адресованных мне. Возможность установки фильтров.
P.S. Мы не ищем легких задач 🙂
2. Установка постоянного коннекта с сервером.
Начнем сначала, ввод имени и установка коннекта с сервером. Для обмена информацией с сервером используем объект XMLSocket. Итак, во флеш создаем новый документ и делаем титульную страницу для входа в чат: поле для ввода ника (TextInput используем стандартные компоненты флеша MX из окна components) и кнопку «войти» (Button). Регистрацию делать не будем, это не имеет отношения к самому чату.
Поле ввода ника обзываем login. Кнопку: enter_button.
Кроме этого понадобится еще пустое поле Dynamic Text, куда будем выводить информацию о подключении к серверу и ошибки, если такие возникнут. Поле привязываем к переменной messages. Цвет текста красный, чтобы в глаза бросалось.
В actions кнопки пишем:
on (click) {
_parent.messages = _parent.login.text;
}
Внешне получилось вот что:

Если нажать Ctrl-Enter, ввести в поле свое имя и нажать на кнопку «Войти», то имя выведется ниже красным цветом. Отлично, уже что-то работает 🙂 Создаем новый слой, обзовем его Scripts, там и будем писать все основные скрипты. Для начала, меняем код нажатия кнопки на:
on (click) {
if (length(_parent.login.text) < 3) {
_parent.messages = «Введите ваше имя»;
_parent.login.setFocus();
} else {
_root.Connect();
}
}
Если имя не было введено (или оно меньше трех символов), то ругаемся, иначе переходим к установке соединения с сервером. Саму функцию Connect () пишем в слое Scripts, вот что у меня получилось (большая часть скопирована из хелпа к XMLSocket 😉 var serverName = «localhost»;
var serverPort = 1024;

// Устанавливает соединение с сервером
function Connect() {
messages = «Устанавливается соединение с сервером…»;
// Create a new XMLSocket object
sock = new XMLSocket();
// Устанавливаем обработчики событий
sock.onConnect = onSockConnect; // соединение
sock.onXML = onGetXML; // получение данных
sock.onClose = onSockClose; // связь утеряна
// Call its connect() method to establish a connection
sock.connect(serverName, serverPort);
}

// Define a function to assign to the sock object that handles
// the server’s response.
function onSockConnect(success){
if (success){ // соединение установлено
messages = «Соединение установлено. Вход…»;
doLogin(); // переходим к процедуре передачи нашего логина
} else {
messages=»Не удалось установить соединение с сервером: «+serverName;
}
}

// Передача нашего логина на сервер
function doLogin() {
var myXML = new XML(««);
sock.send(myXML);
}

// Закрылось соединение с сервером
function onSockClose(){
gotoAndStop(1); // Перейти на титульную страницу, где бы мы не находились
messages = «Сервер разорвал соединение.»;
}

// Прием данных от сервера
function onGetXML(doc) {
trace(«onXML: «+doc);
var e = doc.firstChild; // Берем первый элемент
if (e != null) {
var s = e.nodeName; // Имя элемента
if (s == «ERROR») { // Произошла ошибка, разорвать соединение и
sock.close();
messages = e.attributes.TEXT; // Вывести сообщение об ошибке
}
}
}

Хорошо, теперь нужно немного отвлечься от Flash и написать простенький серверный демон на Perl… куда собственно будем подключаться. Вот код:

#!/usr/bin/perl
use POSIX ();
use Socket;

my $DaemonPort = 1024;
my $work = 1;
$|=1;

my $sock_name = sockaddr_in($DaemonPort, INADDR_ANY)
or die «Couldn’t convert into an Internet address: $!\n»;
socket(SERVER, PF_INET, SOCK_STREAM, getprotobyname(‘tcp’))
or die «Couldn’t create socket: $!\n»;
setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1)
or die «setsockopt() failed: $!\n»;
bind(SERVER, $sock_name)
or die «Couldn’t bind to port $port: $!\n»;
listen(SERVER, SOMAXCONN);
$SIG{PIPE} = ‘IGNORE’;

_log(«Server started…»);

my $rem_addr = accept(CLIENT,SERVER);
next unless (defined $rem_addr);
my($port,$iaddr) = sockaddr_in($rem_addr);
$IP = inet_ntoa($iaddr);
_log(«Connection from $IP:$port»);

my ($byte, $line);
while ($work and sysread(CLIENT, $byte, 1) == 1) {
if (ord($byte) == 0) { goCommand($line) }
else { $line .= $byte }
}

sleep(3); #— Замрем на 3 секунды
_log(«Die connection.»);
close CLIENT;

close(SERVER);
_log(«Server shutdown»);
die;

#— Выводит на экран тестовую информацию
sub _log
{ my ($s) = @_;
print «».(localtime(time)).»\t$s\n»;

}

#— Обработка поступившей от клиента команды
sub goCommand
{ my ($line) = @_;
if (index($line, «#— Говорим, что такой логин занят (test)
sendAnswer(««);
}
}
$work = 0;
}

#— Отсылает ответ клиенту. Проблема в том, что русские буквы надо
#— кодировать в utf и в конце ставим ноль
sub sendAnswer
{ my ($s) = @_;
print CLIENT utf($s).chr(0);
}

#— Функции кодирования русских букв нашел где-то в инете, очень не хотелось
#— цеплять здоровые библиотеки по работе с utf ради такой мелочи
sub utf
{ my $s = shift;
$s=~s/([А-Яа-яЪЬЁъьё])/win2utf($1)/eg;
return $s;
}

sub win2utf
{ my $s = shift;
if ( ord($s)>=192 and ord($s)<=239) { return chr(208).chr(ord($s)-48) }
if ( ord($s)>=240 and ord($s)<=255) { return chr(209).chr(ord($s)-112)}
if ($s==»Ё») { return chr(208).chr(149) }
if ($s==»ё») { return chr(208).chr(181) }
if ($s==»Ъ») { return chr(208).chr(172) }
if ($s==»Ь») { return chr(208).chr(170) }
if ($s==»ъ») { return chr(208).chr(140) }
if ($s==»ь») { return chr(208).chr(138) }
return $s;
}
Комментировать тут особо нечего, кто в Perl разбирается, тот поймет, а другим и не надо (мы пишем чат на Flash, а не Perl изучаем 🙂 В двух словах: он ожидает подключения к порту 1024, затем ожидает команду и если это perl chat_daemon.pl (у вас должен быть установлен Perl на компьютере, скачать его можно на http://www.activestate.com порядка 10Мб.)
И запускаете флешку. Вводим любое имя и нажимаем «Войти», видим сообщение об установке соединения, о том, что соединение установлено и через 3 секунды (такая задержка стоит в серверном скрипте) сообщение об ошибке, что такой логин занят…
3. Протокол.
Итак, с установкой соединения разобрались, теперь нужно подумать о протоколе общения серверной части и клиентской. Здесь обсуждать особенно нечего, как придумаешь, так и реализовывать будешь. Необходимый минимум: залогинивание, получение сообщений, получение списка народа в online, отправка сообщения, вывод сообщений о критических ошибках.
3.1. Залогинивание у нас фактически уже реализовано. Клиент передает на сервер:
В ответ получит или сообщение об ошибке, или команда — что означает нормальное подключение к чату.
3.2. Сообщение о критических ошибках тоже есть. Сервер передает:

3.3. Получение списка народа в чате (online). Для экономии трафика, передадим весь список в одном теге, ведь в нашем варианте чата, кроме имени человека ничего больше знать не нужно. [список имен через запятую] В целях экономии трафика нужно еще сделать команды: пришел новый человек, ушел человек. Чтобы ради одного имени не отсылать весь список полностью. Это нам дает еще одно преимущество, не формировать сообщения вида «вас приветствует [имя]» и «уходит [имя]» непосредственно на сервере, такие сообщения можно формировать в самом Flash-клиенте. Добавляем еще две команды:
[имя] — пришел новый человек.
[имя] — ушел.
3.4. Отправка сообщения на сервер. В целях уменьшения трафика все теги и свойства обозначаем одной буквой. Кроме самого сообщения нужно передавать еще и его цвет.
[текст сообщения] C — цвет текста. Если он не указан — пишем черным.
Кому адресовано сообщение и смайлики указываются прямо в тексте. Например в таком виде:
to [кому] — обращение в общем чате к кому-то
private [кому] — отправка приватного сообщения, его увидит только получатель
смайлики в тексте сообщения выделяются двоеточием с двух сторон :smile:, какие именно будут смайлики уточним позже.
3.5. Получение сообщений от серверного скрипта. Делаем аналогично команде отправки сообщения на сервер.
строка текста
C — цвет текста. По умолчанию — черный.
4. Отображение списка болтунов в online.
Переходим непосредственно к чату. Создаем пустой ключевой фрейм. Что и как должно располагаться, см. рисунок.

Займемся отображением списка online, здесь нас сразу подстерегает «биг проблема»
В списке кроме отображения имен нужно отображать еще и кнопочку, для посылки приватных сообщений. Клик по имени — отправка обычного сообщения в общий чат. Клик по картинке приватного сообщения — отправка приватного сообщения, которое видит только адресат. Стандартный компонент List нам не подходит, он позволяет только выбрать один элемент из списка, но не позволяет делать несколько кнопок в одной строчке списка.
Я вижу четыре пути решения этой проблемы. Первые три — это создание нужного списка самостоятельно, последний — подход не программиста, а менеджера.
4.1. Программно формируется слой, где размещаются необходимые кнопки и имена в нужном порядке. Получается большая такая лента, над этим слоем располагается слой маска, с прямоугольной областью в которую мы и наблюдаем видимую часть списка. При этом надо будет сделать кнопочки прокрутки списка вверх — вниз.
Недостаток — если народу в списке будет очень много, то начнутся серьезные тормоза с формированием и прокруткой большой ленты.
4.2. Программно рисуем и выводим имена и кнопочки только видимой области списка. При нажатии кнопок вверх/вниз все элементы сдвигаем вверх/вниз, одну строчку вверху/внизу удаляем, новую вверху/внизу добавляем. Т.е. реализуем прокрутку полностью самостоятельно программно, это устраняет недостаток первого метода. Размеры списка не играют в этом случае никакой роли, но придется больше программировать.
4.3. Делаем список в виде HTML текста и вставляем его в обычный TextField. Здесь обязательно использование Flash 7.0, т.к. только он умеет вставлять картинки в HTML тексте, или же отказаться от картинки и сделать кнопку привата текстом.
4.4. Изменить интерфейс. Например, сделать только список имен, при этом: один клик по имени — обычное сообщение, второй клик — приватное.

Реализуем последний способ, т.к. один из первых двух частично нам придется делать при отображении самого окна чата.
Существенно переделывается серверный скрипт, добавляем поддержку нескольких коннектов, проверку имен, и т.п. Здесь не привожу код, кому интересно посмотрит его в архиве.
Во Flash, вставляем проверку на вводимое имя. Какие должны быть ограничения:
нельзя использовать в имени символы: » ‘
Использование этих символов дает возможность послать любую команду к серверному скрипту. Например, в качестве имени пишем: «asd» />»
нельзя использовать в имени символы [ ]
т.к. при обращении к кому-либо в чате мы должны написать to [имя], то если в имени будут содержаться квадратные скобки, возникнут проблемы с интерпретацией строки.
нельзя использовать в имени запятую, т.к. списки имен мы передаем через запятую.
проверить длину имени. Например, ограничим минимально 3 максимально 16 символов. (ограничение по максимуму чтобы не делать горизонтальную прокрутку в списке имен)
нельзя в имени использоваться два двоеточия.
Т.к. смайлики в чат добавляются в виде :смайлик: , то если чье-то имя будет содержать такую же последовательность возникнут проблемы у интерпретатора.
нельзя чтобы имя начиналось или заканчивалось пробелом.
Это создаст возможность подделки имени, например, в чате есть:
Вася
и входит
Вася
отличие от первого только в том, что в конце стоит пробел. Это может создать определенные проблемы.
нельзя чтобы имя содержало два пробела подряд.
Причина аналогичная предыдущему ограничению, например, есть в чате:
Вася Пупкин
и входит:
Вася Пупкин
вместо одного пробела, использовано два. На глаз это трудно заметить.
нельзя использовать в имени русские и английские символы одновременно.
Причина все та же, подделка имени. Например, в чате есть:
Вася
и входит:
Ваcя
вместо русской «с» использована английская «си»
Не пропускать неудобочитаемые имена состоящие только из пунктуации. Требовать, чтобы в имени было хотя бы две буквы.

Вот такая функция у меня получилась:
// Проверка правильности ввода имени
function CheckLogin(login) {
var len = length(login);
if (len <= 0) { return «Введите ваше имя» }
else if (len 12) { return «Длина имени должна быть от трех символов до 12-ти» }
else if (login.charAt(0) == » » or login.charAt(len-1) == » «) {
return «Имя не может начинаться и заканчиваться пробелом»;
} else if (login.indexOf(» «)>=0) {
return «Имя не может содержать два пробела подряд»;
}
var i = login.indexOf(«:»);
if (i >= 0 and login.indexOf(«:», i+1)>0) {
return «В имени нельзя использовать два двоеточия»;
}
var stop_chars = Array(«», «‘», «[«, «]», «,», «\»»);
for (i in stop_chars) {
if (login.indexOf(stop_chars[i])>=0) return «В имени использован запрещенный символ: «+stop_chars[i];
}
// составим массив русских и английских букв
var rus_chars = «абвгдежзийклмнопрстуфхцчшщьыъэюяАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯЁё».split(«»);
var eng_chars = «abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ».split(«»);
// подсчитаем количество букв обоих алфавитов
var rus_count = 0, eng_count = 0;
for (i in rus_chars) { if(login.indexOf(rus_chars[i])>=0) rus_count++; }
for (i in eng_chars) { if(login.indexOf(eng_chars[i])>=0) eng_count++; }
if (rus_count>0 and eng_count>0) return «В имени разрешено использовать только буквы одного алфавита русского или английского. Нельзя смешивать.»;
if (rus_count<2 and eng_count<2) return «В имени обязательно должны содержаться хотя бы две буквы»;
return «»;
}
Добавим в место «список народа online» компонент List и назовем его listonline.
Далее необходимо переделать функцию onGetXML. Во-первых, возможно поступление сразу нескольких команд от сервера. Во-вторых, добавим обработку тегов OK и LIST.
Вот что получилось:
// Прием данных от сервера
function onGetXML(doc) {
var e = doc.firstChild;
while (e != null) { // Обход всего XML дерева
var s = e.nodeName;
if (s == «ERROR») { // Произошла ошибка, разорвать соединение и вывести текст ошибки
sock.close();
messages = e.attributes.TEXT; // Вывести сообщение об ошибке
} else if (s == «OK») { // Мы вошли в чат
online = true;
nextFrame(); // Переход на второй кадр, где и расположен интерфейс чата
} else if (online) { // Все остальные команды обрабатываются только, когда мы в online
if (s == «LIST») { // Пришел список народа
// Преобразуем в массив
var logins = e.firstChild.nodeValue.split(«,»);
listonline.removeAll(); // Очистим list
for (var i in logins) { // Добавляем все логины
listonline.addItem(logins[i]);
}
listonline.sortItemsBy(«label», «ASC»); // Сортируем по возрастанию
}
}
e = e.nextSibling; // переход к следующему элементу XML дерева
}
} С этим кодом мучался несколько часов. Не появляется список народа во флешке, хоть ты тресни! Приходит команда от сервера (atest1,test2,Петя,Random — 99), нормально обрабатывается флешкой, но список остается девственно чист. Причина, как оказалось, кроется в том, что переход на второй фрейм (кадр) происходит не сразу, как встретилась команда nextFrame() , а только по завершению выполнения функции onGetXML, чем это нам мешает? А тем, что компонента listonline на момент выполнения этой функции еще не существует, т.к. реально перехода на второй фрейм еще не было!
Как можно решить эту проблему?
Передавать только по одной команде от серверного скрипта (что приводит к его усложнению)
Сделать переход на второй фрейм(кадр) раньше, в момент посылки к серверу нашего логина, а не в момент прихода команды OK.
Заставить Flash реально перейти на второй фрейм(кадр) в нужном месте. Честно говоря не знаю как это сделать.
Отказаться от второго кадра, а разместить интерфейс чата на невидимом слое и включить его видимость по приходу команды OK.
Сделаем так. Функцию doLogin() удалить. Функция onSockConnect() выглядит теперь так:
function onSockConnect(success){
if (success){ // соединение установлено, переходим к процедуре передачи нашего логина
_global.mylogin = login.text; // Запоминаем введенный логин
nextFrame();
} else {
messages=»Не удалось установить соединение с сервером: «+serverName;
}
} А во втором слое, на входе (Layer 1: Frame 2) пишем:
var myXML = new XML(««);
sock.send(myXML);
stop(); Т.е. перед тем, как передать наш логин на сервер, запоминаем его в глобальной переменной (_global.mylogin) и переключаемся на второй фрейм(кадр). И вот только когда он полностью загрузится и выполнится код реально передающий наш логин на сервер, и по приходу оттуда ответа у нас уже все готово, для отображения списка народа online.

Пока изучал help по ActionScript нашел полезную вещь. Если на старте первого кадра (Layer 1: Frame 1) написать:
focusManager.defaultPushButton = enter_button;
то при вводе в поле имени, нажатие кнопки Enter равносильно нажатию мышкой на кнопку «вход».

Дефолтная функция
listonline.sortItemsBy(«label», «ASC»); сортирует список с учетом регистра, что не очень хорошо, поэтому заменяем ее на свой вариант:
listonline.sortItems(upperCaseFunc);
. . .
// Сортировка без учета регистра
function upperCaseFunc(a,b){
return a.label.toUpperCase() > b.label.toUpperCase();
} Теперь можно добавить поле для ввода текста, используем уже знакомый компонент TextInput, обзовем его entertext.
В actions списка имен (listonline) пишем функцию добавления текста в строку ввода при выборе имени человека в списке:
on (change) {
_root.AddName(selectedItem.label);
} В слой Scripts добавляем функцию добавления имени:
// Добавляет в строку ввода to [имя] или private [имя]
function AddName(s) {
var i;
if ((i=entertext.text.indexOf(«to [«+s+»]»))>=0) { // строка уже содержит обращение to [имя]
// меняем на private [имя]
entertext.text = entertext.text.substr(0, i)+»private»+entertext.text.substr(i+2);
} else
if ((i=entertext.text.indexOf(«private [«+s+»]»))>=0) { // строка уже содержит обращение private [имя]
// меняем на to [имя]
entertext.text = entertext.text.substr(0, i)+»to»+entertext.text.substr(i+7);
} else { // строка не содержит обращения по этому имени
// добавляем to [имя]
entertext.text = «to [«+s+»] «+entertext.text;
}
} Отлично. Теперь сделаем отправку введенного текста на сервер. Добавим справа от поля ввода кнопочку (компонент Button) с именем ok_button. Сделаем чтобы она была дефолтной, т.е. при нажатии кнопки Enter в поле ввода срабатывало on(click) этой кнопки. Для этого при открытии второго фрейма(кадра) (Layer 1: Frame 2) добавим строчку:
focusManager.defaultPushButton = ok_button; И теперь в actions кнопки пишем:
on (click) {
_root.SendTextToServer(_parent.entertext.text);// отправка введенного текста на сервер
_parent.entertext.text = «»; // очистка строки ввода
_parent.entertext.setFocus(); // поставим фокус на строку ввода
} И в слой Scripts добавляем функцию:
// Отправка введенного текста на сервер
function SendTextToServer(text :String) {
if (length(text)==0) return ; // выход, если нет текста
var myXML = new XML(); // новый XML
var myNode = myXML.createElement(«T»); // создаем тег «T»
// добавляем текст в тег
myNode.appendChild(myXML.createTextNode(«[«+_global.mylogin+»] «+text));
// добавляем созданный тег в основной XML документ
myXML.appendChild(myNode);
// отправляем на сервер
sock.send(myXML);
} Создавать XML надо именно так, а не: var myXML = new XML(««+text+»>«); т.к. в строке text могут содержаться кавычки и угловые скобки, которые надо самостоятельно заменить на: » > < а функция createTextNode делает это автоматически.

Теперь сделаем простой прием сообщений от сервера (обработка тегов ), чтобы проверить работоспособность всего вышеописанного.
Добавим «Text tool» тип «Dinamic text» на место «Основное окно с текстом чата». Шрифт _sans, размер 16, selectable, Render text as HTML, show border, имя «output_txt».
В функцию onGetXML добавляем простую обработку тега :
if (s == «T») { // Пришел текст в чат
var color = e.attributes.C;
var txt = e.firstChild.nodeValue;
output_txt.htmlText += txt+»
«; // добавляем текст и перевод строки
output_txt.scroll=output_txt.maxscroll; // прокручиваем в конец списка
} Выводим только текст без обработки на цвет, кнопки, ссылки и смайлики.



News Reporter