ajax comet basic tutorial

เพราะว่า server ไม่สามารถเปิด connection ไปยัง browser ได้
ทางเดียวที่เป็นไปได้คือ ให้ browser hold connection ไว้

เช่น ถ้าเป็น php ก็ใช้ sleep

แล้วไปแก้ปัญหา server timeout โดยการเปิด connection ใหม่ ( resetConnection )

resetConnection คร่าวๆ คือ ใข้ heartbeat

ก็คือ call function heartbeat ทุกครั้งที่ response กลับมา เพื่อกันไม่ให้ heartbeat มันไป resetConnection

var timeoutId = null;

function heartbeat(){

clearTimeout(timeoutId);

timeoutId = setTimeout(resetConnection, 10000); // 10 sec.

}

ถ้า connection มันขาดหายไป ( timeout ไปแล้ว ) มันก็จะ resetConnection ใหม่ให้เอง

ซึ่งปกติเขาใช้ iframe เป็นตัว connector กัน โดยเซต src ( location ) ของ iframe เป็นไฟล์ response เลย

แต่มันด้วยการที่มันเป็น iframe เลยมีปัญหานิดหน่อย คือ browser จะขึ้นสถานะเป็น Loading ตลอดเวลา เพราะว่า connection ไปยัง server ในหน้า iframe ไม่ได้ถูกปิดไป

วิธีแก้สำหรับ IE คือ

แทนที่จะใส่ iframe ตรงๆ ก็ไปใส่ iframe ไว้ใน ActiveRecord(“htmlfile”); ซึ่งเป็น object ของ IE

ซึ่ง htmlfile ของ IE มันแยกจากหน้าเวป ดังนั้น จะไม่มีสถานะ loading ใน browser มาให้วุ่นวายใจ

เช่น

oPage = new ActiveRecord("htmlfile");

oPage.open();

oPage.write("<html><body></body></html>");

oPage.close("");

# โดยอย่าลืมใส่ reference ไปที่หน้าปัจจุบันด้วย เพราะว่า htmlfile ของ IE มันแยกเด็ดขาดจากหน้าเพจจริงๆ ( ใช้ parent หรือ top เรียกไม่ได้ )

oPage.parentWindow._parent = self;  # คือ เซตให้เพจลูกเรียก parent._parent แล้วมาเจอ object นี้นั่นเอง
# ดังนั้นในเพจ response ใน iframe จึงเรียก parent._parent.heartbeat() ได้

# หมายเหตุ <em>_parent</em> เป็นชื่อเฉยๆ นะ จะเปลี่ยนเป็นอย่างอื่นก็ได้แหละ</strong>

oPage.body.innerHTML = "<iframe src=\"http://dsin.blogspot.com\"></iframe>"

วิธีแก้สำหรับ firefox คือ

firefox มีวิธีจัดการกับ HTTP streaming ได้ดีกว่า IE

แทนที่จะใช้ iframe ก็ให้ไปใช้ XMLHttpRequest แทน โดยดักจับ สถานะ .readyState เป็น 3 ( คือ ส่งแล้ว ยังไม่ complete )

ref : comet and reverse ajax book ( Apress )

เล่น 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/