- PVSM.RU - https://www.pvsm.ru -
Вдохновившись вот этой статьёй [1] о Socket-соединениях в Веб-приложениях, решил сделать более-менее универсальный модуль с удобным интерфейсом, реализующий эту технологию.
В этой статье под словом «сокет» имеется ввиду програмный интерфейс, который обеспечивает обмен данными между серверным и клиентским скриптами, с возможностью клиентского постоянно «слушать порт». Другими словами, как только что-то произошло на сервере, он может тут же сообщить об этом клиенту, и наоборот. Конечно же, в javascript нет возможности «слушать порты» и создавать полноценные сокеты, но зато у нас есть спички, изолента и пластилин, из которых можно смастерить какое-то подобие.
Сначала я опишу примерный принцип действия этой системы, а затем, по традиции, приведу код примитивного чата построенного на её основе, с, конечно же, ссылкой. Хотелось бы увидеть своими глазами хабраэффект в действии. Ещё в конце будет ссылка на репозиторий с исходниками.
Основан на методе «длинных опросов» (long poll) [2]. Клиентский модуль посылает запрос серверному скрипту, который не закрывает его заранее заданное продолжительное время (время зависит от максимально возможного времени работы скрипта на сервере, например, 20 — 25 секунд). Если за это время ничего не произошло, скрипт сообщает об этом клиенту и прекращает работу. Клиент, получив такое сообщение, сразу создаёт новый запрос. Всё это продолжается до тех пор пока на сервере не произойдёт какое-то событие интересующее клиента, серверный скрипт сразу запускает некую заранее определённую вашу функцию, которая формирует ответ клиенту в виде хеша, отдаёт её обратно, и ответ тут же отправляется клиенту. При этом клиент может сам инициировать какое-либо событие и отправить информацию на сервер, об этом событии сразу узнают остальные клиенты «слушающие» этот порт.
Система реализована в виде двух подключаемых модулей. Один, клиентский, конечно же, на javascript. Второй, серверный, написан на perl. Названия собственно модулей, и всех экспортируемых ими свойств и методов, переменных и функций навеяны, показавшейся мне забавной, аналогией-ассоциацией с меломаном (клиентский модуль) слушающим патефоны в разных комнатах ("портах"), как только меломан слышит что в какой-то комнате сменили пластинку — сообщает об этом какой-то вашей заранее определённой функции. ( все эти синонимы-анологии несмотря на мои титанические усилия навести порядок, хаотично заменяют друг-друга во всём следующем тексте и в комментах к коду, так что стоит их хотя бы примерно запомнить :)
Подключается как-то так:
<script type="text/javascript" src="MWS_meloman.js"></script>
Запускается «прослушивание» функцией Listen, ей передаётся один параметр — строка в формате
<имя функции-обработчика> : <номер порта или диапазон портов>
функция-обработчик запустится как только в этот порт что-то придёт, и это «что-то» сразу же будет передано ей в виде объекта ( свойство: значение, …).
Например,
meloman.Listen('handler1:5')
// или
meloman.Listen('handler1:5-7')
// или даже так
meloman.Listen('handler1:5-7; handler2 : 8-10; handler3 : 11-20');
Если зачем-либо необходимо остановить прослушивание, Listen надо передать пустую строку:
// так
meloman.Listen('');
// или так:
meloman.Listen();
// или, вообще, как угодно :)
Если что-то, о чём небходимо «знать» серверу, и тем кто слушает этот порт, произошло у клиента, то он «упаковывает» это «что-то» в объект и передаёт его функции Change, указав первым параметром «порт».
Например:
var some = { 'name' : 'Mr.Smith', 'message' : 'Find Neo'}
meloman.Change( 5, some );
это тут же отправляется на сервер и передаётся вашей perl-функции, которая знает что с этим делать, в виде хеша.
Ещё есть несколько свойств-настроек:
minReconnectTime — время в милисекундах, чаще которого нельзя отправлять запросы серверу,
reconnectTime — время в милисекундах после которого отправляется новый запрос серверу,
waitingTimeOut — максимальное время ожидания ответа от сервера,
waitingTimeOutHandler — функция, которая обрабатывает ситуацию, когда в течении waitingTimeOut не пришло вообще никакого ответа от сервера,
connectionErrorHandler — функция, которая обрабатывает ошибки сервера ( если ответ от сервера не 200 OK ),
routeToChange — путь к серверному скрипту, которому будет передаваться объект функцией Change,
routeToListen — путь к серверному скрипту, который будет «слушать» заданный порт ( порты ), и передавать, в случае чего, информацию клиенту,
ignoreMyChanges — если false, то изменения сделанные вами функцией Change будут восприняты вами же как новое событие на сервере,
из всех этих настроек обязательными являются только routeToChange и routeToListen, у остальных либо есть дефолтные настройки, либо они не важны для корректной работы.
Модуль Patefon.pm cкачивается, копируется куда-нибудь, например, в ./libs и подключается:
use lib "./.libs";
use Patefon;
Модуль экспортирует функции &change_the_plate и &listen_the_plate и хеш настроек %patefons_knobs. ( Сейчас бета-версия экспортирует чуть больше ( для отладки ), но это будет исправлено в следующей версии. )
Я рекомендую на сервере делать два отдельных скрипта, один для прослушивания, второй для «приёма» информации от клиента. Хотя никто не будет против, если всё будет запихано и в один скрипт.
Тот, который «слушает» использует функцию &listen_the_plate, она запускается без параметров. Перед запуском необходимо указать функции-обработчики для всех необходимых портов.
$patefons_knobs{handlers}{<номер порта>} = <ссылка на функцию-обработчик>;
Например так:
$patefons_knobs{handlers}{1} = &handler_1;
Эта функция-обработчик должна быть готова принять первым параметром номер комнаты-порта в которой что-либо изменилось, и вторым ( если он ей нужен ) номер новой играющей пластинки. Что-то со всем этим ( и, возможно, не только с этим ) сделать, и вернуть хеш, который надо отправить клиенту.
Перед всем этим необходимо указать путь к папке с «комнатами-портами»:
$patefons_knobs{path_to_rooms} = './.rooms/';
в этой папке для каждого порта должна быть одноимённая папка, в каждой такой папке должен быть файл door (неважно что там записано, вполне сойдёт и 'ничего') он используется для блокировки «комнаты-порта», в момент, когда что-то изменяется функцией &change_the_plate (в момент, когда на сервер пришло что-то от какого-либо клиента).
примерная схема каталогов ./.rooms:
./.rooms/
./1/
./door
./2/
./door
и т.д.
Скрипт, который слушает использует функцию &change_the_plate. Перед её запуском необходимо указать путь к «комнатам-портам»:
$patefons_knobs{path_to_rooms} = './.rooms/';
и функции-обработчики:
$patefons_knobs{ChngHandlers}{<порт>} = <ссылка на функцию-обработчик>;
функции обработчику будет передана ссылка на хеш, пришедший от клиента.
после, того как она с ним что-то сделает она должна вернуть значение, которое может быть интерпретировано как true, например 1.
Ещё важно, что все передаваемые хеши/объекты должны быть одномерными.
// Например, такой можно:
var some = {
'name' : 'Mr.Smith',
'message' : 'Find Neo!',
'time' : 'Now!'
};
// а такой нельзя:
var some = {
'name' : 'Mr.Smith',
'action' : {
'message' : 'Find Neo!',
'time' : 'Now!'
}
};
В хеше настроек, кроме указания обработчиков и путей к «комнатам», можно покрутить такие ручки:
patefons_knobs{sample_rate} — время в секундах, через которое будет опрашиваться состояние порта, по умолчанию 1.
patefons_knobs{maxSleeping} — время в секундах, через которое прослушивающий скрипт завершает свою работу по умолчанию 20.
Существует массив patefons_knobs{errors} в который складываются все ошибки, функция модуля, которая выполнилась без ошибок, возвращает 1, с ошибками — 0. Это можно использовать, например, для записи ошибок в лог. Вот так, например:
unless ( &change_the_plate ) {
open LOG, ">>log";
$" = "n";
print LOG qq(ERRORS: @{$patefons_knobs{errors}});
}
Итак, ниже обещанный код примитивного чата. Заметьте, что ( без учета всяких свистелок в виде автоскроллинга и звука ) javascript'a там всего 16 строчек ( а если бы использовались дефолтные настройки, то и, вообще, 14 ).
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>чатиус примитивиус</title>
<link rel="stylesheet" href="chat_style.css" type="text/css" media="screen"/>
<script type="text/javascript" src="MWS_meloman.js"></script>
<script>
var mayScroll = true;
meloman.ignoreMyChanges = true;
meloman.routeToListen = 'ls.pl';
meloman.reconnectTime = 25000;
meloman.routeToChange = 'ch.pl';
meloman.Listen('j3:1');
function j3 ( a ) {
var string = '';
for ( var Name in a ) { string += Name + "t" + a[Name] + "n" }
document.getElementById('chat').innerHTML += a['message'];
playsound();
scrollchat();
};
function send_message () {
var banderol = {};
banderol['message'] = document.getElementById('message').value;
banderol['user'] = document.getElementById('user').value || 'Anonymous';
banderol['color'] = document.getElementById('color').value || '#333333';
if ( /S+/.test( banderol['message'] ) && banderol['message'].length < 500 )
{ meloman.Change( 1, banderol) }
document.getElementById('message').value = '';
}
function scrollchat () {
if ( mayScroll ) document.getElementById('chat').scrollTop = 9999;
}
function playsound () {
if ( document.getElementById('need_sound').checked ) {
document.getElementById("snd").volume = 0.4;
document.getElementById("snd").play();
}
}
</script>
</head>
<body>
<div id="settings" >
<span class="params">имя: <input type="text" size="20" id="user" autofocus /></span>
<span class="params">цвет: <input type="color" value="#00aa00" id="color" /></span>
<span class="params">звук: <input type="checkbox" id="need_sound" /></span>
</div>
<div id="cont">
<div id="chat" onmouseover="mayScroll=false;" onmouseout="mayScroll=true"></div>
<form onsubmit="send_message();return false;">
<div id="message_cont">
<input id="message" type="text" autocomplete="off" value="" spellcheck="false" />
</div>
</form>
</div>
<audio id="snd">
<source src="beep.ogg" type="audio/ogg; codecs=vorbis">
<source src="beep.mp3" type="audio/mpeg">
</audio>
</body>
</html>
И серверная часть:
«слушающий» скрипт:
#!/usr/bin/perl
use strict;
use warnings;
use lib "./.libs";
use Patefon;
$patefons_knobs{handlers}{1} = &j_1;
$patefons_knobs{path_to_rooms} = './.rooms/';
$patefons_knobs{maxSleeping} = 20;
listen_the_plate();
sub j_1 {
my $new = $_[1];
my $old = ( $new - $_[0] ) < 5 ? $_[0] : ( $new - 5 );
my $unreaden_messages;
for ( my $i = ++$old; $i <= $new; $i++ ) {
open F, "<utf8", "./general_chat/$i" or next;
$unreaden_messages .= <F>;
}
my %hash = ( 'message' => $unreaden_messages );
return %hash;
}
и «меняющий пластинки»:
#!/usr/bin/perl
use strict;
use warnings;
use lib "./.libs";
use Patefon;
$patefons_knobs{path_to_rooms} = './.rooms/';
$patefons_knobs{ChngHandlers}{1} = &j_1;
unless ( &change_the_plate ) {
open LOG, ">>log";
$" = "n";
print LOG qq(ERRORS: @{$patefons_knobs{errors}});
}
sub j_1 {
my ( $room, $plate, $banderol ) = @_;
return 0 if ( ${$banderol}{message} eq '' );
unless ( ${$banderol}{color} =~ /^#[0-9a-f]{3}$|^#[0-9a-f]{6}$/i ) { ${$banderol}{color} = '#FE5590' }
# shield < and >
for ( ${$banderol}{message}, ${$banderol}{user} ) { s/</</g; s/>/>/g }
open F, ">", "./general_chat/$plate";
print F qq(<span class="name" style="color:${$banderol}{color};">${$banderol}{user}</span>: <span class="mess">${$banderol}{message}</span><br />)
}
и ссылка на собственно чат:
surr.name/chat/ [3]
Помимо игрушечных чатиков, этим модулям можно найти, думаю, много разных применений, я, например, всё это делал для многопользовательской админ-панели.
Эта система модулей «псевдо-веб-сокетов» только лишь бета, ещё, наверняка, очень сырая, но уже вполне рабочая. Я пока никаких багов не нашёл, надеюсь на тех, кто будет её использовать :)
Вот тут [4], на bitbucket.org доступны исходники. Пользуйтесь, форкайте, пишите багрепорты или кидайте камнями. С удовольствием все «камни» пособираю и постараюсь куда-нибудь их примотать изолентой, прилепить пластилином, подпереть спичками, или приклеить соплями.
Автор: pikko
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/21380
Ссылки в тексте:
[1] этой статьёй: http://habrahabr.ru/post/41223/
[2] «длинных опросов» (long poll): http://javascript.ru/ajax/comet/long-poll
[3] surr.name/chat/: http://surr.name/chat/
[4] тут: https://bitbucket.org/surr/mock-web-socket/src/d54a3bb52d31664d923dabee598eae5be2b0fbc0/?at=master
[5] Источник: http://habrahabr.ru/post/160679/
Нажмите здесь для печати.