Seongsiks

Being A DevOpser. Powered by
Obtvse, highlight.js, theme toc Creative Commons License
Seongsiks Twitter Github Email
DevOps Ruby On Rails Chef Projects Misc Movies & Drama ME

Rails4.0 Live Chat Example

Overview

rails가 4.0으로 버전업하면서 저의 가장 큰관심을 끌었던 기능중에 하나인 ActionController::Live기능을 써보기 위해서 간단한 채팅 프로그램을 만들었습니다. 소스는 [여기]서 다운 받을 수 있습니다. Running Example은 http://chat.devopser.me:3030에서 확인할 수있습니다.

제약 사항

  • IE, FireFox에서는 client에서 사용한 EventSource가 호환되지 않아 작동하지 않습니다.
  • Puma를 사용했는데 제 서버의 스펙상 max thread를 16으로 해놓았습니다. 따라서 15명까지만 채팅이 가능합니다.

Redis 설치

가장 간단하게 채팅을 구현하기 위해서 redis의 pub/sub기능을 이용하기로 했습니다. 여기에 eventMachine도 같이 쓸 수 있겠지만 테스트 용으로 너무 과한것 같아 최대한 간단히 하였습니다. Redis설치는 너무나 간단합니다.

$ wget http://redis.googlecode.com/files/redis-2.6.12.tar.gz
$ tar xzf redis-2.6.12.tar.gz
$ cd redis-2.6.12
$ make

여기까지 하고, 설치 디렉토리에 redis.conf파일을 수정합니다 .

 daemonize yes # 데몬으로 돌리고

 maxmemory 64mb # 제 서버가 너무 딸려서.. ㅠ.ㅠ

위에 두가지만 변경해주고 나머지는 모두 디폴트!

$ (redis 설치 디렉토리)/src/redis-server  (redis 설치 디렉토리)/redis.conf

물론 더 깔끔하게 실행시키는 방법도 있지만, path등록하고 인스톨하기 귀찮아서 그냥 뒀습니다. 위 명령을 실행시키면 redis 설치 완료!!

코딩!

이제 준비물이 다 갖추어 졌습니다. 이제 채팅창을 보여준 후, stream을 연결합니다. 채팅장에 메시지를 입력하면 이를 받아서 redis에 publish합니다. 그럼 redis를 subscribe하고 있는 컨트롤러가 이를 받아 메시지를 연결되어 있는 모든 클라이언트에게 보내 주는 겁니다. 간단하죠?

Routing

# config/routes.rb
Rails4Live::Application.routes.draw do
  get  '/chatroom'  => 'chat#chatroom'
  post '/message'   => 'chat#message'
  root :to => 'chat#index'
end

Routing은 간단합니다.

  1. 채팅창이 보일 root
  2. streaming을 해줄 /chatroom
  3. 메시지를 받아줄 POST message액션

이렇게 세개만 만듭니다.

ChatController

이제 Controller를 만듭니다.

class ChatController < ApplicationController
  include ActionController::Live

여기에 있는 이 라인으로 4.0에서 새롭게 쓸 수 있는 Live streaming기능을 사용할 수 있게됩니다. 아시겠지만 include 메소드는 다른 클래스의 상속 받는 메소드입니다.

  def index        
    respond_to do |format|
       format.html
    end
  end

설명할 필요도 없겠죠?

# 메시지를 받아 redis에 publish 해주는 액션 
def message
  begin
    redis = Redis.new
    #publish( 채널명, 데이터)
    if redis.publish('chatroom', params['message'])
      render json: { result: 'ok'}.to_json
    else
      render json: { result: 'error'}.to_json
    end
  rescue  StandardError => msg
    Rails.logger.fatal "Redis publish exception occur"
    Rails.logger.fatal  msg
    render json: { result: 'error'}.to_json
  ensure
      redis.quit
  end
end

# redis를 subscribe하고 있다가 메시지가 있으면 stream에 써주는 액션
def chatroom
  begin
    #반드시 데이터를 streaming하기전에 Content-Type을 선언해야합니다.
    response.header['Content-Type'] = "text/event-stream"
    redis = Redis.new
    redis.subscribe('chatroom') do |on|
      on.message do |event, data|
        response.stream.write("data: #{data}\n\n")
      end
    end
 # streaming이 끊어졌을때 IOError가 발생합니다. 
 # 이를 잡아서 redis랑 끊어주기 - 
 # 그런데 실제로 잘 안끊어짐.. redis.unsubscribe한다음 끊어야하나..?
  rescue IOError
    Rails.logger.fatal IOError.inspect
    redis.quit
  ensure
    # 까먹지 말고 꼭 닫아주기
    response.stream.close
  end
end

Response.stream.write()을 할때 반드시 \n\n으로 라인피드를 넣어줘야 됩니다. 이것을 몰라서 저는 엄청나게 시간을 잡아 먹었어요.

View

View 별로 중요한 부분이 아니니 대충 넘어가고

Javascript

# File Name : app/assets/javascript/chat.js

# Chat stream에 연결
function chatroom_connect() {
  var source = new EventSource('/chatroom');  // chat stream에 연결

  //메시지를 받으면
  source.addEventListener("message", function(event) { 
    $('#chat_window').append("\n"+event.data);
    // 메시지가 많아지면 자동 스크롤
    $('pre').scrollTop($('#chat_window').height()-310); 
  });
}
# 메시지 보내기
function message_send(){
     $.post('/message', {'message':$("#chat").val()});
  $("#chat").val("");
}

$(document).ready(chatroom_connect);
$(document).ready(function() {
  $('#message_send').bind('click', message_send); //send버튼 누르면 발송
  $(document).bind('keypress', function(event){
    if(event.keyCode == 13){ //엔터키 치면 메시지 발송
      message_send();
    }
  });
});

여기 까지가 대충 중요한 부분입니다. 그러나 제가 이걸 만들면서 삽질했던 부분들을 공유합니다.. 정말 삽질했는데, 눈물을 머금고 공개합니다.

삽질 Tips

developments.rb, production.rb

Rails 4.0이 되면서 이 환경 설정 파일이 많이 바뀌었습니다. 특히 Streaming을 하기 위해서 커넥션을 keep-alive상태로 해주기 위해서는 PUMA, Thin, Rainbow들과 같은 Thread기반 서버들을 사용해야 합니다. 그리고 이 서버들은

cache_class = true

위 옵션이 없으면 thread가 늘어나지 않습니다. Puma의 thread설정을 0:16으로 해놓아도 1개만 만들고 더 이상 늘어나지 않습니다. 그래서 development.rb에 위 옵션이 false로 되어 있어 Live기능을 제대로 테스트해볼 수 없습니다. 결국 결론은 developement.rb에도 cache_class를 true 로!!(물론 개발하는게 엄청 불편해집니다.) 다되면 false로 바꾸고 다른 feature들을 개발하세요

Gemfile Assets group

group :assest do 
  gem 'therubyracer', '~> 0.11'
  gem 'sass-rails', '~> 3'
  gem 'coffee-rails', '~> 3'
  gem 'uglifier', '~> 1'
end

이 assets group은 production 환경에서는 로드되지 않습니다. Thread Safty관련 exception이 production에서 발생한다면 group으로 묶어 놓은것을 풀어 놓으세요.

Thread + connection pool

Puma는 리퀘스트가 몰리면 지정해놓은 max thread까지 새로운 thread를 스폰합니다. 그런데 주의할것은 DB connection은 thread 하나당 하나씩이 할당된다는 것입니다. Thread를 0:16으로 해놓고 connection pool를 5로 잡아놓으면 스레드를 6개째 스폰할때 Connection Full exception이 발생합니다!!

nginx + puma

nginx도 이제 스트리밍을 받을 수 있지만, 저는 아직 어떻게 하는 줄 몰라서 running exaple의 url에 포트 번호가 있네요.. 포트 번호 있는 있는 url 창피하지만.. 시간이 없으니 일단 두겠습니다.

Redis publish

Redis에 publish 할때 매번 메시지가 왔을때 마다 Redis client를 생성하지 않고 글로벌하게 하나를 연결해 놓고 쓰면 redis에 커넥션을 맺는 시간을 절약 할 수 있습니다. 사용자가 굉장히 많다 그럼 redis client pool을 만들거나 rails 4.0의 새로운 기능인 queue을 사용하여 성능을 향상 시킬 수 있을겁니다. 저는 글로벌 redis publish용 client을 만들었습니다.

 # File name : config/initializers/redis.rb
 # Redis global connection for publish
require 'redis'
REDIS_CLIENT = Redis.new 

## at ChatController
def message
begin
  redis = REDIS_CLIENT
  if redis.publish('chatroom', params['message'])
     ...

이렇게 채팅을 간단히 구현해 보았습니다. 이 포스트를 보셨다면 Rails 4.0의 새로운 기능들을 찬찬히 보실 수 있는 다음 포스트를 확인해보세요^^ [Rails4-1 New Routes]

comments powered by Disqus
Back to Ruby On Rails