PHP & JQuery ile Comet Programlama

Günümüzün kaçınılmaz programlama tekniği olan comet programlamayla ilgili basit bir anlatım ve örnek olacak.

Comet programlama’nın amacı nedir?

Bildiğiniz gibi PHP ve JQuery ile yapabilecekleriniz sınırlı, çünkü sunucu ile ilişkiniz sayfa yüklendiği anda biter. Chat gibi uygulamalarda sunucuyla sürekli bağlantıya ihtiyaç duyarız, bunu aslında küçük bir hile ile ekstradan hiç bir tool kullanmadan halledeceğiz.

Başlarken…

Amacımız sunucu ile sürekli bağlantı sağlamak, yani bağlantımızı koparmamak. Madem sayfa yüklenmesi bittiği anda sunucu ile bağlantımız kesiliyor, bizde sayfanın yüklenmesini hiç bitirmeyeceğiz Nasıl? Sonsuz döngü ile. 🙂 Comet’teki asıl mantıkta burada. Sürekli bağlığız ne zaman ki yeni bir istek geldi (Mesela chat uygulamasında mesaj gelmesi) o zaman bağlantımızı kopartıyoruz yani sonsuz döngümüz bitiyor ve işlemek istediğimiz veriyi işliyoruz ve hemen ardından tekrar sunucu ile bağlantıda kalacak bir php sayfasını çağırıyoruz. Görüldüğü gibi çok zor değil. Şimdi gelelim Facebook, Gmail vs. gibi siteler bu işi nasıl yapıyor? Tamamen aynı! Şimdi size facebook’unuz açıkken arka planda ne olduğuna dair bir resim göstereceğim;

comet

En aşağıda gördüğünüz istek sürekli çalışıyor, yüklenmesi hiç bitmiyor, az önce anlattığım tekniği uyguluyor yani. Comet programlama ile yapılan örneklerde genelde veritabanı yerine dosyalar kullanılır. Bana göre bu pek gerçekçi bir örnek olmadığı için, size veritabanı ile bu işi nasıl halledeceğiz onu göstereceğim. Öncelikle az önce bahsettiğim sonsuz döngümüzde neler olduğunu bir anlatayım, daha sonra bu örneklerde neden veritabanı kullanılmıyor bunu sizde anlayabileceksiniz. Sonsuz döngümüz her zaman şunun kontrolünü yapar; Ben en güncel veriyi aldığımdan beri bir değişiklik olmuş mu? Şimdi eğerki veritabanı ile bu işi yaparsanız, yani siz en güncel veriyi aldığınızdan beri daha güncel veri gelmiş mi diye kontrol ederken veritabanı kullanırsanız şu olur; Sonsuz döngümüz 10 milisaniyede bir kontrol yapacak (10 ms sınırlamasını biz getireceğiz) 10 ms’de bir, veritabanı kontrolü yapılırsa ne olur? Siz tahmin edin 🙂 Dosya ile uygulanmasının sebebi şudur; En son güncel veriye sahip miyiz değil miyiz diye yaptığımız kontrolü dosyanın son değiştirme tarihine bakarak öğrenilir. Dosyanın son değişme tarihi bizim elimizde olan son değişme tarihinden büyükse daha güncel bir veri gelmiş ve hemen bunu alalım demek oluyor. Veritabanından 10 ms’de bir çekmekten çok daha etkili bir yöntem. Şimdi asıl sorunumuz neymiş? Veri en son ne zaman güncellendi bunun bilgisini sunucuda bir yerde saklamalıyız. Benim çözümüm bellekte tutmak yani; PHP’de memcache kullanmak. Bu yöntem ile dosya ile yapılan uygulamadan çok daha hızlı ve etkili erişebiliriz verimize. Eğerki windows kullanıcı iseniz memcache eklentisini php sunucunuza kurmanız gerekiyor, google’dan destek alabilirsiniz 🙂

Kodlamaya geçelim;

Öncelikle chat bilgilerini tutacak bir veritabanı tablomuza ihtiyaç var tabiki 🙂

comet

çok basit bir veritabanı tablosu. “chat” ismindeki tablomuzun metin kolonu gönderilen mesajı ve timestamp’ta bu mesajın unix time formatındaki gösterimini tutuyor ( time() fonksiyonunun döndürdüğü değer gibi)

<html>
    <head>
        <style type="text/css">
            #gonderiler {
                padding: 5px;
                margin:0;
                display: block;
            }
        </style>
        <script src="http://code.jquery.com/jquery-1.7.min.js"></script>
    </head>
    <body>

        <div id="gonderiler">
        </div>
        <div id="gonderform">
            <form action="javascript:void(0)" method="POST" onSubmit="gonder()">
                Mesaj: <input type="text" id="metin"/>
            </form>
        </div>
    </body>
</html>

ve bu şekilde çok basit bir html sayfamız var. İlk olarak mesaj gönderme sistemini yapalım, yani veritabanına bir metin eklemeyi; (Veritabanına veri eklerken, çekerken PDO objesini kullanıyorum, mysql_* fonksiyonlarıyla yapabilirsiniz).

send.php;

$db = new PDO("mysql:dbname=test;host=127.0.0.1", "root", "");
$stmt = $db->prepare("INSERT INTO chat (metin,timestamp) VALUES (:metin,:timestamp)");
$stmt->bindParam(":metin", $metin);
$stmt->bindParam(":timestamp", $timestamp);
$metin = $_POST['metin'];
$timestamp = time();
$stmt->execute();

Bu şekilde verimizi ekliyoruz veritabanına. Şimdi yapmamız gereken birde şu var; Veritabanına veri eklendi ama diğer kullanıcılar bundan nasıl haberdar olacak? Yapmamız gereken belleğe en son güncelleştirme tarihini eklemek olacak. O da çok basit;

$mc = new Memcache;
$mc->connect("localhost", 11211);
$result = $mc->replace("timestamp", $timestamp);
if ($result == false) {
    $result = $mc->set("timestamp", $timestamp);
}
$mc->close();

$timestamp’ı yukarıda veritabanına eklerken atamıştık. Belleğe şu anda atadık ileride get timestamp gibi birşey çağırdığımız zaman en son güncellenme tarihini bellekten hızlıca alacağız. send.php sayfamız böyleydi. Madem ki gönderme işlemimizi tamamladık html formumuzdaki submit özelliğini ekleyebiliriz;

function gonder() {
                $.post("send.php",{metin: $("#metin").val()},function(data) {});
                $("#metin").val("");
}

formumuz submit edildiği anda artık verimiz veritabanına eklenecek ve en son güncellenme tarihini belleğe kaydetmiş olduk. Şimdi gelelim verileri alacağımız bölüme (messages.php); Öncelikle; verimizi alırken XML kullanacağız ve sunucuya en son aldığımız verinin timestamp’ını göndereceğiz. Sunucuda bizim gönderdiğimiz timestamp’a göre en son hangi verileri alıp almadığımızı anlayacak.

$mc = new Memcache;
$mc->connect("localhost",11211);
$cacheTime = $mc->get("timestamp");
$curr = $_GET['timestamp'];
while (($cacheTime <= $curr) ¦¦ ($cacheTime == "")) {
    usleep(100000); // 10 ms uyuyoruz (:
    $cacheTime = $mc->get("timestamp");
}

sonsuz döngümüz bu şekilde. Ne zamanki bizim elimizdeki timestamp’tan daha güncel bir timestamp gelirse o zaman döngümüz sona erecek ve biz elimizdeki timestamp’tan sonraki verileri çekeceğiz. Yani kodun devamı şöyle;

$db = new PDO("mysql:dbname=test;host=127.0.0.1", "root", "");
$stmt = $db->prepare("SELECT * FROM chat WHERE timestamp > :curr ORDER BY timestamp ASC");
$stmt->bindParam(":curr",$curr);
$stmt->execute();
$sonuclar = $stmt->fetchAll();
echo '<?xml version="1.0" encoding="UTF-8"?>'."\r\n";
echo '<all>';
foreach ($sonuclar as $sonuc) {
    echo "<bilgiler><zaman>".date("h:i",$sonuc["timestamp"])."</zaman><timestamp>".$sonuc["timestamp"]."</timestamp><metin>".$sonuc["metin"]."</metin></bilgiler>";
}
echo '</all>';
$mc->close();

Güncelleme olduğunu anladık ve verilerimizi sunucu tarafından bize gönderilmesini sağladık. Şimdi bir kilit nokta daha ajax ile nasıl çekeceğiz;

var timestamp=0,lock=false;
            $(document).ready(function() {
                setInterval(fetchData,300);
            });
            function fetchData() {
                if (lock == false) {
                    lock = true;
                    $.ajax({
                        url: 'messages.php?timestamp='+timestamp,
                        dataType: "xml",
                        success: parse,
                        error:function() { lock = false; }
                    });
                }
            }
            function parse(data) {
                $xml = $(data);
                lock = false;
                $xml.find("bilgiler").each(function () {
                    if ($(this).find("timestamp").text() > timestamp) timestamp = $(this).find("timestamp").text();
                    $("#gonderiler").append("<br/><i>["+$(this).find("zaman").text()+"]</i> "+$(this).find("metin").text());
                });
            }

İlk olarak 300 milisaniyede bir sürekli çalışacak bir setInterval belirtiyoruz. Bu 300 ms’de bir fetchData fonksiyonunu çalıştıracak. Ancak her ne kadar o 300 ms’de bir fetchData() fonksiyonunu çalıştırsada fetchData çalışmayacak. Çünkü; fetchData fonksiyonu çalışmaya başladığı anda lock ile onu kilitliyoruz ve ne kadar çağırılsada çalışmıyor ta ki bir error veya başarılı bir sonuç alana kadar. Başarılı bir sonuç veya error aldığımızda kilidi açıyoruz ve tekrar ajax request’i göndermesine izin veriyoruz. Görüldüğü gibi çok basit bir işlem aslında. Önemli olan mantığı kavramak şimdi tüm kodları toparlıyıp ekleyeyim:

send.php:

$db = new PDO("mysql:dbname=test;host=127.0.0.1", "root", "");
$stmt = $db->prepare("INSERT INTO chat (metin,timestamp) VALUES (:metin,:timestamp)");
$stmt->bindParam(":metin", $metin);
$stmt->bindParam(":timestamp", $timestamp);
$metin = $_POST['metin'];
$timestamp = time();
$stmt->execute();
$mc = new Memcache;
$mc->connect("localhost", 11211);
$result = $mc->replace("timestamp", $timestamp);
if ($result == false) {
    $result = $mc->set("timestamp", $timestamp);
}
$mc->close();

messages.php:

$mc = new Memcache;
$mc->connect("localhost",11211);
$cacheTime = $mc->get("timestamp");
$curr = $_GET['timestamp'];
while (($cacheTime <= $curr) ¦¦ ($cacheTime == "")) {
    usleep(100000);
    $cacheTime = $mc->get("timestamp");
}
$db = new PDO("mysql:dbname=test;host=127.0.0.1", "root", "");
$stmt = $db->prepare("SELECT * FROM chat WHERE timestamp > :curr ORDER BY timestamp ASC");
$stmt->bindParam(":curr",$curr);
$stmt->execute();
$sonuclar = $stmt->fetchAll();
echo '<?xml version="1.0" encoding="UTF-8"?>'."\r\n";
echo '<all>';
foreach ($sonuclar as $sonuc) {
    echo "<bilgiler><zaman>".date("h:i",$sonuc["timestamp"])."</zaman><timestamp>".$sonuc["timestamp"]."</timestamp><metin>".$sonuc["metin"]."</metin></bilgiler>";
}
echo '</all>';
$mc->close();
?>

show.php:

<html>
    <head>
        <style type="text/css">
            #gonderiler {
                padding: 5px;
                margin:0;
                display: block;
            }
        </style>
        <script src="http://code.jquery.com/jquery-1.7.min.js"></script>
    </head>
    <body>
        <script type="text/javascript">
            var timestamp=0,lock=false;
            $(document).ready(function() {
                setInterval(fetchData,300);
            });
            function fetchData() {
                if (lock == false) {
                    lock = true;
                    $.ajax({
                        url: 'messages.php?timestamp='+timestamp,
                        dataType: "xml",
                        success: parse,
                        error:function() { lock = false; }
                    });
                }
            }
            function parse(data) {
                $xml = $(data);
                lock = false;
                $xml.find("bilgiler").each(function () {
                    if ($(this).find("timestamp").text() > timestamp) timestamp = $(this).find("timestamp").text();
                    $("#gonderiler").append("<br/><i>["+$(this).find("zaman").text()+"]</i> "+$(this).find("metin").text());
                });
            }
            function gonder() {
                $.post("send.php",{metin: $("#metin").val()},function(data) {});
                $("#metin").val("");
            }
        </script>
        <div id="gonderiler">
        </div>
        <div id="gonderform">
            <form action="javascript:void(0)" method="POST" onSubmit="gonder()">
                Mesaj: <input type="text" id="metin"/>
            </form>
        </div>
    </body>
</html> 

27 thoughts on “PHP & JQuery ile Comet Programlama

  1. Özgür

    herşey çok güzel ama ben bi konuda takıldım kaldım ve yardımınıza ihtiyacım var. minik bi chat uygulaması yazıyorum. uygulamaya admin olarak giriş yaptığım zaman ekranda listelenen mesajların yan taraflarında bi silme butonu beliriyo. bu butona bastığım zaman sadece o mesaj silinmesini istiyorum. nası yapabileceğim hakkında tavsiyelerinizi bekliyorum. teşekkürler..

    Reply
    1. admin Post author

      Mesajı silme butonuna JQuery ile silme sayfasına ajax isteği yapabilirsiniz. Her mesaj için bir id tutun ve silme sayfasına o id’yi gönderin.

      Reply
  2. Özgür

    silme işlemi için http://goo.gl/hHvU3 bu sayfadaki kodları kullanıyorum. bu kodları farklı bi yerde kullanıp çalıştırdığım zaman herhangi bi sorunla karşılaşmıyorum. ama sizin verdiğiniz sistemle birleştirmeye çalıştığımda bi sonuç alamıyorum. acabı kodları yanlış yere mi yazıyorum?

    Reply
  3. Özgür

    yaptım ama daha basit bi yöntemi olabilir diye düşünüyorum. show.php’nin içindeki ilgili bölümü şu şekilde değiştirdim.

    function parse(data)
    {
    $xml = $(data);
    lock = false;
    $xml.find(“bilgiler”).each(function ()
    {
    if ($(this).find(“zaman”).text() > zaman) zaman = $(this).find(“zaman”).text();
    $(“#cari_gruplar”).append(“$(document).ready(function(){$(‘a.delete’).click(function(e){e.preventDefault();var parent = $(this).parent();$.ajax({type: ‘get’,url: ‘inc/jquery/cari_grup_sil.php’,data: ‘ajax=1&delete=’ + parent.attr(‘id’).replace(”,”),beforeSend: function(){parent.animate({‘backgroundColor’:’#fb6c6c’},300);},success: function(){parent.slideUp(300,function(){parent.remove();});}});});}); “+$(this).find(“cari_grup”).text()+” “);
    });
    }

    ne düşünüyosunuz? 🙂

    Reply
    1. admin Post author

      Gördüğüm ilk hata şu;

      $(“#cari_gruplar”).append(“$(document).ready(function(){$(‘a.delete’).click(function(e){e.pr…

      bir layer’a (div) javascript kodları append etmek istemişssiniz ancak javascript kodları sadece <script> tagleri içerisinde çalışır. Bunları parse fonksiyonu dışında biryerde yapabilirsiniz.

      Reply
  4. Yusuf Karagün

    Merhaba;
    pratik ve açıklayıcı olmuş teşekkürler. Birde pdo sınıfını kullanmadan bir örnek parylaşabilirseniz harika olucak.

    Reply
    1. admin Post author

      PDO sınıfının orada konuyla pek bağlantısı yok. Tek yapılan bir insert bir de select sorgusu var. Normal mysqli fonksiyonlarını kullanarak da aynısının yapılması pek zor değil.

      Reply
  5. Mehmet

    Merhaba,
    Örnek için teşekkürler, gerçekten mükemmel bir mantık kurulmuş. Ama anlayamadığım bir şey var. Yaptığımız requestten bir sonuç dönmediği için biz bir kaynak kilitlemiş oluyoruz. Çok fazla kullanıcı olduğunda bu bir sıkıntı oluşturmaz mı? Facebook’un bu mantık üzerinde çalıştığına emin misiniz?

    Reply
    1. Can Geliş Post author

      Aslında bir kaynak kilitlemiş olmuyorsunuz. Sizin bir döngüde beklemeniz herhangi bir kaynağı kilitlemez. İşte bu yüzden multi-tasking dediğimiz şey var. İşletim sistemi bunların hepsini hallediyor.

      Reply
  6. Talha

    sonunda aradıgım blogu buldum
    günlerdir facebook duvar benzeri script yazmaya calısıyorum ve yazdımda . hep bu olayı merak etmiştim çünkü ben post ile kontrol edip güncelliyordum alanları. chromeden facebook twitter vs network tabına bakıyorum haraketlilik yok ama benim script bissürü post alıp veriyor 🙂 yarın burdakileri uygulamaya calıscam çok tşk.

    Reply
  7. Hacı Seydaoğlu

    Merhabalar, öncelikle detaylı ve sade yazınız için teşekkürler. Ben de bu teknolojiyi kendi projemde kullanmaya çalışıyorum. İlk denemeler başarılı. Fakat şöyle bir sıkıntım var: mesela while döngüsünde while($sayi < 100) şartı koyuyorum. Ve her döngü de $sayi'yi 1 artırıyorum ($sayi++). Bu şekilde çalışıyor. Fakat şartı 1000 yapınca çalışmıyor. Sürekli bekleniyor diyor ve site tamamen çalışmamaya başlıyor. Sürekli bekleniyor diyor. Siteye tekrar erişmek için ön belleği temizlemek zorunda kalıyorum. Acaba bu neyden kaynaklanıyor. Sunucu ile ilgili bir durum mu acaba? Yardımcı olursanız sevinirim. İyi çalışmalar.

    Reply
  8. Ali İhsan Candemir

    memcache kullanımı host yönünden sıkıntı oluyor. Yani paylaşımlı hostlar memcache desteği vermiyorlar haliyle sunucu kiralama gereği doğuyor. Bu işlemde apc kullanılabilir mi? yine aynı sonuç elde edilebilir mi?

    Reply
    1. Can Geliş Post author

      Tabikide buradakinin aynısını apc ile de yapabilirsiniz. Buradaki asıl amacımız birden fazla sürecin ihtiyaç duyduğu bir veriyi herkesin ulaşabileceği bir alana aktarmak.

      Reply
  9. Emre

    bir sorum olacak
    $stmt->bindParam(“:timestamp”, $timestamp);
    bu gibi ifadeler ne demek. yani anlamadığım -> operatörü ne işe yarıyor.

    Reply
  10. Emre

    cevabınız için teşekkürler. sizden bir ricam olabilir mi. basit bir çoklu chat odası mantığı anlatabilir misiniz bir dersinizde. ilginiz için teşekkürler.

    Reply
  11. Murat

    hocam merhaba ya bu şekilde sayfayı f5 yapmaya çalışınca yapmıyor yani devamlı bir bağlantı pedding durduğu için sayfayı yenilemiyor o işlem bitmeden bunu nasıl aşabiliriz acaba?

    Reply
  12. ümit

    yazı için teşekkür ederim.
    benim yaptığım örnekte Maximum execution time of 30 seconds exceeded hatası veriyor

    Reply
  13. Kazım Ölmez

    Merhabalar Hocam konu icin tesekkurler.

    Murat ve Ümit arkadaslarim. Aldiginiz hata ve sıkıntılar sayfa yüklenmesinden kaynaklanıyor. Php’nin usleep fonksiyonunu kullanmak yerine jquery’nin sleep fonksiyonunu kullanin.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax