Выпуск 26. Апрель 2015

Анонс конференции YAPC::Russia 2015 | Содержание | Промисы в Perl 6

Работа с WebSocket в Perl

Рассмотрены несколько подоходов при работе с технологией WebSocket из Perl

Технология WebSocket в современных браузерах уже широко поддерживается и используется во многих интернет-приложениях. В Perl поддержка WebSocket появилась еще на этапе разработки самого протокола во фреймворке Mojolicious. Затем появился модуль общего назначения Protocol::WebSocket и несколько оберток вокруг библиотек на других языках. На данный момент из-за сложности самого протокола в основном все приложения используют Protocol::WebSocket.

Что такое WebSocket?

Технология WebSocket позволяет установить двусторонний постоянный канал между клиентом и сервером используя HTTP-протокол. Вначале клиент посылает заголовок Upgrade и некоторый набор специальных заголовков, сервер отвечает своим набором заголовков и соединение устанавливается. Далее данные упаковываются в пакеты и отправляются. Кроме пакетов с данными существуют управляющие пакеты, например, для закрытия соединения.

1
GET /demo HTTP/1.1
2
Upgrade: WebSocket
3
Connection: Upgrade
4
Host: example.com
5
Cookie: foo=bar; alice=bob
6
Origin: http://example.com
7
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
8
Sec-WebSocket-Version: 13
9
 
10
HTTP/1.1 101 Switching Protocols
11
Upgrade: websocket
12
Connection: Upgrade
13
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
 
 

Существует множество нюансов в зависимости от браузера, версии WebSocket, прокси-серверов, стоящих на пути запроса. Поэтому всегда стоит воспользоваться реализацией модуля Protocol::WebSocket, который обрабатывает все специальные случаи, скрывая подробности под простым интерфейсом.

Protocol::WebSocket позволяет написать как клиентскую, так и серверную часть. Рассмотрим низкоуровневую реализацию серверной части с помощью AnyEvent.

Вначале напишем html-файл. После подключения к серверу посылаем ему сообщение, а получив сообщение, выводим его в консоль.

<!DOCTYPE html>
1
<!DOCTYPE html>
2
<meta charset="utf-8" />
3
<title>WebSocket Test</title>
4
<script language="javascript" type="text/javascript">
5
    websocket = new WebSocket('ws://localhost:3000');
6
    websocket.onopen = function(evt) {
7
        console.log('opened');
8
        websocket.send('echo');
9
    };
10
    websocket.onclose = function(evt) { console.log('closed') };
11
    websocket.onmessage = function(evt) {
12
        console.log('message=' + evt.data);
13
    };
14
    websocket.onerror = function(evt) { console.log('error') };
15
</script>
16
<h2>WebSocket Test</h2>
 
 

Затем напишем серверную часть:

#!/usr/bin/env perl
1
#!/usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
6
use AnyEvent::Socket;
7
use AnyEvent::Handle;
8
 
9
use Protocol::WebSocket::Handshake::Server;
10
use Protocol::WebSocket::Frame;
11
 
12
my $cv = AnyEvent->condvar;
13
 
14
my $hdl;
15
 
16
AnyEvent::Socket::tcp_server undef, 3000, sub {
17
    my ($clsock, $host, $port) = @_;
18
 
19
    my $hs    = Protocol::WebSocket::Handshake::Server->new;
20
    my $frame = Protocol::WebSocket::Frame->new;
21
 
22
    $hdl = AnyEvent::Handle->new(fh => $clsock);
23
 
24
    $hdl->on_read(
25
        sub {
26
            my $hdl = shift;
27
 
28
            my $chunk = $hdl->{rbuf};
29
            $hdl->{rbuf} = undef;
30
 
31
            if (!$hs->is_done) {
32
                $hs->parse($chunk);
33
 
34
                if ($hs->is_done) {
35
                    $hdl->push_write($hs->to_string);
36
                    return;
37
                }
38
            }
39
 
40
            $frame->append($chunk);
41
 
42
            while (my $message = $frame->next) {
43
                $hdl->push_write($frame->new($message)->to_bytes);
44
            }
45
        }
46
    );
47
};
48
 
49
$cv->wait;
 
 

Все можно сильно упростить, если приложение будет запускаться в PSGI-среде. Protocol::WebSocket позволяет получить все необходимые заговки напрямую из окружения PSGI, например:

#!/usr/bin/env perl
1
#!/usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
6
use AnyEvent::Handle;
7
use Protocol::WebSocket::Handshake::Server;
8
use Protocol::WebSocket::Frame;
9
 
10
my $psgi_app = sub {
11
    my $env = shift;
12
 
13
    my $fh = $env->{'psgix.io'} or return [500, [], []];
14
 
15
    my $hs = Protocol::WebSocket::Handshake::Server->new_from_psgi($env);
16
    $hs->parse($fh) or return [400, [], [$hs->error]];
17
 
18
    return sub {
19
        my $respond = shift;
20
 
21
        my $h = AnyEvent::Handle->new(fh => $fh);
22
        my $frame = Protocol::WebSocket::Frame->new;
23
 
24
        $h->push_write($hs->to_string);
25
 
26
        $h->on_eof(sub {});
27
        $h->on_read(
28
            sub {
29
                $frame->append($_[0]->rbuf);
30
 
31
                while (my $message = $frame->next) {
32
                    $h->push_write(Protocol::WebSocket::Frame->new($message)->to_bytes);
33
                }
34
            }
35
        );
36
    };
37
};
38
 
39
$psgi_app;
 
 

Запустив это приложение под Twiggy или Feersum или каким-либо другим PSGI-сервером на AnyEvent, получим аналогичное предыдущему примеру поведение.

Написание низкоуровневых WebSocket приложений может быть несколько утомительно, рассмотрим высокоуровный подход к написанию WebSocket-клиента. В модуле Protocol::WebSocket находится утилита wsconsole, которая позволяет очень удобно тестировать или отлаживать WebSocket-серверы. Далее представлен ее упрощенный вид:

#!/usr/bin/env perl
1
#!/usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
6
use AnyEvent;
7
use AnyEvent::Socket;
8
use AnyEvent::Handle;
9
use Protocol::WebSocket::Client;
10
 
11
my $cv = AnyEvent->condvar;
12
 
13
my $client = Protocol::WebSocket::Client->new(url => 'ws://localhost:3000');
14
my $ws_handle;
15
 
16
tcp_connect 'localhost', '3000', sub {
17
    my ($fh) = @_ or return $cv->send("Connect failed: $!");
18
 
19
    $ws_handle = AnyEvent::Handle->new(
20
        fh     => $fh,
21
        on_eof => sub {
22
            $cv->send;
23
        },
24
        on_error => sub {
25
            $cv->send;
26
        },
27
        on_read => sub {
28
            my ($handle) = @_;
29
 
30
            my $buf = delete $handle->{rbuf};
31
 
32
            $client->read($buf);
33
        }
34
    );
35
 
36
    $client->on(
37
        write => sub {
38
            my $client = shift;
39
            my ($buf) = @_;
40
 
41
            $ws_handle->push_write($buf);
42
        }
43
    );
44
    $client->on(
45
        read => sub {
46
            my $self = shift;
47
            my ($buf) = @_;
48
 
49
            print "message=$buf\n";
50
        }
51
    );
52
    $client->connect;
53
};
54
 
55
my $stdin = AnyEvent::Handle->new(
56
    fh      => \*STDIN,
57
    on_read => sub {
58
        my $handle = shift;
59
 
60
        my $buf = delete $handle->{rbuf};
61
 
62
        $client->write($buf);
63
    },
64
    on_eof => sub {
65
        $client->disconnect;
66
 
67
        $ws_handle->destroy;
68
        $cv->send;
69
    }
70
);
71
 
72
$cv->wait;
 
 

В данном случае не нужно заботиться об обработке подключения, парсинге пакетов и прочего. Достаточно зарегистрировать нужные события и обработать их.

Сама утилита wsconsole позволяет отправлять сообщения прямо из терминала, видеть полный формат пакетов, сообщать с какой версией подключаться. Так можно проверить, насколько хорошо и полно реализован WebSocket-сервер.

hello
1
hello
2
> Writing
3
vvvvvvvvvv
4
[0000]   81 86 9E 17  E2 BC F6 72  8E D0 F1 1D                .... ...r ....
5
 
6
^^^^^^^^^^
7
< Reading
8
vvvvvvvvvv
9
[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
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье