آموزش های این وب سایت به صورت رایگان در دسترس است. اطلاعات بیشتر
مشکل عدم دسترسی خریداران پیشین به برخی آموزش ها برطرف شد
بروز خطا
   [message]
اشتراک در سوال
رای ها
[dataList]

اسکریپت چت با استفاده از COMET به همراه آموزش + دمو

محسن موحد  9 سال پیش  9 سال پیش
+22 0

سلام.

این اسکریپتو با استفاده از ajax  و comet نوشتم.بد نیست شما هم به کدهام نگاهی بندازین و اگه نظری داشتین مطرج کنین.

لینک: دموی این اسکریپت

لینک: دانلود اسکریپت


نکته: اگر خواستین با چنتا مرورگر تست کنید , با یوزر های متفاوت لاگین کنید.
انشالله توضیحات در مورد comet و کدها , در پست های بعدی میذارم.

 برای این سوال 2 پاسخ وجود دارد.
پاسخ به سوال 
محسن موحد  9 سال پیش
+9 0

برای پیاده سازی این سیستم میتونیم از ajax استفاده کنیم و هر یک یا چند ثانیه با توابعی مثل setinerval یا settimeout عمل آجاکس رو تکرار کنیم و سمت سرور بریم و چک کنیم اگر پیغام جدیدی اومده بود برای کاربر ارسال بشه. ولی تعداد درخواست های زیادی که از سمت کلاینت فرستاده میشه بار زیادی رو سرور میزاره که باعث اتلاف منابع سرور و عدم کارایی سیستم میشه. یکی از راه حل های این موضوع تکنولوژی Comet هست.

 در کامت وقتی درخواستی با توابع ajax به سمت سرور از سمت کلاینت فرستاده میشه, در سمت سرور فورا پاسخی داده نمیشه و ارتباط باز میمونه و تا زمانی که نتیجه ای رو دریافت کنه این ارتباطو نگه میداریم.این کارو با یک حلقه ی بینهایت میتونیم اجرا کنیم.البته در هاست های اشتراکی بعد از 30 ثانیه timeout میشه و ما ارتباطو در حد چند ثانیه باز نگه میداریم. کلیت قضیه این بود.حالا یکی یکی با کد میرم جلو. 


واسه سند یک پیام نیازی به comet نیست و از ajax استفاده میکنیم.چون قراره هر وقت دکمه ی اینتر و یا دکمه ی سند کلیک شد یک پیام در دیتابیس ثبت شود که این اتفاق زمان کلیک کردن کاربر روی دکمه بوجود میاد بنابراین از ajax استفاده میکنیم.

ارسال پیام:  (فایل send.js)

تابع sendMessage با فشردن دکمه ی enter اجرا میشه:

 function sendMessage() 
{ 
   var qt = ''; 
   var txt = text = $('#txtedit').val(); 
   to = 0; 

   if(typeof quote != 'undefined' && quote.trim() != '') 
   { 
      if(text.match(/\[:::\]_.+_\[:::\]/g)) 
      { 
         var rx = /\[:::\]_(.+)_\[:::\]/g; 
           var arr = rx.exec(text)[1]; 
           arr = arr.split('-'); 
           to = arr[1]; 
           messageID = arr[2]; 

         txt = text = text.replace(/\[:::\]_.+_\[:::\]/g, ''); 
      } 
       
      qt = quote.split(':'); 
      if(qt[1].trim() != '') 
      { 
         text += '[_::_]' + quote; 
         qt = '<div class="qt"><div class="from" style="'+ colorQuote +'">'+
htmlEntities(qt[0]) +':</div>'+ htmlEntities(qt[1]) +'</div>'; } else { qt = ''; } quote = undefined; } $('#messageBox').text(''); $('#txtedit').val(''); if($('.context').scrollTop() + $('.context').innerHeight() >= $('.context')[0].scrollHeight) { $(".context").animate({ scrollTop: $('.context')[0].scrollHeight}, 1000); } if(txt.trim() != '') { color = $('.me').attr('color'); txt = htmlEntities(txt); txt = convertToImg(txt); $('.context').append('<div class="inner quote lfloat" from="no">'+ qt.trim()
+'<div class="from" style="color: '+ color +' !important;">Me:</div>'+ txt +'</div>'); $('#send').attr('disabled', 'disabled'); $.ajax({ url: 'http://localhost/chat/display', async: true, type: "POST", data: 'text=' + text.trim() + '&to=' + to, cache: false, //dataType: 'json', success: function (msg) { if (msg == 1) { $('#send').removeAttr('disabled'); } }, error: function (msg) { alert('app has an error!'); } }); } }

این تابع شرو میکنه به دریافت مقادیر مورد نیاز از textbox و چک کردن وجود نقل قول و ... تا به ارسال پیام میرسه.
قبل از ارسال پیام من پیغام کاربرو در صفحه ی خودش چاپ کردم:

 $('.context').append('<div class="inner quote lfloat" from="no">'+ 
qt.trim() +'<div class="from" style="color: '+ color +' !important;">Me:</div>'+ txt +'</div>');

بعد هم بخش اصلی تابع که عمل سند پیام رو انجام میده این قسمت هست:

 $.ajax({ 
         url: 'http://localhost/chat/display', 
         async: true, 
         type: "POST", 
         data: 'text=' + text.trim() + '&to=' + to, 
         cache: false, 
         //dataType: 'json', 
         success: function (msg) { 
            if (msg == 1) 
            { 
               $('#send').removeAttr('disabled'); 
            } 
         }, 
         error: function (msg) { 
            alert('app has an error!'); 
         } 
      });

text حاوی پیام کاربره و to مشخص میکنه به چه کاربری داره ارسال میشه یعنی اگر صفر نباشد آیدی کاربری که پاسخش رو دادیم ارسال میکنه.
نمایش پیام کاربرو قبل از آجاکس انجام دادم و append کردم.اولش این عمل نمایش پیامو داخل رویداد success گذاشته بوده ولی چون ارتباط comet باز بود و درخواست کاربر pending میموند تا برگرده و این عمل سند انجام بشه که به همین علت مدت زمانی طول میکشید و نمایش پیامی که سند کرده به تاخیر می افتاد, بخاطر همین قبل از ajax قرار دادم که به محض دکمه ی enter روی صفحه ی کاربر پیامش نمایش داده بشه.
در ادامه , تابع ajax درخواستی رو برای آدرس display در صفحه ی index.php ارسال میکنه و در صفحه ی ایندکس این کد اومده:

 if (isset($parts[0])) 
      { 
         switch ($parts[0]) 
         { 
            case 'save': 
               $obj = new Save(); 
               echo $obj->save(); 
               break; 

            case 'display': 
               $obj = new Message(true); 
               echo $obj->display(); 
               break; 

            case 'logout': 
               session_destroy(); 
               Base::redirect($config->baseUrl . '/login'); 
               break; 

            case '404': 
               exit('Pooof...'); 
               break; 

            default: 
               Base::redirect($config->baseUrl . '/404'); 
               break; 
         } 

وقتی درخواست ارسال میشه به این خط کد مرسه و آبجکتی از کلاس message ساخته میشه و متد display صدا زده میشه:

 case 'display': 
               $obj = new Message(true); 
               echo $obj->display(); 
               break;

نکته: شرط (case) اول , که save هست اضافیه. یادم رفته پاکش کنم.(چون اول دریافت و ارسال در دو کلاس مختلف انجام میشد و به دلیل اینکه درخواست دریافت پیام ها به مدت چند ثانیه pending میموند و عملیات ارسال هم باید منتظر میشد , تصمیم گرفتم عمل دریافت و ارسال رو در یک کلاس و در یک متد به نام display پیاده کنم.بنظرم خوبه اینجا یه stop به درخواست pending بدیم تا درخواست ارسال پیام اجرا بشه Surprised )  - (فایل دانلود اصلاح شد.)

در ادامه وارد متد display میشه :

 public function display() 
   { 
      if(isset($_POST['text'])) 
      { 
         $text = DB::Escape($_POST['text']); 
         $time = microtime(true); 
         $to_message_id = (isset($_POST['to']) && $_POST['to'] > 0 ? intval($_POST['to']) : 0); 
         DB::Query("INSERT INTO `messages` 
(`user_id`, `to_message_id`, `text`, `timestamp`)
VALUES ('{$_SESSION['id']}', '{$to_message_id}', '{$text}', '{$time}')"); if(DB::AffectedRows() > 0) { return 1; } else { return DB::LastError(); } } if(isset($_POST['time'])) { $time = floatval($_POST['time']); $current = time(); $list = ''; while (time() - $current < 15) { $result = self::findAll("WHERE (`timestamp` > '{$time}' AND `user_id` != '{$_SESSION['id']}')"); if(count($result) > 0) { $result = array_reverse($result); $html = ''; $to = ''; foreach ($result as $obj) { $qt = ''; $text = ''; if(strpos($obj->text, '[_::_]')) { $array = explode('[_::_]', $obj->text); $text = $array[0]; $q = explode(":", $array[1]); $qt = '<div class="qt">
<div class="from" style="color: '. ($_SESSION['username'] == trim($q[0]) ? $_SESSION['userColor'] : '#000')
.' !important;">'
. ($_SESSION['username'] == trim($q[0]) ? 'Me' : Base::HtmlEscape($q[0])) .':</div>'
. Base::HtmlEscape($q[1]) .'</div>'; } else { $text = $obj->text; } $html .= '<div class="inner quote" from="'
. ($_SESSION['id'] == $obj->user_id ? "no" : $obj->user_id . '-'
. $obj->message_id) .'">'. $qt .'<div class="from" style="color: '. $obj->color .' !important;">'
. Base::HtmlEscape($obj->username) .':</div>'. Base::convertToImg(Base::HtmlEscape($text)); $html .= '<div style="height: 20px;"></div>'; $html .= '<div class="time">'.
Jdf::jdate('H:i:s | l , j F Y', (explode('.', $obj->timestamp)[0])). '</div>'; $html .= '</div>'; if ($_SESSION['id'] == $obj->to_message_id) { $to .= '<div class="innerMyMessages quote" from="'
. $obj->user_id . '-' . $obj->message_id .'">'. $qt .'<div class="from" style="color:
'. $obj->color .' !important;">'. Base::HtmlEscape($obj->username)
.':</div>'. Base::convertToImg(Base::HtmlEscape($text)) .'</div>'; } } return json_encode(array('message' => $html, 'to' => $to ,
'time' => $obj->timestamp, 'onlineUsers' => substr($list, 0, -1))); } else { sleep(2); } $sess = Loader::load('Session'); $onlineUsers = $sess->onlineUsers(); foreach ($onlineUsers as $user) { $list .= $user . '.'; } } return json_encode(array('onlineUsers' => substr($list, 0, -1))); } }

دو تا شرط وجود داره.
یکی وجود متغیر text و شرط دیگه وجود متغیر time. اگر متغیر تکست وجود داشت یعنی کاربر پیامی ثبت کرده.
در قسمت دیتای تابع ajax , ما text رو ارسال کردیم.پس وارد این قسمت از متد میشه:

 if(isset($_POST['text'])) 
      { 
         $text = DB::Escape($_POST['text']); 
         $time = microtime(true); 
         $to_message_id = (isset($_POST['to']) && $_POST['to'] > 0 ? intval($_POST['to']) : 0); 
         DB::Query("INSERT INTO `messages` 
(`user_id`, `to_message_id`, `text`, `timestamp`)
VALUES ('{$_SESSION['id']}', '{$to_message_id}', '{$text}', '{$time}')"); if(DB::AffectedRows() > 0) { return 1; } else { return DB::LastError(); } }

و عملیات ثبت پیام در دیتابیس انجام میشه.
بهتر بود بعد از عملیات insert , بجای return 1 عمل سلکت هم انجام میدادم و نتایج جدید return میکردم. کل ماجرای سند پیام جدید اینه.بقیه ی کدهایی که در فایل send.js اومده , تجزیه و بررسی متن وارد شده ی کاربره.

پاسخ به سوال 
محسن موحد  9 سال پیش
+7 0

دریافت پیام کاربران در حین چت: (فایل display.js)

بعد از ورد به صفحه ی چت و لود صفحه تابع displayMessage اجرا میشه:

 $(document).ready(function() { 
   $(window).load(displayMessage(lastTimestamp)); 
});

ین تابع یک درخواست به سمت سرور ارسال میکنه و ما در سمت سرور تا زمانی که رکورد جدیدی از دیتابیس پیدا نکنیم حلقه ای رو سمت سرور تکرار میکنیم و پاسخی به سمت کلاینت نمیفرستیم.
به محض دریافت رکورد جدید , پاسخ به سمت کاربر ارسال میشه و پاسخ دریافت شده در رویداد success بعد از چند بررسی به کاربر نمایش داده میشود و بعد از نمایش پیام های جدید یک وقفه ی چند ثانیه با تابع settimeout ایجاد میکنیم و دوباره تابع displayMessage رو صدا میزنیم تا همین مرحله ها از ابتدا تکرار شوند و درخواستی سمت سرور ارسال شود.
یعنی با کامت request ها و response ها رو به حداقل رسوندیم.

تابع displayMessage:

 function displayMessage(time) 
{ 
   $.ajax({ 
      url: 'http://localhost/chat/display', 
      async: true, 
      type: "POST", 
      data: 'time=' + time, 
      cache: false, 
      dataType: 'json', 
      success: function (msg) { 
         //alert(msg['message']);return; 
         if(msg['message']) 
         { 
            if($('.context').scrollTop() + $('.context').innerHeight() >= $('.context')[0].scrollHeight) 
            { 
               $(".context").animate({ scrollTop: $('.context')[0].scrollHeight}, 1000); 
            } 
            $('.context').append(msg['message']); 
            if(msg['to'] != '') 
            { 
               $('.my-messages').append(msg['to']); 
            } 
            time = msg['time']; 
            //alert(time) 
         } 
         if(msg['onlineUsers']) 
         { 
            var users = msg['onlineUsers'].split('.'); // point 
            var dvUsers = ''; 
            users = array_unique(users); 
             
            $('.users .innerUsers').each(function() { 
               dvUsers += $(this).text().trim() + ','; 
            }); 

            dvUsers = dvUsers.split(','); 
            dvUsers = dvUsers.filter(function(a){return  a != '';}); 

            for (var i = 0; i < users.length; i++) 
            { 
               if($('.users').html().indexOf(users[i]) == -1) 
               { 
                  $('<div class="innerUsers '+ users[i] +'">'+ users[i] +'</div>').appendTo('.users').fadeIn(1000); 
               } 

               if(dvUsers.indexOf(users[i]) != -1) 
               { 
                  delete dvUsers[dvUsers.indexOf(users[i])]; 
                  dvUsers = dvUsers.filter(function(a){return typeof a !== 'undefined';}); 
               } 
            }; 

            if(dvUsers.length > 0) 
            { 
               $.each(dvUsers, function(index, value) { 
                  $('.' + value).remove(); 
               }); 
            } 
         } 
         setTimeout(displayMessage, 2000, time); 
      }, 
      error: function (msg) { 
         displayMessage(time); 
      } 
   }); 
} 

این تابع یک time میگیرد.
در واقع مقدار time برابر time آخرین رکوردیست که از دیتابیس فراخوانی شده و نمایش داده شده. زمانیکه پیج چت را باز میکنیم , در صفحه ی main.php گفته شده 100 پیام آخر داخل دیتابیس , در صفحه نمایش داده شود.
و در ادامه ی همین کدهای main.php این کدو گذاشتم:

<script> 
         var lastTimestamp = <?php echo (isset($obj) ? $obj->timestamp : 0); ?>; 
</script>

متغیری در جاواسکریپت تعریف کردم و مقدارشو برابر timestamp آخرین رکورد fetch شده قرار دادم.
این timestamp بعنوان اولین مقدار time و در اولین اجرای تابع displayMessage قرار میگیره.
در فراخوانی های بعدی تابع این مقدار جایگزین میشه. بعد از اجرای displayMessage آجاکس فراخوانی میشه و در خواستی به همان آدرس قبلی یعنی display به صفحه ی index.php ارسال میشه که حاوی دیتای time است.

در صفحه ی index.php و شرط switch بازهم شرط display اجرا میشود. متد display فراخوانی میشود و از بین دو شرطی که در پست قبل گفتیم , کد شرط وجود time اجرا میشود:

 if(isset($_POST['time'])) 
      { 
         $time = floatval($_POST['time']); 
         $current = time(); 
         $list = ''; 
         while (time() - $current < 15) 
         { 
            $result = self::findAll("WHERE (`timestamp` > '{$time}' AND `user_id` != '{$_SESSION['id']}')"); 
            if(count($result) > 0) 
            { 
               $result = array_reverse($result); 
               $html = ''; 
               $to = ''; 
               foreach ($result as $obj) 
               { 
                  $qt = ''; 
                  $text = ''; 
                  if(strpos($obj->text, '[_::_]')) 
                  { 
                     $array = explode('[_::_]', $obj->text); 
                     $text = $array[0]; 
                     $q = explode(":", $array[1]); 
                     $qt = '<div class="qt"><div class="from" style="color: '
. ($_SESSION['username'] == trim($q[0]) ? $_SESSION['userColor'] : '#000')
.' !important;">'. ($_SESSION['username'] == trim($q[0]) ? 'Me' : Base::HtmlEscape($q[0]))
.':</div>'. Base::HtmlEscape($q[1]) .'</div>'; } else { $text = $obj->text; } $html .= '<div class="inner quote" from="'
. ($_SESSION['id'] == $obj->user_id ? "no" : $obj->user_id . '-'
. $obj->message_id) .'">'. $qt .'<div class="from" style="color: '. $obj->color .' !important;">'
. Base::HtmlEscape($obj->username) .':</div>'
. Base::convertToImg(Base::HtmlEscape($text)); $html .= '<div style="height: 20px;"></div>'; $html .= '<div class="time">'
. Jdf::jdate('H:i:s | l , j F Y', (explode('.', $obj->timestamp)[0])). '</div>'; $html .= '</div>'; if ($_SESSION['id'] == $obj->to_message_id) { $to .= '<div class="innerMyMessages quote" from="'
. $obj->user_id . '-' . $obj->message_id .'">'. $qt
.'<div class="from" style="color: '. $obj->color .' !important;">'
. Base::HtmlEscape($obj->username) .':</div>'
. Base::convertToImg(Base::HtmlEscape($text)) .'</div>'; } } return json_encode(array('message' => $html, 'to' => $to ,
'time' => $obj->timestamp, 'onlineUsers' => substr($list, 0, -1))); } else { sleep(2); } $sess = Loader::load('Session'); $onlineUsers = $sess->onlineUsers(); foreach ($onlineUsers as $user) { $list .= $user . '.'; } } return json_encode(array('onlineUsers' => substr($list, 0, -1))); }

کل کدها به کنار قسمت اصلی کد , حلقه ی while با تکرار به مدت 15 ثانیه هست:

 while (time() - $current < 15) 

هر بار که این حلقه اجرا میشود , یک کوئری اجرا میشود:

 $result = self::findAll("WHERE (`timestamp` > '{$time}' AND `user_id` != '{$_SESSION['id']}')"); 

این کوئری تمام رکوردهایی که timestamp اشون بزرگتر از time ارسالی توسط آجاکس(یعنی time آخرین رکورد نمایش داده شده در صفحه ی کاربر) است رو واکشی میکنه.البته این شرط هم وجود داره که پیام هایی واکشی بشن که فرستندشون خود کاربر نباشن.(چون پیام های خود کاربر به محض سند شدن رو صفحه ی خودش یکبار نمایش داده شده.اینجا فرضو براین گرفتم که یک یوزر با یک مرورگر قراره باز بشه و بصورت همزمان کسی نباید باز کنه.)
میشد شرطو جابجا کرد و اول user_id چک بشه و بعد AND بشه با شرط timestamp. چون اگه شرط user_id برقرار نبود دیگه شرط timestamp اجرا نشه و باعث سرعت بیشتری در کوئری بشه.

خلاصه بعد از عمل fetch کردن اگر رکوردی پیدا شد if اجرا میشه و تمام پیام های جدید واکشی شده بسمت کلاینت return میشن و در success دریافت و نمایش داده میشن و بعد از چند ثانیه دوباره یک درخواست جدید بهمراه آخرین timestamp بسمت سرور فرستاده میشه.
اما اگر رکوردی یافت نشد یعنی پیغامی ثبت نشده , بنابراین else اجرا میشود. در قسمت else :

else 
{ 
      sleep(2); 
}

به مدت دوثانیه وقفه ایجاد کردیم تا به سرور فشار زیادی وارد نشه. و بعد از else دوباره این حلقه اجرا میشه.
این حلقه در مدت 15 ثانیه ای که تکرار میشه همین روالو ادامه میده.
اگر رکوردی پیدا شد ارتباط قطع میشه قطع میشه و پاسخ به سمت کلاینت فرستاده میشود. اما اگر در مدت 15 ثانیه رکوردی پیدا نشد این اسکریپت به پایان میرسه و تنها تعداد کاربران آنلاینو return میکنه.

یک نکته در مورد time: در اول کار , زمان رو برحسب ثانیه وارد دیتابیس میکردم.ولی در تست هایی که انجام دادم , وقتی سریع و پشت سرهم مثلا عدد 1 تا 9 ارسال میکردم , در نمایش پیام ها قاطی میکرد.بعضی هارو هم جا مینداخت. یعنی در دیتابیس درست درج شده بود ولی در نمایش به مشکل میخورد. واسه همین فیلد timestamp رو double گذاشتم و مقدار تایم هر پیام رو با microtime ذخیره کردم.


در مورد comet و این اسکریپت اگه سوالی بود , توو همین تاپیک بذارید.

0 0
سلام دوست عزیز. خیلی جالب بود. فقط 1 سوال. شما میای یه کانکشن برقرار میکنی و live نگهش میداری تا جوابی بیاد. برای هاست های اشنراکی که مثلا تعداد بازدید در لحظه اشون 10 تاست ، این سیستم تنها برای 10 نفر آنلاین جواب میده درسته؟ (9 سال پیش)
+3 0
سلام.بنچمارکی که گرفتم روی 10 تا خوب جواب میداد ولی در کل 10 تا 15 رو جواب میده. ولی قطعا هاست اشتراکی بدرد این کار نمیخوره. (9 سال پیش)
0 0
ممنون از جوابتون. اگه بخوایم یه سیستم چت راه بندازیم (در حد مثلا 20 نفر) چه برنامه ای رو پیشنهاد میدید؟ من openfire رو شنیدم خوبه برای این کار و روی سرورم نصب کردم ولی خیلی سر در نیاوردم ازش. ممنون میشم نظرتون رو بگید. (9 سال پیش)
+2 0
متأسفانه با سیستم های دیگه کار نکردم و آشنایی ندارم. (9 سال پیش)

پاسخگویی و مشاهده پاسخ های این سوال تنها برای اعضای ویژه سایت امکان پذیر است .
چنانچه تمایل دارید به همه بخش ها دسترسی داشته باشید میتوانید از این بخش لایسنس این آموزش را خریداری نمایید .