PDA

View Full Version : اسکریپت چت با استفاده از COMET بهمراه دمو



***BiDaK***
سه شنبه 14 بهمن 1393, 09:50 صبح
سلام.
از عنوان تاپیک مشخصه.
اینم لینک دمو (http://demo.bidakplus.ir/login).
-------------------------------
بعدا توضیحات کدهایی که نوشتمو میزارم توو تاپیک.هنوز خیلی کارهای دیگه میشه روش انجام داد ولی الان چارچوبه کلیش مهم بود.
تست کنید نظراتتونو هم بگید و مد نظر داشته باشین توو دو سه روز وایبرو ... در نمیاد.یکم ملایم انتقاد کنید:لبخند:
یه نکته: اگه خواستین روی چنتا مرورگر تست کنید با یوزر متفاوت لاگین کنید چون پیام های یک یوزر توو چنتا مرورگر نمیاد.فرضو براین گرفدم یک یوزر قرار نیست همزمان از چند مرورگر باز بشن.

***BiDaK***
پنج شنبه 16 بهمن 1393, 00:22 صبح
دوستان هر نظری و هر راه بهتری داشتین بگین.
یاد اشپیلن بخیر :دی پایه ی اینجور تاپیکا بود.http://yoursmiles.org/tsmile/no/t2605.gif
-----------------------------------------------------------------
خوب قسمت ارسال پیام , با استفاده از ajax انجام شده.
فایل send.js و تابع sendMessage:

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 بدیم تا درخواست ارسال پیام اجرا بشه http://aftab.cc/modules/Forums/images/smiles/icon_surprised.gif )
در ادامه وارد متد 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 اومده , تجزیه و بررسی متن وارد شده ی کاربره.

***BiDaK***
پنج شنبه 16 بهمن 1393, 00:49 صبح
دریافته پیام بصورت comet پیاده شده توو فایل display.js
بعد از ورد به صفحه ی چت و لود صفحه تابع displayMessage اجرا میشه:

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

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

تابع 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 ذخیره کردم.

Mohammadsgh
پنج شنبه 16 بهمن 1393, 09:57 صبح
اگه بتونی با nodejs بسازی خیلی خوب میشه.اینجوری با comet در هر صورت چک میشه که پیام اومده یا نه و هر بار page رفرش میشه