ตัวอย่าง shooting_star observer ที่หามานาน

หลังจาก ประสบปัญหาว่า จะรู้ว่า user online และ offline จากห้องแชทได้ยังไง

ในที่สุดก็ไปเจอมาแล้ว ที่นี่ หรือว่า ที่นี่

โค้ดเป็นตามนี้

#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../config/boot'
require File.dirname(__FILE__) + '/../config/environment'
require 'drb/drb'

class ChatObserver
include DRb::DRbUndumped
def name; 'simple_chat/chatroom observer' end
def enter(params)
puts "ChatObserver:"
puts params.inspect
end
def leave(params)
puts "ChatObserver:"
puts params.inspect
end
end

Meteor::shooter.observe('<strong>chat1</strong>', ChatObserver.new) # สมมติว่า channel ชื่อ chat1 ละกัน [ ของในตัวอย่างมันเป็น simple_chat/chatroom ตัวหลังเดาว่าเป็น tag (ไม่ชัวร์นะ) ]
puts 'ChatObserver installed successfully.'
puts '[ PRESS ENTER KEY TO EXIT ]'
gets

นอกจากนี้ยังมีการเขียน observer ที่มีประโยชน์อยู่ในเทสของเวอร์ชั่น 3.2.0

อยู่ใน test/lib/shooting_star_test.rb

require File.join(File.dirname(__FILE__), '../test_helper')
require 'shooting_star'
require 'socket'
require 'thread'

$command_line = 'echo "testing"'

class ShootingStarTest &lt; Test::Unit::TestCase
class TestObserver
include DRb::DRbUndumped
def name; 'test-observer' end
def enter(params) @params = params end
def params; @params end
end

def setup
@config = ShootingStar.configure :silent =&gt; true,
:pid_file =&gt; 'tmp/pids/shooting_star.test.pid',
:log_file =&gt; 'log/shooting_star.test.log',
:server =&gt; {:host =&gt; '127.0.0.1', :port =&gt; 8081},
:shooter =&gt; {:uri =&gt; 'druby://127.0.0.1:7124'}
mutex = Mutex.new
mutex.lock
@thread = Thread.new{ShootingStar.start{mutex.unlock}}
mutex.lock
@query = "sig=0123456789&amp;execute=http://127.0.0.1:4001/meteor/strike"
@query2 = "sig=1123456789&amp;execute=http://127.0.0.1:4001/meteor/strike"
end

def teardown
ShootingStar.stop
File.rm_f(@config.pid_file)
File.rm_f @config.log_file
@thread.join
end

def test_connection_with_invalid_method
client = TCPSocket.open('127.0.0.1', 8081)
assert_not_nil client
send(client, "GET", "test/channel", @query)
assert client.read.empty?
end

def test_connection
client = TCPSocket.open('127.0.0.1', 8081)
send(client, "POST", "test/channel", @query)
assert_not_nil result = client.read
assert_not_nil result.index('xhr.getResponseHeader')
assert_not_nil result.index('test\/channel')
client.close

mutex = Mutex.new
mutex.lock
Thread.new do
client = TCPSocket.open('127.0.0.1', 8081)
send(client, "POST", "test/channel", "#{@query}&amp;__t__=c")
mutex.unlock
end
mutex.lock
shooter = DRbObject.new_with_uri('druby://127.0.0.1:7124')
assert_not_nil shooter
shooter.shoot("test/channel", 12, [])
assert_not_nil result = client.read
assert_not_nil result.index('meteor/strike/12')
end

def test_multi_user_communication
client1 = TCPSocket.open('127.0.0.1', 8081)
client2 = TCPSocket.open('127.0.0.1', 8081)
assert_not_nil client1
assert_not_nil client2
shooter = DRbObject.new_with_uri('druby://127.0.0.1:7124')
assert_not_nil shooter
observer = TestObserver.new
assert_not_nil observer
shooter.observe('test/channel', observer)
mutex = Mutex.new
mutex.lock
assert_nil observer.params
Thread.new do
send(client1, "POST", "test/channel", "#{@query}&amp;__t__=c")
mutex.unlock
end
mutex.lock
assert_nil observer.params
Thread.new do
send(client2, "POST", "test/channel", "#{@query2}&amp;__t__=c")
mutex.unlock
end
mutex.lock
assert_not_nil observer.params
assert_equal :enter, observer.params[:event]
assert_not_nil result1 = client1.read
assert_not_nil result1.index('meteor/strike/event-')
shooter.shoot("test/channel", 12, [])
assert_not_nil result2 = client2.read
assert_not_nil result2.index('meteor/strike/12')
end

def test_xmlsocket_server
client = TCPSocket.open('127.0.0.1', 8081)
client.write("&lt;policy-file-request/&gt;")
assert_not_nil client.read.index('allow-access-from')
end

def test_shooter_exists
shooter = ShootingStar.shooter
assert_not_nil shooter
end

def test_c10k_problem
bin = File.join(RAILS_ROOT, 'bin/test_c10k_problem')
src = File.join(File.dirname(__FILE__), 'test_c10k_problem.c')
if !File.exist?(bin) || File.mtime(src) &gt; File.mtime(bin)
system "gcc #{src} -o #{bin}"
end
system bin
end

private
def send(client, method, path, body)
client.write "#{method} /#{path} HTTP/1.1\n\r" +
"Host: #{@config.server.host}:#{@config.server.port}\n\r" +
"Keep-Alive: 300\n\r" +
"Content-length: #{body.length}\n\r" +
"Connection: keep-alive\n\r\n\r#{body}"
end
end
Advertisements

เล่น comet บน rails ด้วย shooting star (1)

ลองเข้าไปดูตามลิงก์นี้ แล้วทำตาม ได้ดังนี้

# เริ่มแรก ลง shooting star plugin ด้วย gem

$ gem install shooting_star

————————————————————————–

# create your own project

$ rails real_chat

# change the direction

$ cd real_chat

# initialize the shooting star, this command reacts nothing but don’t worry 🙂

$ shooting_star init

# มันแอบไปสร้าง config file ของ shooting_star ( config/shooting_star.yml )

# แล้วก็ vendor/plugins/meteor_strike/

[ meteor_strike เป็น plugin ของ rails ซึ่งทำหน้าที่เป็น client ของ shooting_star ]
# generate the bottom technology of comet with the generator given by shooting star

$ ruby script/generate meteor

exists  app/controllers/
exists  test/functional/
create  app/views/meteor
exists  app/models/
exists  test/unit/
create  app/controllers/meteor_controller.rb
create  test/functional/meteor_controller_test.rb
create  app/helpers/meteor_helper.rb
create  app/views/meteor/strike.rhtml
create  app/models/meteor.rb
create  test/unit/meteor_test.rb
create  public/meteor_strike.swf
create  db/migrate
create  db/migrate/20081028162753_create_meteors.rb
# Also you can get the chat scaffold type the following command

$ ruby script/generate chat

exists  app/controllers/
exists  test/functional/
create  app/views/chat
exists  app/models/
exists  test/unit/
create  app/controllers/chat_controller.rb
create  test/functional/chat_controller_test.rb
create  app/helpers/chat_helper.rb
create  app/views/layouts/chat.rhtml
create  app/views/chat/index.rhtml
create  app/views/chat/show.rhtml
create  app/models/chat.rb
create  test/unit/chat_test.rb
exists  db/migrate
create  db/migrate/20081028162839_create_chats.rb

# Launch your web server!!

$ ruby script/server

# Shooting star also needs to be launched

$ shooting_star start

shooting_star service started.

ลองเด๊ะ http://localhost:3000/chat

จะได้มาประมาณนี้

เหมือน มันจะต่อโดยใช้ flash เป็นอันดับแรก

  • (* system *) connection established by flash.

ถ้าใช้ flash ไม่ได้ มันถึงต่อโดยใช้ XMLHttpRequest

  • (* system *) connection established by xhr.

วิธี disable flash คือ ใส่คำว่า :noflash => true เข้าไปให้ meteor_strike helper เช่น

<%= meteor_strike ‘chat’, :uid => session[:name], :event => %Q{
new Ajax.Updater(‘chat-list’, #{url_for(:action => ‘event’).to_json}, {
insertion: Insertion.Top, parameters: params})}, :noflash => true
%>

มาแงะโค้ดกันเถอะ !!

ส่วน view ก่อนละกัน หน้า /chat นะ

มันมี layout อยู่

app/views/layouts/chat.rhtml

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” xml:lang=”en” lang=”en”>
<head>
<meta http-equiv=”content-type” content=”text/html;charset=UTF-8″ />
<title>Chat: <%= controller.action_name %></title>
<%= javascript_include_tag :defaults %>
</head>
<body>
<%= yield %>
</body>
</html>

พอ view source  แล้ว

พบว่า <%= javascript_include_tag :defaults %> ที่ render จริงๆ มี vbscript ด้วย อูฮู คนทำ มันขยันทำจริงๆ

แล้วก็นี่ app/views/chat/index.rhtml

<% form_remote_tag(:url => {:action => ‘listen’}) do |f| %>
<%= text_field_tag :name, session[:name] %>
<%= submit_tag ‘Listen‘ %><br />
<% end %>

<% form_remote_tag(:url => {:action => ‘talk’}) do |f| %>
<%= text_field_tag :message %>
<%= submit_tag ‘Talk‘ %><br />
<% end %>
<ul id=”chat-list”>
<% for chat in @chats %>
<%= render_component :action => ‘show’, :id => chat.id %>
<% end %>
</ul>
<%= meteor_strike ‘chat’, :uid => session[:name], :event => %Q{
new Ajax.Updater(‘chat-list’, #{url_for(:action => ‘event’).to_json}, {
insertion: Insertion.Top, parameters: params})}
%>

ปุ่ม Listen กับ Talk แยก form กัน

Listen เรียกไปที่ http://localhost:3000/chat/listen

Talk เรียกไปที่ http://localhost:3000/chat/talk

ถ้า database มีการ update มันจะส่งมาทาง

http://localhost:3000/meteor/strike/16?channel=chat&#1

http://localhost:3000/meteor/strike/17?channel=chat&#2

ซึ่งมันเป็น javascript ประมาณนี้

  (function(){
    var channel = "chat";
    var javascript = "try {\nnew Insertion.Top(\"chat-list\", \"<li>\\n  <span>guest</span>\\n  <span
>kkk</span>\\n</li>\\n\");\n} catch (e) { alert('RJS error:\\n\\n' + e.toString()); alert('new Insertion
.Top(\\\"chat-list\\\", \\\"<li>\\\\n  <span>guest</span>\\\\n  <span>kkk</span>\\\\n</li>\\\\n\\\")
;'); throw e }";
    var execute = function(){
      var ms = null;
      try{if(parent.meteorStrike_installed) ms = parent.meteorStrike}catch(e){}
      ms = (ms || parent.parent.meteorStrike)[channel];
      if(ms) ms.evaluate(javascript, location.hash.slice(1));
      else setTimeout(execute, 0);
    };
    execute();

ul chat-list เป็นที่แสดง chat ธรรมดา

ส่วน tag meteor_strike

<%= meteor_strike ‘chat’, :uid => session[:name], :event => %Q{
new Ajax.Updater(‘chat-list’, #{url_for(:action => ‘event’).to_json}, {
insertion: Insertion.Top, parameters: params}
)}
%>

สังเกต ว่า เราสามารถใส่ event ได้ให้มันไปอัพเดตที่ไหน

ซึ่ง ruby code ตะกี้

พอเรนเดอร์แล้วจะได้ดังนี้

  <iframe id="meteor-strike-1" name="meteor-strike-1"></iframe>
  <form id="meteor-strike-1-form" target="meteor-strike-1" method="POST"
    action="http://localhost:8080/chat">
    <input name="execute" value="http://localhost:3000/meteor/strike" />
    <input name="tag" /><input name="uid" /><input name="sig" />
    <input name="heartbeat" value="" />

  </form>
  <div id="meteor-strike-1-flash"></div>
  <script type="text/javascript">
//<![CDATA[
    window.meteorStrike_installed = true;
    window.meteorStrike = window.meteorStrike || new Object;
    meteorStrike.getFlashVersion = function(){
      ...
      return version
    };
    (function(){
      var channel = "chat";
      var UID = "haha", TAGS = [];
      var encodeTags = function(tags){
        var encode = function(i){return encodeURIComponent(i)};
        return $A(tags).uniq().map(encode).join(',');
      };
      var ms = meteorStrike[channel] = meteorStrike[channel] || new Object;
      ms.getTags = function(){return TAGS};
      ms.getUid = function(){return UID};
      ms.executionQueue = {};
      ms.executionCounter = null;
      ms.evaluate = function(js, serialId){
        if(ms.executionCounter == null){
          ms.executionCounter = serialId;
        }
        ms.executionQueue[serialId] = js;
        if(serialId == ms.executionCounter){
          while(js = ms.executionQueue[ms.executionCounter]){
            eval(js);
            delete ms.executionQueue[ms.executionCounter];
            ++ms.executionCounter;
          }
        }else{
          setTimeout(function(){
            if(js = ms.executionQueue[this]){
              eval(js);
              delete ms.executionQueue[this];
              ms.executionCounter = null;
            }
          }.bind(serialId), 3000);
        }
      };
      ms.event = function(params){
        if(params.event == 'init'){
          if(ms.connection) return;
          if(ms.connecting && ms.connecting != params.type) return;
          ms.connection = params.type;
        }
# request ไปที่ http://localhost:3000/chat/event
# นี่คึอผลลัพธ์ที่ได้มา
#
# <li>
#  <span>(* system *)</span>
# <span>connection established by flash.</span>
# </li>
 (function(){
  new Ajax.Updater('chat-list', "/chat/event", {
    insertion: Insertion.Top, parameters: params})})();
      };
      ms.update = function(uid, tags){
        new Ajax.Request("http://localhost:3000/meteor/update", {postBody: $H({
          channel: channel, uid: uid || UID,
          tag: encodeTags(tags || TAGS), sig: "1225212975634282"
        }).toQueryString(), asynchronous: true});
        UID = uid || UID, TAGS = tags || TAGS;
      };
      ms.tuneIn = function(tags){
        ms.update(UID, TAGS.concat(tags || []).uniq());
      };
      ms.tuneOut = function(tags){
        ms.update(UID, Array.prototype.without.apply(TAGS, tags));
      };
      ms.tuneInOut = function(tagsIn, tagsOut){
        var tags = TAGS.concat(tagsIn || []).uniq();
        ms.update(UID, Array.prototype.without.apply(tags, tagsOut));
      };
      ms.tuneOutIn = function(tagsOut, tagsIn){
        var tags = Array.prototype.without.apply(TAGS, tagsOut);
        ms.update(UID, tags.concat(tagsIn || []).uniq());
      };
# Event.observe ของ prototype ( ดู ดอก ที่ http://www.prototypejs.org/api/event/observe )
# อันนี้คือ window.onload นั่นเอง
      Event.observe(window, 'load', function(){
        setTimeout(ms.connector = function(){
          if(ms.connection) return;
          if(ms.connecting && ms.connecting != 'xhr') return;
          var form = $("meteor-strike-1-form");
          form.uid.value = "haha";
          form.tag.value = "";
          form.sig.value = "1225212975634282";
          var timerId = setTimeout(ms.connector, 3000);
          $('meteor-strike-1').onload = function(){clearTimeout(timerId)};
          form.submit();
        }, meteorStrike.getFlashVersion() >= 8 ? 3000 : 0);
      });
    })();

    function meteor_strike_1_DoFSCommand(command, args){
      var ms = meteorStrike["chat"];
      args = unescape(args);
      switch(command){
      case 'execute':
        if(ms.connection == 'flash' || ms.connecting == 'flash') eval(args);
        else if($('meteor-strike-1-flash')) Element.remove('meteor-strike-1-flash');
        break;
      case 'event':
        switch(args){
        case 'connect': // intercept xhr connection
          ms.connecting = 'flash';
          if(ms.connection != 'flash') ms.connection = null;
          if($('meteor-strike-1')) Element.remove('meteor-strike-1');
          break;
        }
        break;
      }
    }

    if(meteorStrike.getFlashVersion() >= 8){
      $('meteor-strike-1-flash').innerHTML = "\n<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\"\n codebase=\"http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,0,0\" width=\"300\" height=\"300\"\n id=\"meteor_strike_1\">\n  <param name=\"allowScriptAccess\" value=\"sameDomain\" />\n  <param name=\"FlashVars\" value=\"channel=chat&tag=&uid=haha&sig=1225212975634282&base_uri=http://localhost:3000&server=localhost:8080&heartbeat=&debug=null&meteor_strike_id=1\" />\n  <param name=\"movie\" value=\"/meteor_strike.swf?1225034522\" />\n  <param name=\"menu\" value=\"false\" />\n  <param name=\"quality\" value=\"high\" />\n  <param name=\"devicefont\" value=\"true\" />\n  <param name=\"bgcolor\" value=\"#ffffff\" />\n  <embed src=\"/meteor_strike.swf?1225034522\" menu=\"false\"\n   quality=\"high\" devicefont=\"true\" bgcolor=\"#ffffff\" width=\"300\" height=\"300\"\n   swLiveConnect=\"true\" id=\"meteor_strike_1\"\n   name=\"meteor_strike_1\" flashvars=\"channel=chat&tag=&uid=haha&sig=1225212975634282&base_uri=http://localhost:3000&server=localhost:8080&heartbeat=&debug=null&meteor_strike_id=1\"\n   allowScriptAccess=\"sameDomain\" type=\"application/x-shockwave-flash\"\n   pluginspage=\"http://www.macromedia.com/go/getflashplayer\" />\n</object>\n";
    }
//]]>
</script>

น่าจะเป็นส่วนที่สำคัญที่สุด เพราะว่าเป็นตัวรับข้อความ chat กลับมาโชว์ เมื่อ database มีการ update แล้ว

ถ้าสังเกตดูจะเจอ ฟังก์ชั่นหน้าตาประหลาด เลยลองเขียนโค้ดเทสดูดังนี้

<script language=”javascript”>
(function(){
alert(‘test’)
})();
</script>

ปรากฏว่ามัน alert test มาแหะ

เลยเดาๆ ว่า เป็น execute code ธรรมดาละกันนะ

ต่อไปมาดู model ว่ามันเรียกอะไรกันยังไง

หรือ จะเล่นแบบหลายๆอัน

จะเห็นว่ามันใช้ druby ( Distributed Ruby ) เพื่อไปเรียก port อื่น

ดูมาจากไฟล์ config/shooting_star.yml

server:
host: 0.0.0.0
port: 8080
shooter:
uri: druby://0.0.0.0:7123

จริงๆ แล้วมันมี shooting_star รันเป็น server อยู่ที่ port 8080 นะ

ซึ่ง binary อยู่ที่ /var/lib/gems/1.8/bin/shooting_star

เวลา talk ปุ๊ปมันจะส่ง request ไปที่ chat_controller.rb

action talk ซึ่งมัน insert chat log ลง database แล้วแอบไปเรียก Meteor.shoot

—-

referenece

Drb : http://www.chadfowler.com/ruby/drb.html

ทางเลือกอื่นของการทำ chat บน rails : http://abhijat.name/crossroads/2008/04/01/rails-chat-demystified/

chat ตัวนี้ก็น่าสนใจนะ : http://home.gna.org/xmpp4r/