- 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/