Выпуск 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
Нас уже 1393. Больше подписчиков — лучше выпуски!

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