@luzluna - Seoul.pm과 #perl-kr의 육아 전문 컨설턴트, 사회적 기업을 꿈꾸는 커피 매니아이자 백수.
비동기 웹서버의 유행들을 따라 펄에도 비동기 웹서비스를 제공할만한 좋은 방법들이 몇 가지 생겼습니다. 그 중 Tatsumaki는 Tatsuhiko Miyagawa씨께서 Tornado를 펄 버전으로 새로 구현한 프레임워크입니다.
비동기 웹서버에 대해 부정적으로 생각하지만(Larry Wall의 표현을 빌어 표현하자면 "Not For Human"), 웹 상에서 채팅이나 메신저같은 Long Polling 서비스를 구현하려면 마땅한 다른 방법도 없으니... 필요하면 배워야겠죠. ㅜ.ㅠ
Tatsumaki 소스를 다운받으면 eg
디렉터리 아래에 간단한 채팅 서버 예제가 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $ tree eg /chat eg /chat |-- app.psgi |-- static | |-- DUI.js | |-- jquery-1.3.2.min.js | |-- jquery.cookie.js | |-- jquery.ev.js | |-- jquery.md5.js | |-- jquery.oembed.js | |-- pretty.js | |-- screen .css | `-- Stream.js `-- templates `-- chat.html |
PSGI 어플리케이션으로 되어 있으며 모든 펄 코드는 app.psgi
에 집적되어 있습니다.
이 중, 먼저 main
패키지의 코드를 봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package main; use File::Basename; my $chat_re = '[\w\.\-]+' ; my $app = Tatsumaki::Application->new([ "/chat/($chat_re)/poll" => 'ChatPollHandler' , "/chat/($chat_re)/mxhrpoll" => 'ChatMultipartPollHandler' , "/chat/($chat_re)/post" => 'ChatPostHandler' , "/chat/($chat_re)" => 'ChatRoomHandler' , ]); $app ->template_path(dirname(__FILE__) . "/templates" ); $app ->static_path(dirname(__FILE__) . "/static" ); return $app ->psgi_app; |
Tatsumaki::Application를 생성하면서 처리할 URL 패턴과 각 패턴에 대한 핸들러를 추가해줍니다.
그런 다음 템플릿 경로(template_path
) 설정도 해주고 정적 파일(static_file
)을 처리하기 위한 설정도 추가해줍니다.
여기까지는 간단하죠?
처리할 URL 패턴 중, 먼저 /chat/($chat_re)
에 접근한다고 가정해 봅시다.
따라서 이번에는 ChatRoomHandler
를 보겠습니다.
1 2 3 4 5 6 7 | package ChatRoomHandler; use base qw(Tatsumaki::Handler); sub get { my ( $self , $channel ) = @_ ; $self ->render( 'chat.html' ); } |
그냥 chat.html
템플릿을 랜더링하고 있습니다.
ChatRoomHandler
핸들러에 제공된 URL 패턴은 정규표현식이었습니다.
이 정규표현식에 매치가 성공하면 해당 핸들러에 디스패치되고
매치를 통해 일치 변수($1
, $2
, $3
등)로 기억된 결과가
핸들러의 변수로 넘어갑니다. 이 경우에는 $channel
에
$chat_re
에 매치된 문자열이 넘어가겠네요.
핸들러에 HTTP 메소드명의 사용자 함수를 작성하면,
해당 메소드 요청에 대해 연결됩니다.
이 경우에는 get
함수를 정의하여 GET 메소드에 대해 템플릿을 랜더링하도록
하고 있습니다.
이번엔 ChatPostHandler
입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package ChatPostHandler; use base qw(Tatsumaki::Handler); use HTML::Entities; use Encode; sub post { my ( $self , $channel ) = @_ ; my $v = $self ->request->parameters; my $html = $self ->format_message( $v ->{text}); my $mq = Tatsumaki::MessageQueue->instance( $channel ); $mq ->publish({ type => "message" , html => $html , ident => $v ->{ident}, avatar => $v ->{avatar}, name => $v ->{name}, address => $self ->request->address, time => scalar Time ::HiRes::gettimeofday, }); $self -> write ({ success => 1 }); } sub format_message { my ( $self , $text ) = @_ ; $text =~ s{ (https?://\S+) | ([&<>"']+) } { $1 ? do { my $url = HTML::Entities::encode( $1 ); qq(<a target= "_blank" href= "$url" > $url </a>) } : $2 ? HTML::Entities::encode( $2 ) : '' }egx; $text ; } |
핵심적인 코드는 아래와 같이 채널 이름에 맞는 Tatsumaki::MessageQueue를 만드는 코드와
1 | $mq = Tatsumaki::MessageQueue->instance( $channel ); |
아래와 같이 메시지를 Queue에 쏘는 두 줄이 끝입니다.
1 2 3 4 5 6 | $mq ->publish({ type => "message" , html => $html , ident => $v ->{ident}, avatar => $v ->{avatar}, name => $v ->{name}, address => $self ->request->address, time => scalar Time ::HiRes::gettimeofday, }); |
채널명에 해당하는 메시지 큐에 채팅을 통해 전달받은 채팅 메시지를 전달하고 있습니다.
메시지 "message"
로 분류하고, 사용자명, 아바타, 시간, HTML로 랜더링된 메시지 등을 담았습니다.
이번에는 ChatPollHander
를 봅시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package ChatPollHandler; use base qw(Tatsumaki::Handler); __PACKAGE__->asynchronous(1); use Tatsumaki::MessageQueue; sub get { my ( $self , $channel ) = @_ ; my $mq = Tatsumaki::MessageQueue->instance( $channel ); my $client_id = $self ->request->param( 'client_id' ) or Tatsumaki::Error::HTTP->throw(500, "'client_id' needed" ); $client_id = rand (1) if $client_id eq 'dummy' ; # for benchmarking stuff $mq ->poll_once( $client_id , sub { $self ->on_new_event( @_ ) }); } sub on_new_event { my ( $self , @events ) = @_ ; $self -> write (\ @events ); $self ->finish; } |
방금 본 post
함수와 비슷합니다.
먼저 해당 핸들러는 __PACKAGE__->asynchronous(1);
을 통해 비동기 모드로 설정했습니다.
Tatsumaki::MessageQueue 인스턴스를 하나 만들고 $mq->poll_once
로 모든 메시지를 한꺼번에 대기합니다.
핸들러를 비동기 모드로 설정했기 때문에 핸들러 객체에 등록된 Writer 객체를 사용하는
write
과 finish
함수로 도착한 이벤트를 출력합니다. 해시 레퍼런스였던 메시지는 JSON으로 변환되어 전달됩니다.
ChatMultipartPollHandler
는 어떨까요?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package ChatMultipartPollHandler; use base qw(Tatsumaki::Handler); __PACKAGE__->asynchronous(1); sub get { my ( $self , $channel ) = @_ ; my $client_id = $self ->request->param( 'client_id' ) || rand (1); $self ->multipart_xhr_push(1); my $mq = Tatsumaki::MessageQueue->instance( $channel ); $mq ->poll( $client_id , sub { my @events = @_ ; for my $event ( @events ) { $self ->stream_write( $event ); } }); } |
이전과 거의 비슷합니다.
대신 멀티파트 multipart_xhr_push
를 한 줄 넣어 멀티파트 헤더를 추가해주고
연결을 끊지 않고 계속 poll 하기 위해 stream_write
로 이벤트를 전송합니다.
마지막으로 plackup을 통해서 Twiggy로 띄우면... 채팅 잘 되네요~
1 | $ plackup -s Twiggy app.psgi |
채팅만 하려니까 뭔가 심심해서 재미있는걸 해볼 수 있게 canvas를 추가해봅시다.
chat.html
에 아래와 같이 캔버스 한 줄을 추가합니다.
1 | < canvas id = "c" width = "200" height = "100" style = "border:1px solid" ></ canvas > |
그런 다음 아래와 같이 스크립트를 좀 추가해 줍시다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | function draw_dot(x,y) { var canvas = document.getElementById( 'c' ); var ctx = canvas.getContext( '2d' ); ctx.beginPath(); ctx.arc(x,y,5,0,Math.PI*2, true ); ctx.fillStyle = '#5555AA' ; ctx.fill(); ctx.stroke(); } $( function (){ $( '#c' ).mouseup( function (e) { var canoffset = $( '#c' ).offset(); var x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft - Math.floor(canoffset.left); var y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop - Math.floor(canoffset.top) + 1; draw_dot(x,y); $.ajax({ url: "/chat/<%= $channel %>/post" , data: { type: 'game' , x: x, y: y, text: 'g' }, type: 'post' , dataType: 'json' , success: function (r) { } }); }); var onGameEvent = function (e) { draw_dot(e.x, e.y); } $.ev.handlers.game = onGameEvent; }); |
마지막으로, game 타입의 메시지를 다루기 위해 서버 코드를 조금 수정합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package ChatPostHandler; use base qw(Tatsumaki::Handler); use HTML::Entities; use Encode; sub post { my ( $self , $channel ) = @_ ; for ( keys %{ $self ->request->parameters} ) { $self ->request->parameters->{ $_ } = decode( 'utf8' , $self ->request->parameters->{ $_ }); } my $v = $self ->request->parameters; my $html = $self ->format_message( $v ->{text}); my $mq = TatsumakiZeroMQ->instance( $channel ); if ( defined $v ->{type} && $v ->{type} eq 'game' ) { $mq ->publish({ type => "game" , html => $html , x => $v ->{x}, y => $v ->{y}, ident => $v ->{ident}, avatar => $v ->{avatar}, name => $v ->{name}, address => $self ->request->address, time => scalar Time ::HiRes::gettimeofday, }); } else { $mq ->publish({ type => "message" , html => $html , ident => $v ->{ident}, avatar => $v ->{avatar}, name => $v ->{name}, address => $self ->request->address, time => scalar Time ::HiRes::gettimeofday, }); } $self -> write ({ success => 1 }); } |
이제 캔버스에 클릭으로 점을 찍으면 상대편에게도 점이 찍히는게 보이죠? 로직을 좀 구현해넣으면 간단한 게임도 만들 수 있을 것 같고 그림을 공유하는 것도 될 것 같습니다.
그림 1. 완성된 채팅 서비스 (원본)
Articles by Seoul Perl Mongers
Illustrated by Hyunsu Park, Designed by Hojung Youn, Edited by Hojung Youn & Keedi Kim