스물한번째 날: Mojolicious와 웹소켓 그리고 Redis

저자

@aanoaa - 홍형석, 사당동 펠프스, github:aanoaa

시작하며

Mojolicious은 인기있는 펄의 경량 웹 프레임워크입니다. 경량의 MVC 프레임워크임에도 불구하고 HTTP 클라이언트 및 서버의 거의 풀 스택을 구현한 웹 프레임워크로 지원하지 못하는 기능을 찾기가 더 어려울 정도인 잘 만들어진 모듈입니다. 웹소켓 역시 대표적인 예로 Mojolicious는 이 웹소켓을 아주 잘 지원합니다. 실시간으로 상태를 갱신한다던가 등의 동작을 단순 HTTP만으로 구현하려면 자바스크립트 및 웹응용의 컨트롤러에서 처리해야 할 내용이 꽤 많죠. 이번 기사에서는 Mojolicious에서 손쉽게 웹소켓을 다루는 방법을 소개합니다.

준비물

필요한 모듈은 다음과 같습니다.

Mojo::Redis2 모듈을 사용하려면 Redis 서버를 설치해야 합니다. 데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 패키지를 설치합니다.

$ sudo apt-get install redis-server

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Mojolicious Mojo::Redis2

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Mojolicious Mojo::Redis2

웹소켓 채팅 서버

Mojolicious 저장소위키에는 유용한 정보가 많은데, 그 중 Writing websocket chat using Mojolicious Lite 문서를 조금 변경해 보았습니다.

#!/usr/bin/env perl

use Mojolicious::Lite;

use Time::HiRes 'time';

get '/' => 'index';

my %clients;
websocket '/echo' => sub {
    my $self = shift;
    my $log  = $self->app->log;
    my $id   = time;

    $clients{$id} = $self->tx;
    $log->debug('[ws] client connected');
    $self->on(
        message => sub {
            my ( $self, $msg ) = @_;
            $log->debug("[ws] < $msg");
            for my $key ( keys %clients ) {
                $clients{$key}->send($msg);
                $log->debug("[ws] > ($id) $msg");
            }
        }
    );

    $self->on(
        finish => sub {
            $log->debug('[ws] client disconnected');
            delete $clients{$id};
        }
    );
};

app->start;

__DATA__

@@ index.html.ep

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Echo client</title>
    <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
    <script>
      $(function() {
        var sock;
        var port = location.port;
        sock = new WebSocket('ws://localhost:' + port + '/echo');
        sock.onopen = function(e) {
          console.log('Connected');
        }
        sock.onmessage = function(e) {
          $('<p>' + e.data + '</p>').appendTo('#msg');
        };

        $('#txt').keydown(function (e) {
          $this = $(this)
          if (e.keyCode == 13 && $this.val()) {
            sock.send($this.val());
            $this.val('');
          }
        });
      });
    </script>
  </head>
  <body>
    <h1>Mojolicious + WebSocket</h1>
    <div id="msg">
    </div>
    <div>
      <input id="txt" type="text" />
    </div>
  </body>
</html>

정말 간단한 예제죠? 놀랍지만 이 짧은 코드가 웹소켓 채팅 서버로써의 최소한의 코드인 셈입니다. Mojolicious의 모든 기능을 지원하려면 morbohypnotoad로 작성한 웹 응용을 실행해야 한다는 점에 유의하세요. Starman이나 Starlet으로는 Mojolicious의 웹소켓 기능이 제대로 동작하지 않습니다.

웹소켓 서버를 구동하기 위해 명령줄에서 다음 명령을 실행하세요.

$ morbo echo

자원의 공유

앞서 살펴본 예제는 마치 잘 동작하는 것처럼 보이지만 사실 서버로 띄운 웹 응용이 하나일 때, 즉 워커 프로세스가 하나 일 때만 정상적으로 동작합니다. morbo는 하나의 워커로 동작하는 비동기 서버이기 때문에 문제가 없지만 hypnotoad처럼 여러개의 워커를 띄울 수 있는 비동기 서버일 경우 메세지를 받은 워커에 연결된 클라이언트에게만 전달하기 때문입니다.

이런 자원 공유 문제를 해결하는 여러가지 방법이 있겠지만, 그 중 Redis의 Pub/Sub 모델을 활용하면 너무나도 간단하게 문제를 해결할 수 있습니다. 메세지를 받은 워커가 출판(publish)하고 이를 구독(subscribe)하는 워커가 메세지를 받아서 연결되어있는 클라이언트에게 전달하면 되겠죠!

Redis를 사용한 예제는 다음과 같습니다. 여러 개의 사용하기 위해 hypnotoad 관련 설정에서 띄울 워커 개수를 5개를 설정합니다. __DATA__ 섹션은 앞선 예제와 동일하므로 생략합니다.

#!/usr/bin/env perl

use Mojolicious::Lite;

use Mojo::Redis2;
use Scalar::Util;
use Time::HiRes 'time';

my $config = plugin 'Config' => {
    default => {
        hypnotoad => {
            listen  => [ "http://*:5000" ],
            workers => 5,
        },
    },
};

my $REDIS_CHANNEL = 'echo';

helper redis => sub { shift->stash->{redis} ||= Mojo::Redis2->new; };

get '/' => 'index';

websocket '/echo' => sub {
    my $self = shift;

    my $log  = $self->app->log;
    my $name = time;

    $log->debug('[ws] client connected');
    $self->inactivity_timeout(60);    # 60 seconds
    Scalar::Util::weaken($self);

    $self->on(
        message => sub {
            my ( $self, $msg ) = @_;

            $log->debug("[ws] < $msg");
            $self->redis->publish( $REDIS_CHANNEL => $msg );
        }
    );

    $self->on(
        finish => sub {
            my ( $self, $code, $reason ) = @_;

            $log->debug("[ws] client disconnected with status $code");
            delete $self->stash->{redis};
        }
    );

    $self->redis->on(
        message => sub {
            my ( $redis, $message, $ch ) = @_;

            return if $ch ne $REDIS_CHANNEL;
            return unless $self;
            $log->debug("[ws] > ($name) $message");
            $self->send($message);
        }
    );

    $self->redis->subscribe(
        $REDIS_CHANNEL => sub {
            my ( $redis, $err ) = @_;

            $log->error("[REDIS ERROR] subscribe error: $err") if $err;
        }
    );
};

app->start;

웹소켓 서버를 구동하기 위해 명령줄에서 다음 명령을 실행하세요.

$ hypnotoad -f echo-redis

정리하며

웹소켓Redis에 대한 자세한 부분을 설명한 것은 아닙니다만, 펄을 사용하면 이런 느낌으로, 이 정도의 코드로 웹에서 비동기 프로그램이 가능하고, 골치아픈 자원 공유의 문제를 손쉽게 해결할 수 있다는 것을 이해하기만 해도 큰 수확이라고 생각합니다. 묵묵히 펄을 사용해서 웹 프로그래밍을 하는 몽거스 분들에게 조금이나마 도움이 되었으면 합니다. :)

blog comments powered by Disqus