Pull to refresh

Псевдо веб сокеты

Reading time 8 min
Views 12K
     Вдохновившись вот этой статьёй о Socket-соединениях в Веб-приложениях, решил сделать более-менее универсальный модуль с удобным интерфейсом, реализующий эту технологию.
     В этой статье под словом «сокет» имеется ввиду програмный интерфейс, который обеспечивает обмен данными между серверным и клиентским скриптами, с возможностью клиентского постоянно «слушать порт». Другими словами, как только что-то произошло на сервере, он может тут же сообщить об этом клиенту, и наоборот. Конечно же, в javascript нет возможности «слушать порты» и создавать полноценные сокеты, но зато у нас есть спички, изолента и пластилин, из которых можно смастерить какое-то подобие.
     Сначала я опишу примерный принцип действия этой системы, а затем, по традиции, приведу код примитивного чата построенного на её основе, с, конечно же, ссылкой. Хотелось бы увидеть своими глазами хабраэффект в действии. Ещё в конце будет ссылка на репозиторий с исходниками.


Принцип действия


     Основан на методе «длинных опросов» (long poll). Клиентский модуль посылает запрос серверному скрипту, который не закрывает его заранее заданное продолжительное время (время зависит от максимально возможного времени работы скрипта на сервере, например, 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 />)
}



и ссылка на собственно чат:
< она тут была, но больше её нет. вы уж простите =) >

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

     Эта система модулей «псевдо-веб-сокетов» только лишь бета, ещё, наверняка, очень сырая, но уже вполне рабочая. Я пока никаких багов не нашёл, надеюсь на тех, кто будет её использовать :)
Вот тут, на bitbucket.org доступны исходники. Пользуйтесь, форкайте, пишите багрепорты или кидайте камнями. С удовольствием все «камни» пособираю и постараюсь куда-нибудь их примотать изолентой, прилепить пластилином, подпереть спичками, или приклеить соплями.
Tags:
Hubs:
+23
Comments 19
Comments Comments 19

Articles