English Version here
Antes de iniciar a implementação do chat, nos precisamos instalar as dependências do juggernaut, ele precisa das gems json e eventmachine instaladas, para instalar ambas basta executar o seguinte comando:
Agora estamos prontos para começar a brincar …
Primeiro, vamos criar uma aplicação rails e instalar o plugin juggernaut com os seguitnes comandos:
O Juggernaut usa um servidor externo implementando Flash XML Push, e precisamos configurar este servidor, então vamos editar o arquivo: config/juggernaut.yml
Coloquei aqui no blog apenas as configurações que precisei alterar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | PUSH_PORT: 8080 ... DEFAULT_CHANNELS: - "chat" ... PUSH_HELPER_HOST: "localhost" ... SECRET: "481516232342edededededed" ... LOGIN_GET_URL: "http://localhost:3000/session/login" LOGOUT_GET_URL: "http://localhost:3000/session/logout" ... SESSION_ID: "_chattest_session_id" ... BASE64: true |
Precisei alterar PUSH_PORT Por que no linux apenas o root pode ouvir em portas inferiores a 1024, e mesmo assim eu não acho que a porta 443 padrão seja uma boa escolha, ja que esta é a porta padrão do protocolo HTTPS.
A linhe com PUSH_HELPER_HOST precisa ser alterada para o mesmo nome do servidor que for utilizado para a acessar a aplicação, em desenvolvimento, localhost deve resolver, mas não esqueça de alterar esta configuração quando for fazer o deploy do seu chat.
LOGIN_GET_URL e LOGOUT_GET_URL são utilizadas para notificar a aplicação quando um usuário conecta ou desconecta, apenas o desconectar é realmente importante para nós.
A SESSION_ID precisa ser igual ao nome do cookie utilizado como marcador de sessão da aplicação, no rails 1.2.x fica no application controller, no rails 2.0 esta configuração vai para o enviroment.rb
e BASE64 é a configuração mágica que habilita o uso dos helpers do rails para geração do Javascript que vai ser utilizado.
Agora vamos começar a criar o layout da aplicação.
Crie um arquivo de nome pubic/stylesheets/public.css com o seguinte conteúdo:
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 | body { background-color: white; } #users { float: left; width: 200px; height: 400px; border-style: inset; overflow: auto; color: white; background-color: gray; } #dasd { height: 400px; margin-left: 5px; border-style: inset; overflow: auto; color: white; background-color: gray; } #controls { clear: both; padding: 0 0 0 0; height: 55px; vertical-align: top; border-style: inset; overflow: auto; color: white; background-color: gray; } |
e o arquivo: app/views/layouts/application.rhtml com o seguinte conteúdo.
1 2 3 4 5 6 7 8 9 10 | <html> <head> <title>Chat Test</title> <%= stylesheet_link_tag 'public' %> <%= javascript_include_tag :defaults %> </head> <body> <%= yield %> </body> </html> |
Com o layout pronto (sim, esta bem feio, mas eu não sou designer eu sou um desenvolvedor, então solicite ao designer da sua equipe a criação de um novo layout para este chat
) vamos criar o código fonte necessário e o banco de dados com estes 4 comandos:
Layout pronto, arquivos criados, agora só precisamos escrever o código da aplicação ![]()
Vamos começar editando o model OnlineUser (app/model/online_user.rb) para adicionar algumas validações, elas não são realmente necessárias, são resquícios da primeira tentativa de implementação do chat, mas mesmo assim mantive elas no código
1 2 3 4 | class OnlineUser < ActiveRecord::Base validates_presence_of :username, :session_id, :last_seen validates_uniqueness_of :username, :if => Proc.new {|user| user.online } end |
Agora a view principal da aplicação no arquivo:app/views/chat/index.rhtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- Register with Juggernault --> <%= listen_to_juggernaut_channels [:generic],session.session_id %> <!-- The Users List --> <div id="users"> <ul id="users_list"></ul> </div> <!-- The messages pane --> <div id="dasd"></div> <!-- The controls pane (login and send messages) --> <div id="controls"><%= render :partial => 'login' %></div> <!-- An util javascript to scroll the messages window --> <script type="text/javascript"> function scrollMessages(){ $('dasd').scrollTop = $('dasd').scrollHeight; } </script> |
Como podem ver é bem simples, apenas 3 DIVs, um javascript para fazer o scroll da tela de mensagens e um comando para inicializar o juggernaut no canal “generic” e utilizando o session_id como identificador individual do usuário.
A DIV de mensagens esta com o id dasd por que eu estava testando algumas possibilidades de conflito e esqueci de mudar o ID depois
Como pode ser visto na página index, precisaremos de um partial de nome “login” e vamos também precisar de um de nome “controls”, então vamos começar codificando o partial “controls”: app/views/chat/_controls.rhtml
1 2 3 4 5 6 | <% form_remote_tag( :url => { :action => :say }, :complete => "$('message').value = '';$('message').focus();" ) do %> <%= text_field_tag( 'message', '', { :size => 90, :id => 'message'} ) %> <%= submit_tag "Send" %> <% end %> |
Bastante simples, possui apenas uma tag remote_form_tag que fara um post para a action “say”, um campo de texto para a mensagem e um botão de submit, quando o formulário é enviado o foco volta para o campo message para que o usuário possa digitar a próxima mensagem …
E o partial de login: app/views/chat/_login.rhtml
1 2 3 4 5 6 7 | <%= "#{@message}<br/>" if @message %><% form_remote_tag(
:url => { :action => :login },
:complete => "$('username').value = ''",
:after => "$('login').disabled = true" ) do %>
<%= text_field_tag( 'username', '', { :size => 90, :id => 'username'} ) %>
<%= submit_tag "Join", :id => 'login' %>
<% end %> |
Bastante parecido com o anterior, mas faz o post para uma action de nome “login”, e o javascript agora desabilita o botão quando o formulário é enviado, e também possui uma mensagem que deve aparecer para o usuário informando que o apelido ja esta sendo utilizado.
Agora todas as views ja estão prontas, podemos passar para a codificação da lógica da aplicação.
Como podemos ver nas views até agora, precisaremos de um controller de nome “chat” com dois métodos: login e say
vamos analisar um pouco o código do chat controller: app/controllers/chat_controller.rb
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | class ChatController < ApplicationController #this method does not need to exist, but I like to see it here, it only needs to render the index.rhtml view def index end def login #creates a new OnlineUser record, this is used to store who are the users that are online now @user = OnlineUser.new @user.username = Juggernaut.html_and_string_escape params[:username] @user.session_id = session.session_id @user.online = true @user.last_seen = Time.now #if we can save, it means that there is no other user with the same nick online, so this user can join the chat if @user.save #let's save the username in the session for future reference session[:username] = @user.username #if there are online users, fill the users box for the new user know who is online @users = OnlineUser.find(:all, :conditions => ["online = true and id != ?", @user.id]) if @users.size >0 data = render_to_string(:update) do |page| @users.each {|u| page.insert_html :bottom, :users_list, %Q{<li id="user_#{u.username}">#{u.username}</li>} } end #send the javascript only to the new user Juggernaut.send_to(@user.session_id, data) end #create a javascript call to add the new user to the end of the online users list data = render_to_string(:update) do |page| page.insert_html :bottom, :users_list, %Q{<li id="user_#{@user.username}">#{@user.username}</li>} page.insert_html :bottom, :dasd, "<b>user #{@user.username} just joined the chat</b><br/>" end #add the new user to the chat channel Juggernaut.add_channel(@user.session_id, 'chat') #send the javascript to all users in the chat channel Juggernaut.send_data(data, 'chat') render(:update) do |page| page.replace_html 'controls', :partial => "controls" end else @message = 'This nick name is already in use, please choose another' render(:update) do |page| page.replace_html 'controls', :partial => "login" end end end def say #escape the message, that way the user can not harm others sending HTML ot JavaScript commands message = "#{session[:username]}: #{Juggernaut.html_and_string_escape(params[:message])}" #create a javascript to add the new message to the end of the messages screen and scroll the div to the bottom data = render_to_string(:update) do |page| page.insert_html :bottom, :dasd, "#{message}<br/>" page.call "scrollMessages" end #send the message to all users Juggernaut.send_data(data, 'chat') render :nothing => true end end |
O método say é bastante simples, então vamos começar analisando este:
Primeiro a mensagem é reconstruída adicionando o nome do usuário no inicio da mensagem enviada removendo todos os possíveis códigos HTML ou Javascript para garantir a segurança dos outros usuários.
Então é construido um Javascript utilizando o JavaScriptBuilder do rails e o método render_to_string para adicionar a nova mensagem no final da div “dasd” e fazer o scroll caso necessário.
Então este Javascript é enviado para todos os clientes que estão ouvindo o canal “chat”.
Agora um pouco sobre o método login:
Esta é quase toda a lógica que precisamos, a única parte que falta é remover um usuário da lista de usuários online de todos os participantes quando este fechar o browser ou deixar o chat por qualquer motivo, para isto vamos ao método logout do controller session: app/controllers/session_controller.rb
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 | class SessionController < ApplicationController #Called when a user disconnect (a refresh in the browser causes this to be called too) def logout #search for the user record using the session_id @u = OnlineUser.find_by_session_id(session.session_id) reset_session #if a user was found if @u username = @u.username #remove it from the database @u.destroy #remove from the online users list from all users, and tell others this user left the chat data = render_to_string(:update) do |page| page.remove "user_#{username}" page.insert_html :bottom, :dasd, "<b>User #{username} left the chat</b><br/>" end Juggernaut.send_data(data,'chat') end render :nothing => true end def login render :nothing => true end end |
O método login não faz nada, pois não precisamos desta notificação para o chat, então vamos a descrição do método logout …
E isto é tudo pessoal! (isto só fica engraçado em ingles
)
Agora só falta executar a aplicação.
Para executar esta aplicação, precisamos iniciar o servidor do Rails como padrão, e então iniciar o push_server, para fazer isto basta executar os seguintes dois comandos.
E acessar o seu chat novo em folha no endereço: http://localhost:3000/chat
Eu criei este exemplo emquanto estudava um pouco sobre o Juggernaut, é possível que existam maneiras mais fáceis de implementar isto, mas eu acredito que seja um bom ponto de partida.
Quaisquer dicas para melhorar o exemplo são muito bem vindas.
Se você gostou deste post, lembre-se de assinar o RSS feed do blog, para ser notificado de novos posts!
Tags: ajax, chat, example, flash, howto, push, rails, reverse, Ruby, simple, xml
[...] Comments Um exemplo de chat com Ruby On Rails e Juggernaut (utilizando AJAX Push) | Blog do Urubatan on A cool chat example created with Ruby On Rails and JuggernautUrubatan on It is getting easier to [...]
É um otimo exemplo usando o Juggernaut. Mas esse tipo de ajax, eu ja ouvi falar outros nomes também, como, “Comite” e “Ajax Inverso”.
Sabe me dizer se esse “push_server” é leve para o servidor?
Abraços, Claudio
Claudio,
não é comite e sim Commet, mas Comment é uma outra forma de implementar isto.
e este push_server é bem leve sim, ele foi criado por que se fosse utilizar um processo rails para cada cliente ficaria pesado demais.
[...] Leia o tutorial, que desta vez tem uma versão em português. [...]
hehe, tem razão, “Commet”, eu que estava atrasado para o trabalho e escrevi errado.
Valeu pelo toque ai.
Abraços
Legal. Tentei com o NetBeans mas o modulo de plugins deu erro (que nao vou colar aqui). Aconteceu com mais alguem?
Max, eu usei o GVim como editor de código, mas não deveria dar problema algum com o NetBeans …
Bom, com o JRuby (Netbeans e linha de comando, no Windows) deu essa:
…\chattest>jruby script/plugin install svn://rubyforge.org//var/svn/juggernaut/trunk/juggernaut
…/NetBeans6.0/ruby1/jruby-1.0.1/lib/ruby/1.8/pathname.rb:4
20:in `realpath_rec’: No such file or directory - …/chattest/C: (Errno::ENOENT)
from …/NetBeans6.0/ruby1/jruby-1.0.1/lib/ruby/1.8/
pathname.rb:453:in `realpath’
from …/NetBeans6.0/ruby1/jruby-1.0.1/lib/ruby/gems
/1.8/gems/rails-1.2.5/lib/initializer.rb:543:in `set_root_path!’
from …/NetBeans6.0/ruby1/jruby-1.0.1/lib/ruby/gems
/1.8/gems/rails-1.2.5/lib/initializer.rb:509:in `initialize’
from ./script/../config/boot.rb:38:in `new’
from ./script/../config/boot.rb:38:in `run’
from ./script/../config/boot.rb:38
from :1:in `require’
from :1
Nao sei bem pq adiciona esse :drive (no caso, :C) no final do path.
Com o Ruby nao deu problemas. Nao testei no Linux, mas pelo visto funciona.
Olá
Estou com problema no meu Juggernaut, aparentemente esta funcionando, mas as mensagem não chegam.
Verifiquei o log, e as mensagem estão chegando no servidor de juggernaut, porém aparece que a seguinte mensagem:
No such channel: chat
Verifiquei no juggernaut.yml e esta assim:
DEFAULT_CHANNELS:
- ‘chat’
O que poderia ser?
Clodonil
Clodonil,
que browser esta usando? Testei no Linux e com o Firefox funciona legal, mas o Konqueror por exemplo envia e recebe mas nao faz o refresh da pagina, entao nao pode se ver os usuarios e as mensagens (mesmo existindo).
Na linha 39 o elsif deveria ser um else.
Pelo que vi, o Konqueror aceita o render(:update) mas nao o data = render_to_string(:update). Talvez seja assim para outros browsers tambem.
Tem uns probleminhas ai com a gestao das sessoes etc. acredito seja coisa do plugin, ainda nao sei
Max, verdade, tem um erro na linha 39, já arrumei aqui
Eu não havia testado no Konkeror, muito obrigado por avisar do problema …
Gostaria de poder baixar o arquivo por completo já funcionando. agradeço desde já
Ehh, eu to vendo que preciso implentar algo parecido com isso… Queria ser igual o Ed-Novoplano ai em cima, cara de pau e pedir o código pronto!
ehehehehe
[]s
Olá,
Esse foi meu primeiro projeto de teste usando gems e plugins.. tive alguma dificuldades mas consegui intalar eles.
Porém depois que instalei o plugin, nao apareceu o arquivo config/juggernaut.yml, esse arquivo foi gravado na pasta: C:\Ruby\testes\chattest\vendor\plugins\juggernaut\media e nao tem as propriedades que vc citou.
Ele gravou esse arquivo em config: juggernaut_hosts.yml
que possui:
# You should list any juggernaut hosts here.
# You need only specify the secret key if you’re using that type of authentication (see juggernaut.yml)
#
# Name: Mapping:
# :port internal push server’s port
# :host internal push server’s host/ip
# :public_host public push server’s host/ip (accessible from external clients)
# :public_port public push server’s port
# :secret_key (optional) shared secret (should map to the key specified in the push server’s config)
# :environment (optional) limit host to a particular RAILS_ENV
:hosts:
- :port: 8080
:host: localhost
:public_host: localhost
:public_port: 8080
:secret_key: 481516232342edededededed
:environment: :development
Mesmo assim criei os arquivos e deu esse erro:
NoMethodError in Chat#index
Showing chat/index.html.erb where line #2 raised:
undefined method `listen_to_juggernaut_channels’ for #
Fiz algo de errado?
Vlw pelo artigo, muito bacana.
Abraço