Выпуск 26. Апрель 2015
← Анонс конференции YAPC::Russia 2015 | Содержание | Промисы в Perl 6 →Работа с WebSocket в Perl
Рассмотрены несколько подоходов при работе с технологией WebSocket из Perl
Технология WebSocket в современных браузерах уже широко поддерживается и используется во многих интернет-приложениях. В Perl поддержка WebSocket появилась еще на этапе разработки самого протокола во фреймворке Mojolicious. Затем появился модуль общего назначения Protocol::WebSocket и несколько оберток вокруг библиотек на других языках. На данный момент из-за сложности самого протокола в основном все приложения используют Protocol::WebSocket.
Что такое WebSocket?
Технология WebSocket позволяет установить двусторонний постоянный канал между клиентом и сервером используя HTTP-протокол. Вначале клиент посылает заголовок Upgrade
и некоторый набор специальных заголовков, сервер отвечает своим набором заголовков и соединение устанавливается. Далее данные упаковываются в пакеты и отправляются. Кроме пакетов с данными существуют управляющие пакеты, например, для закрытия соединения.
GET /demo HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: example.com
Cookie: foo=bar; alice=bob
Origin: http://example.com
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Существует множество нюансов в зависимости от браузера, версии WebSocket, прокси-серверов, стоящих на пути запроса. Поэтому всегда стоит воспользоваться реализацией модуля Protocol::WebSocket, который обрабатывает все специальные случаи, скрывая подробности под простым интерфейсом.
Protocol::WebSocket позволяет написать как клиентскую, так и серверную часть. Рассмотрим низкоуровневую реализацию серверной части с помощью AnyEvent.
Вначале напишем html-файл. После подключения к серверу посылаем ему сообщение, а получив сообщение, выводим его в консоль.
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript" type="text/javascript">
websocket = new WebSocket('ws://localhost:3000');
websocket.onopen = function(evt) {
console.log('opened');
websocket.send('echo');
};
websocket.onclose = function(evt) { console.log('closed') };
websocket.onmessage = function(evt) {
console.log('message=' + evt.data);
};
websocket.onerror = function(evt) { console.log('error') };
</script>
<h2>WebSocket Test</h2>
Затем напишем серверную часть:
#!/usr/bin/env perl
use strict;
use warnings;
use AnyEvent::Socket;
use AnyEvent::Handle;
use Protocol::WebSocket::Handshake::Server;
use Protocol::WebSocket::Frame;
my $cv = AnyEvent->condvar;
my $hdl;
AnyEvent::Socket::tcp_server undef, 3000, sub {
my ($clsock, $host, $port) = @_;
my $hs = Protocol::WebSocket::Handshake::Server->new;
my $frame = Protocol::WebSocket::Frame->new;
$hdl = AnyEvent::Handle->new(fh => $clsock);
$hdl->on_read(
sub {
my $hdl = shift;
my $chunk = $hdl->{rbuf};
$hdl->{rbuf} = undef;
if (!$hs->is_done) {
$hs->parse($chunk);
if ($hs->is_done) {
$hdl->push_write($hs->to_string);
return;
}
}
$frame->append($chunk);
while (my $message = $frame->next) {
$hdl->push_write($frame->new($message)->to_bytes);
}
}
);
};
$cv->wait;
Все можно сильно упростить, если приложение будет запускаться в PSGI-среде. Protocol::WebSocket позволяет получить все необходимые заговки напрямую из окружения PSGI, например:
#!/usr/bin/env perl
use strict;
use warnings;
use AnyEvent::Handle;
use Protocol::WebSocket::Handshake::Server;
use Protocol::WebSocket::Frame;
my $psgi_app = sub {
my $env = shift;
my $fh = $env->{'psgix.io'} or return [500, [], []];
my $hs = Protocol::WebSocket::Handshake::Server->new_from_psgi($env);
$hs->parse($fh) or return [400, [], [$hs->error]];
return sub {
my $respond = shift;
my $h = AnyEvent::Handle->new(fh => $fh);
my $frame = Protocol::WebSocket::Frame->new;
$h->push_write($hs->to_string);
$h->on_eof(sub {});
$h->on_read(
sub {
$frame->append($_[0]->rbuf);
while (my $message = $frame->next) {
$h->push_write(Protocol::WebSocket::Frame->new($message)->to_bytes);
}
}
);
};
};
$psgi_app;
Запустив это приложение под Twiggy или Feersum или каким-либо другим PSGI-сервером на AnyEvent, получим аналогичное предыдущему примеру поведение.
Написание низкоуровневых WebSocket приложений может быть несколько утомительно, рассмотрим высокоуровный подход к написанию WebSocket-клиента. В модуле Protocol::WebSocket находится утилита wsconsole
, которая позволяет очень удобно тестировать или отлаживать WebSocket-серверы. Далее представлен ее упрощенный вид:
#!/usr/bin/env perl
use strict;
use warnings;
use AnyEvent;
use AnyEvent::Socket;
use AnyEvent::Handle;
use Protocol::WebSocket::Client;
my $cv = AnyEvent->condvar;
my $client = Protocol::WebSocket::Client->new(url => 'ws://localhost:3000');
my $ws_handle;
tcp_connect 'localhost', '3000', sub {
my ($fh) = @_ or return $cv->send("Connect failed: $!");
$ws_handle = AnyEvent::Handle->new(
fh => $fh,
on_eof => sub {
$cv->send;
},
on_error => sub {
$cv->send;
},
on_read => sub {
my ($handle) = @_;
my $buf = delete $handle->{rbuf};
$client->read($buf);
}
);
$client->on(
write => sub {
my $client = shift;
my ($buf) = @_;
$ws_handle->push_write($buf);
}
);
$client->on(
read => sub {
my $self = shift;
my ($buf) = @_;
print "message=$buf\n";
}
);
$client->connect;
};
my $stdin = AnyEvent::Handle->new(
fh => \*STDIN,
on_read => sub {
my $handle = shift;
my $buf = delete $handle->{rbuf};
$client->write($buf);
},
on_eof => sub {
$client->disconnect;
$ws_handle->destroy;
$cv->send;
}
);
$cv->wait;
В данном случае не нужно заботиться об обработке подключения, парсинге пакетов и прочего. Достаточно зарегистрировать нужные события и обработать их.
Сама утилита wsconsole
позволяет отправлять сообщения прямо из терминала, видеть полный формат пакетов, сообщать с какой версией подключаться. Так можно проверить, насколько хорошо и полно реализован WebSocket-сервер.
hello
> Writing
vvvvvvvvvv
[0000] 81 86 9E 17 E2 BC F6 72 8E D0 F1 1D .... ...r ....
^^^^^^^^^^
< Reading
vvvvvvvvvv
[0000] 81 06 68 65 6C 6C 6F 0A ..he llo.
Когда нет WebSocket
Не каждый браузер поддерживает WebSocket, иногда они специально отключаются, но все же приложение должно как-то работать. Есть несколько библиотек, которые имитируя интерфейс WebSocket при отсутствии последних переключаются на другие способы двусторонней передачи данных.
WebSocketJS
WebSocketJS представляет собой реализацию WebSocket с помощью технологии Flash. Исходный код библиотеки доступен на GitHub. Для полноценной работы Flash необходим запуск специального Policy-сервера отдельно или же можно воспользоваться специальным inline-сервером Fliggy, который на Flash-специфичный запрос формирует нужный ответ, а в остальном работает как Twiggy.
Socket.IO
Socket.IO в зависимости от возможностей браузера использует различные подходы для реализации двусторонней связи: Polling, Streaming, Htmlfile и другие. Socket.IO в Perl реализован PSGI-приложением PocketIO. К сожалению, недавно выпущенная версия 1.0 не поддерживается PocketIO, в связи с сильными изменениями протокола. На сегодняшний день использование Socket.IO в Perl затруднительно.
SockJS
SockJS был написан как альтернатива Socket.IO. Данная библиотека также поддерживает несколько альтернативных WebSocket-технологий. Для Perl есть реализация SockJS-perl. Преимущество SockJS — в наличии удобных утилит для тестирования в независимости от языка реализации самого протокола.
Автор советует использовать именно эту библиотеку на сегодняшний день.
Альтернативы WebSocket
Когда нет необходимости в дуплексной связи, например, для уведомления пользователя или обновления новостной ленты, можно воспользоваться технологией Server Side Events или EventSource. EventSource работает поверх HTTP и не требует значительной переработки серверной части. Реализация EventSource крайне проста и занимает несколько десятков строк, в этом можно убедиться, посмотрев на исходный код Plack-реализации EventSource.
← Анонс конференции YAPC::Russia 2015 | Содержание | Промисы в Perl 6 →