Comet چیست! (2)

در این پست پیاده سازی کامیت به شکل Long polling و با کمک AJAX رو نشون میدیم.

برای این منظور از یک مثال خیلی ساده شده و نمونه برنامه ای که میتونه بخشی از یک برنامهء چت باشه استفاده میکنیم. البته مثال ما برای سادگی یک طرفه هست و تنها این بخش یک برنامهء چت رو بررسی میکنیم که یک طرف پیغامی برای طرف دیگر به سرور ارسال میکنه و طرف دیگر میخواد با حداکثر سرعت و بهینگی ممکن این پیغام رو از سرور دریافت بکنه.

فرض کنید ما یک کلاینت تحت مرورگر داریم که منتظر پیغامی از سمت دیگر هست. برنامه به این صورت کار میکنه که اون طرف دیگر پیام رو به سرور میفرسته، و البته ما تا اینجا به کدش کاری نداریم و فقط به کدهای طرف دریافت کننده میپردازیم (که هم سمت سرور داره و هم سمت کلاینت)، و بعد ما اون پیام رو به روش کامیت دریافت میکنیم.

بخش در سرور ما برای ذخیرهء پیغامهای دریافتی از سمت دیگر، از یک فایل بنام msg.txt استفاده میکنیم.
البته گفتم که ما برای سادگی مثال، تا اینجا به کدش کاری نداریم و فرض میکنیم این عملیات انجام میشه و ما فقط الگوریتم و کدهای دریافت اطلاعات رو که به روش کامیت هستن نشون میدیم. یعنی شما برای تست عملی این برنامه باید پیغامهای فرضی رو خودتون بصورت دستی در فایل msg.txt وارد و Save کنید و بعد مشاهده میکنید که چطور برنامهء ما بلافاصله اون پیغامها رو از فایل بیرون میکشه و نمایش میده.

فایل msg.txt باید از قبل توسط خودتون ایجاد شده باشه و توش چیزی نباشه. ضمنا PHP باید امکان نوشتن و خواندن این فایل رو در دایرکتوری مورد نظر داشته باشه. البته اگر روی لوکال ویندوز تست میکنید از این جهت مشکل و نیاز به تنظیم خاصی نیست.

خب کد سمت سرور ما فایلی بنام get_msg.php هست با چنین محتویاتی:

<?php

header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: private, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
header('Pragma: private');
header("Pragma: no-cache");

header('Content-Type: text/html; charset=utf-8');

$msg_file='msg.txt';

clearstatcache();
while(filesize($msg_file)==0) {
usleep(500000);
clearstatcache();
}

$fp=fopen($msg_file, "r+");
flock($fp, LOCK_EX);
$msg=fread($fp, filesize($msg_file));
ftruncate($fp, 0);
fclose($fp);

echo '_-_', $msg;

?>

توضیح کد:
خطوط اول که صرفا ارسال هدرهای جلوگیری کننده از Cache و همچنین هدر اعلام انکدینگ UTF-8 هستن. البته هدرهای ضدکش ممکنه نیاز نباشن ولی از جهت اطمینان ارسال میکنیم، چون قرار نیست پاسخ درخواست ها به این اسکریپت در جایی کش بشن و کش شدن اون موجب اختلال در برنامه میشه.

دستور مهم بعدی در خط 13 فراخوانی تابع clearstatcache است.
چون ما میخوایم در فواصل زمانی کوتاه با استفاده از تابع filesize اندازهء فایل پیغامها (msg.txt) رو چک کنیم تا بفهمیم آیا پیغامی درش ذخیره شده (که در اینصورت حجمش صفر نخواهد بود) یا نه، و چون PHP اطلاعات مربوط به فایلها منجمله حجم فایل رو برای مدتی Cache میکنه و در نتیجه فراخوانی های بعدی تابع filesize همون نتایج قبلی رو که آپدیت نیستن برمیگردونه، ما با استفاده از تابع clearstatcache هربار قبل از فراخوانی تابع filesiz، اطلاعات موجود در کش داخلی PHP در ارتباط با فایلها رو پاک میکنیم یا میتونیم طور دیگه بگیم که به PHP اعلان میکنیم که وضعیت فایل رو دوباره مستقیما از سیستم عامل بگیره تا آپدیت باشه.

خب ما در یک حلقهء while با شرط filesize($msg_file)==0 مدام دور میزنیم و بین هر دور بوسیلهء دستور usleep(500000) مدت نیم ثانیه تاخیر ایجاد میکنیم. این تاخیر لازمه، وگرنه CPU دائما مشغول میشه و بار زیادی به سرور تحمیل شده و اصلا ممکنه درست کار نکنه (روی سیستم بنده که CPU usage بدون این دستور میره روی 100% و آپاچی تقریبا هنگ میکنه).
بنابراین عملا اجرای برنامهء PHP ما تا زمانی که پیغامی در فایل پیغامها ذخیره نشه و نتیجتا حجمش از صفر بیشتر نشه، داخل این حلقه دور میزنه و هیچ پاسخی هم به درخواست مرورگر داده نمیشه و بنابراین مرورگر در پشت صحنه منتظر دریافت نتایج درخواست AJAX باقی میمونه. البته این انتظار بصورت معمول تا حدود 30 ثانیه بیشتر طول نمیکشه چون بعد از اون خود PHP بر اساس محدودیت زمانی اجرای اسکریپت ها که بصورت پیشفرض 30 ثانیه هست اجرای برنامه رو خاتمه میده و یک پیام خطا رو بعنوان پاسخ به مرورگر ارسال میکنه. البته ما میتونیم بوسیله قرار دادن دستور set_time_limit(0) در ابتدای این برنامه، از این محدودیت زمانی و Timeout جلوگیری بکنیم، ولی شاید این کار از بعضی نظرها جالب نباشه و ضمنا برای کاربرد ما ضروری و دارای فایدهء قابل توجهی بنظر نمیرسه و از طرف دیگه باید برای تست کامل برنامه و ثابت شدن قدرت و انعطاف اون در مدیریت خطاها، این Timeout رو بحال خودش باقی بگذاریم.

سرانجام وقتی پیغامی در فایل ذخیره شد و ما از حلقهء while خارج شدیم، در خط 19 فایل رو برای خواندن و نوشتن باز میکنیم. از اونجایی که نمیخوایم موقع خوندن ما از فایل، پیغام جدیدی همزمان در اون نوشته بشه، بنابراین فایل رو بوسیلهء تابع flock قفل میکنیم. از فلگ LOCK_EX هم که معرف قفل انحصاری است استفاده میکنیم چون هم نمیخوایم کس دیگری همزمان محتویات فایل رو بخونه و هم خودمون میخوایم بعد از خواندن، محتویات فایل رو پاک کنیم، و این یه جور نوشتن (تغییر محتویات) در فایل محسوب میشه و بنابراین کس دیگه ای نباید در مدت نوشتن در فایل بتونه فایل رو بخونه (یا بنویسه) وگرنه ممکنه دیتای خراب دریافت کنه. البته ما در این برنامه خوانندهء دیگری هم نداریم و بنابراین این فلگ عملا مهم نیست، ولی بهتره از نظر اصول برنامه نویسی و توسعهء برنامه در آینده، از این فلگ استفاده کنیم.
ضمنا توجه کنید که قفل های تابع flock اجباری نیستن (به اصطلاح advisory هستن) و بنابراین تمام برنامه هایی که به فایل مورد نظر دسترسی پیدا میکنن باید قبل از عملیات خواندن و نوشتن، این تابع رو خودشون با فلگ مناسب اجرا کنن، وگرنه چیزی جلوی دسترسی همزمان اونها به فایل رو نمیگیره.
نکته: البته در داکیومنت آمده که این تابع در ویندوز mandatory هستن. یعنی بنظرم قفل اجباری ایجاد میکنه.

خب ما محتویات فایل رو با تابع fread خونده و در متغییر msg قرار میدیم تا بعد بعنوان پاسخ به مرورگر توسط دستور echo ارسال کنیم.
بعد از خواندن پیغام، در خط 22 هم با تابع ftruncate محتویات فایل رو پاک میکنیم که این کار حجم فایل رو به صفر میرسونه و برنامه در درخواست بعدی از جانب مرورگر دوباره تا وقتی چیزی در این فایل ذخیره بشه و حجمش بیشتر از صفر بشه در حلقهء while دور خواهد زد.

سرانجام با دستور fclose فایل رو میبندیم و البته این باعث میشه قفل flock روی فایل هم آزاد بشه. هرچند برای آزاد کردن قفل و از نظر روش برنامه نویسی اصولی تر میتونید از تابع flock با فلگ LOCK_UN هم استفاده کنید.

در خط آخر، پیام رو با دستور echo ‘_-_’, $msg با اضافه کردن سه کاراکتر دیگه به ابتدای اون به نحوی که میبینید، به مرورگر ارسال میکنیم.
علت اضافه کردن این رشتهء خاص به ابتدای پیام طرف دیگر اینه که بعد در سمت مرورگر بتونیم پیامهای خطا مثلا ناشی از خطای Timeout رو از پیامهای طرف دیگه تشخیص بدیم. بنظرم این یه روش ساده اما در عین حال بقدر کافی مطمئن و بهینه برای کاربرد ما باشه. بخصوص که Resposne status پیام خطای Timeout ای که PHP تولید میکنه هم 200 هست و فرقی با پاسخهای عادی نداره و بنابراین نمیشه از روی کد وضعیت پاسخ HTTP تشخیص بدیم که پاسخ حاوی متن یک پیام خطا هست یا پیام طرف دیگر.

خب تا اینجا بخش سمت سرور کد دریافت کنندهء پیامها به روش کامیت تموم شد.

میرسیم به بخش کلاینت و تحت مرورگر.

کد زیر رو در فایلی با نام client.php ذخیره میکنیم و همین فایل هست که در مرورگر باز میکنیم:

<?php
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: private, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
header('Pragma: private');
header("Pragma: no-cache");

header('Content-Type: text/html; charset=utf-8');
?>
<html>
<head>
<script>
var comet_xhr;

function create_xhr() {
var xhr;
if(window.XMLHttpRequest) xhr = new XMLHttpRequest();
else if (window.ActiveXObject) xhr = new ActiveXObject("Microsoft.XMLHTTP");
return xhr;
}

function receive_state() {
if(comet_xhr.readyState!=4) return;
msg=comet_xhr.responseText;
if(msg.indexOf('_-_')==0) {
msg=msg.substring(msg.indexOf('_-_')+3);
txt.value=txt.value+"\n"+'received message: '+msg;
}
comet_xhr.open("GET", 'get_msg.php');
comet_xhr.send(null);
}

function load() {
txt=document.getElementById('txt');
txt.value='';
comet_xhr=create_xhr();
comet_xhr.onreadystatechange=receive_state;
comet_xhr.open("GET", 'get_msg.php');
comet_xhr.send(null);
}

</script>
</head>
<body onload="load();">
<center>
<span style="vertical-align: top">Messages:</span>
<textarea id="txt" style="width: 300; height: 300;"></textarea>
</center>
</body>
</html>

یک سر میریم سراغ بخش جاوااسکریپت چون تقریبا همه چیز همونه.
متغییر comet_xhr قراره شیء XMLHttpRequest ای رو که برای درخواست های comet بصورت AJAX استفاده میکنیم در خودش نگه داره.
اساسا کار ما همون AJAX هست اما با درخواست و پاسخها به روش Comet.
تابع create_xhr که مشخص هست برای ایجاد یک شیء XMLHttpRequest بکار میره.
تابع receive_state تقریبا تمام الگوریتم اصلی ما رو در خودش داره.
این تابع هنگام لود کامل صفحهء HTML، پس از ایجاد شیء XMLHttpRequest، بعنوان تابعی برای هندل کردن رویدادهای onreadystatechange اون بکار میره. یعنی هروقت وضعیت درخواست ارسال شده تغییری کرد این تابع فراخوانی میشه تا با توجه به وضعیت درخواست عمل مناسب رو انجام بده و نهایتا پاسخ رو دریافت کرده و اعمال کنه.
در خط اول این تابع ما با عبارت if(comet_xhr.readyState!=4) return بررسی میکنیم اگر وضعیت شیء XMLHttpRequest عدد 4 نبود که این عدد به معنای دریافت کامل Response هست، هیچ کاری انجام نشه و با دستور return که باعث خروج از تابع میشه از اجرای بقیهء دستورات تابع جلوگیری میکنیم.
اگر به دستورات بعدی برسیم به معنی اینه که پاسخ کامل از جانب سرور دریافت شده.
با عبارت msg=comet_xhr.responseText، متن پاسخ دریافت شده را در متغییری با نام msg ذخیره میکنیم تا بعدا اون رو بررسی و روش کار کنیم.
با عبارت if(msg.indexOf(‘_-_’)==0) چک میکنیم که آیا پاسخ دریافت شده با کاراکترهای خاصی که ما در اسکریپت سمت سرور برای تشخیص پاسخهای صحیح از پیامهای خطا به ابتدای پیام اضافه کرده بودیم شروع میشه یا نه. وقتی پیام با این کاراکترها شروع میشه ما با عبارت msg=msg.substring(msg.indexOf(‘_-_’)+3) اون کاراکترهای اضافی رو از ابتدای پیام حذف کرده و بعد با دستور txt.value=txt.value+”\n”+’received message: ‘+msg پیام دریافت شده رو به یک کادر متنی چند خطی که متغییر txt بهش ارجاع میکنه به انتهای پیامهای قبلی در یک خط جدید اضافه میکنیم.
در خطوط بعدی با دو دستور زیر:

comet_xhr.open("GET", 'get_msg.php');
comet_xhr.send(null);

چون ارتباط HTTP قبلی با دریافت پاسخش خاتمه یافته، درخواست کامیت جدیدی رو به بخش سمت سرور ارسال میکنیم و دوباره منتظر دریافت پاسخ میمونیم.

تابع load هم که مشخص هست در رویداد onload صفحه اجرا شده و این سیستم رو شروع میکنه.
این کار رو با قرار دادن ارجاع به شیء معرف کادر متن پیامهای دریافتی در متغییر txt،
پاک کردن محتویات کادر متن پیامها،
ایجاد شیء XMLHttpRequest و قرار دادن اون در متغییر comet_xhr،
تعیین تابع receive_state بعنوان هندلر رویداد onreadystatechange شیء XMLHttpReques،
باز کردن کانکشن به get_msg.php که بخش سمت سرور برنامهء ماست،
و ارسال درخواست HTTP به سمت سرور،
انجام میده.

به همین سادگی – به همین خوشمزگی!!
خب اگر سوال و مشکلی هست بفرمایید و لطفا بگید کیفیت آموزش چطور بود.
ضمنا بازم تاکید میکنم این کدها فقط یک بخش کوچک از یک نوع الگوریتم یک برنامهء چت به روش کامیت هستن و به هیچ وجه کد کامل، عملیاتی، و اصولی ای بحساب نمیان. اما در این حد هست که بشه تستش کرد و کارکرد عملی این الگوریتم و سیستمها رو مشاهده کرده.

ضمنا توجه داشته باشید که درخواست کامیت ما حتی درصورتیکه مرورگر بسته بشه در سمت سرور درحال اجرا باقی میمونه تا وقتی که پیامی دریافت بشه یا بر اثر Timeout اجرای اون خاتمه پیدا کنه. در طول این مدت آپاچی بصورت عادی Stop یا ریستارت نمیشه. بنده تست کردم و دیدم که حتی اجرای متد abort شیء XMLHttpRequest در این وضعیت تاثیری نداشت.
بخاطر همین گفتم بهتره Timeout رو برنداریم. وگرنه ممکنه مجبور بشید با Task manager ویندوز پراسسهای آپاچی رو Kill کنید.
اینها نظیر مسائلی هست که باید بدونیم و در یک برنامهء کامل نتایج اونها رو بررسی و مدیریت کنیم و مثلا کدهایی به برنامه اضافه کنیم که حتی الامکان هرچه زودتر یا موقع خارج شدن از صفحه یا از دست رفتن یک اتصال، به اجرای برنامه در سمت سرور هم خاتمه بدن.

یادتون نره برای تست برنامه باید بعد از اجرای client.php در مرورگر، پیامهای فرضی از طرف مقابل رو خودتون بصورت دستی در فایل msg.txt وارد و Save کنید و اونوقت اونها به سرعت توسط مرورگر دریافت و نمایش داده میشن. ضمنا PHP باید امکان خواندن و نوشتن در این فایل رو داشته باشه.

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

*

شما می‌توانید از این دستورات HTML استفاده کنید: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>