AJAX 聊天室實現原理終極解析
閑來無事,做了一個AJAX聊天室,以前一直想做一個,因為我和幾個朋友是Linux機子,儘管我們的機
子上都有apache伺服器,但要發送一個資訊卻不是很容易,老是要藉助用戶端,有時候吧Linux下的qq和gtalk之類的聊天軟體太麻煩,所以呢,
就寫了一個聊天室。
先說一下我實現的這個聊天室的聊天模式:
1,無須註冊,登入之類,開啟頁面就可以聊天。
2,為避免過量冗餘資訊,用戶端只擷取在一定時間以後發送的資訊,比如10秒內。
3,可以單對單聊天,僅限於一個對一個,如果想一對多同時聊天,那麼就必須要註冊登入才能解決。
這樣就簡化了一些聊天的模式了,如果想要實現例如qq,msn,gtalk之類的聊天模式,就必須要用到使用者註冊登入,這樣一來,先有的很多ajax聊天程式都已經設計的很完美了,例如:
http://blog.jwchat.org/jwchat/
http://ajaxbber.sourceforge.net/index.php?page=home
還有很多,不一一列舉。
分析一下原理:
客
戶端必須不斷的重新整理,向伺服器擷取資訊和線上會員列表。我採用session驗證,因為session夠安全,但是線上會員是存入資料庫的,因為如果線上
會員比較多,存入資料庫,遠比用原來的session直接以檔案形式儲存的要快。session存入資料庫,有專門講解的,為了簡化,這裡只用到
session_id 這個session_id其實在伺服器端還是以檔案形式儲存的,先做兩個表:
message( id , status, nick, to_uid, to_sid, to_ip, from_uid,from_sid,from_ip , message)
online(uid, sid, ip ,nick, message_id , lastupdate, tip , status , link_uid , link_sid, link_ip )
如果不實現單聊,表就可以簡化為:
message( id , nick, from_uid,from_sid , from_ip , message) ,
online(uid, sid, ip ,nick, message_id , lastupdate, tip )
為了簡化,分單獨幾道ajax請求和伺服器通訊:
1.擷取資訊,這個請求負責重新整理擷取資訊,線上使用者。 chat.php?ac=get
2.發送資訊,這個請求負責向伺服器發送資訊。 chat.php?ac=send
3.請求串連,這個地方負責實現單聊串連的。 chat.php?ac=linkto
4.處理串連,處理接收或拒絕單聊請求。 chat.php?ac=cut
先來分析第一個發送請求,擷取訊息和線上會員。
用戶端向伺服器發出請求後,這裡我採用這樣的提交形式:chat.php?ac=get
先判斷使用者是否存在?
session_start();
$sid = session_id();
$ip = $_SERVER['REMOTE_ADDR'];
$dateline = time();
// 擷取使用者當前設定的暱稱。
$nick=$_POST['nick']; // 如果你的ajax請求是get實現的,那麼這裡就改動一下。
$sql="SELECT * FROM online WHERE sid='$sid' AND ip='$ip' LIMIT 1 " ;
第一步就是要判斷使用者是否已經存在資料表中,沒有,則表示是第一次登入,需要建立立一個使用者。
這
裡同時採用了 sid 和 ip
來驗證使用者是否已經在表中存在,用ip主要是為了防止session被劫持。但如果是內網劫持,因為網關出口ip通常是一個,所以這一招基本沒太大用處,
可以不用判斷ip,後來發現我這裡其實是自找麻煩,因為有的公司的ip是不斷變化的,我一個朋友的公司ip就是在兩個直接隨機切換,所以建議還是只判斷
sid就可以了,畢竟劫持session_id的可能性還是比較小的。
如果使用者不存在,那麼新插入一條資料就可以了。
如果使用者存在,先重新整理使用者的最後更新時間,
$sql="UPDATE online SET nick='$nick',dateline='$dateline' WHERE sid='$sid' AND ip='$ip' LIMIT 1 ";
然後刪除已經離線的使用者:
$interval = 10 ; // 十秒沒用重新整理的話,就認為其已經離線了。
$sql="DELETE FROM online WHERE dateline < $dateline - $interval ";
擷取線上會員
$sql="SELECT * FROM online WHERE 1 ";
擷取使用者自己:
$sql="SELECT * FROM online WHERE sid='$sid' ";
存入變數$user中。
擷取資訊。
$message_id=$user['message_id'] ? $user['message_id'] : 0;
// 這個$user變數就是我們前面擷取的目前使用者。
$sql="SELECT
* FROM message WHERE dateline > $dateline-$interval AND id >
$message_id AND from_sid != '$sid' AND status=1 ORDER BY dateline ASC";
解釋一下,這裡只擷取目前時間-10以後的資訊,那麼以前的資訊就不會擷取了,也就是說不會像qq那樣,即使你離線了,別人發給你的資訊,在你下次登入後還能擷取到。因為我們沒用會員註冊,所有的資訊都是無定向的。
這裡用 status=1表示資訊是群聊的, status=0表示資訊是單聊的。所以如果只想實現群聊,不想實現單聊的,就不用判斷狀態了。
在
擷取資訊的同時需要更新使用者的message_id,這個標誌用來表示目前使用者已經擷取到哪條資訊的位置了,如果沒用這個標誌,那麼,每次重新整理,使用者上次
擷取過的資訊,如果在10秒內,這次還會擷取到,假設有人連續發送資訊,那麼用戶端會擷取到大量重複資訊,當然,用戶端可以用一個隱藏的文字框來儲存當前
使用者已經擷取到哪條資訊了,這個方法既笨拙,又不實用,而且我好像見到有人就是這麼做的。
while($row=mysql_fetch_array(mysql_query($sql)) ){
$message_id = $row['id'] > $message_id ? $row['id'] : $message_id ;
// 不論您是否採用這樣的迴圈方式擷取訊息,您都應該把message表的取到的最大id記錄下來。
}
下面擷取單聊的資訊。因為單聊的視窗通常會和群聊分開的。
$sql="SELECT
* FROM message WHERE dateline > $dateline-$interval AND id >
$message_id AND from_sid != '$sid' AND status=0 AND to_sid='$sid' order
by dateline asc";
好下面在擷取完單聊訊息後,仍然需要執行:
$message_id = $row['id'] > $message_id ? $row['id'] : $message_id ;
以擷取最大的訊息id。
當然了,如果您覺得麻煩,完全可以這樣:
$maxId_sql="SELECT max(id) FROM message";
因為我們每次取完訊息後,必然會把當前所有的可用資訊取完,只是這樣做增加了一次資料庫查詢。
下面,更新使用者表的 message_id;
$sql="UPDATE online SET message_id='$message_id' WHERE sid='$sid' AND ip='$ip' LIMIT 1";
好了,這樣下次再取資訊的時候,就會由這個message_id向後開始取,只取 id 比message_id大的資訊。
吐出的最終資料最好是JSON格式的,這樣也好減少流量,方便處理文本。
您總不希望別人發送給您資訊的時候,發給你這樣一個:<script> location.href="g.cn" ;</script>
這樣你的頁面啪的一聲就轉到google的首頁了!
所以在前台處理的時候需要把將要提交的資訊進行轉義:
str.replace(/</g,'<').replace(/>/g,'>').replace(/'/g,''').replace(/"/g,'"');
在php端處理的時候就比較輕鬆了:htmlspecialchars( $_POST['msg'] );
好了,資訊算是處理完成了,然後只要吐出就可以了:
header('Content-Type:text/html ; charset=UTF-8'); // 設定你的編碼。
按照某些w3c mime標準來說,json吐出的格式應該是:application/json;
當然不推薦這麼做,只需吐出 text,或html就可以了,這樣做是方便調試。
我們在後台把所有取出的資訊組合成一個大的數組:
$echo_json=array();
$echo_json['msg']=$msg ; // 取出的訊息
$echo_json['online']=$online; // 取出的使用者
echo json_encode( $echo_json );
當然,為了節約流量,最好把吐出的字母變數縮短,我就是這麼乾的。一個字母足夠了。
2。發送資訊。
$msg_status = 1; // 資訊狀態,1表示群,0表示私聊
$toip = $user['link_ip']; // 發送的目標對象資訊直接由使用者的表裡取,如果沒有單聊,省去。
$touid = $user['link_uid'];
$tosid = $user['link_sid'];
$nick = htmlspecialchars($_POST['nick']); // 這個是使用者發送訊息時的暱稱。
$message = htmlspecialchars($_POST['message']); // 訊息體。
$sql="INSERT INTO
message(status,from_uid,from_sid,from_ip,to_uid,to_sid,to_ip,nick,dateline,message)
VALUES('$msg_status','$uid','$sid','$ip','$touid','$tosid','$toip','$nick','$dateline','$message')";
插入訊息體,如果有單聊的話,需要加上判斷:
$sql="SELECT * FROM online WHERE sid='$link_sid' AND ip='$link_ip' LIMIT 1";
而如果不存在 $user2,單聊的對象,那麼,返回失敗訊息,提示使用者發送訊息已失敗,對象已斷開。
好,吐出訊息:
這裡可用簡化一下處理結果,如果發送訊息成功了,那麼什麼也不返回,如果失敗了返回0.
這樣做為了省流量。因為畢竟發送訊息成功的時候比較多,失敗比較少。在用戶端用js取出返回的資料
判斷是否為空白,為空白,則,發送成功,不為空白,則發送失敗。
echo $result;
3。單聊請求,如果不提供單聊,3,4兩條可用略過了。
這裡採用的是,向伺服器發送串連請求,然後由對方選擇是否接納。
當然如果不想讓使用者自己選擇串連某個對象,而是由伺服器自動配對,這就是當今很流行的路過聊天方式。
其實原理非常簡單,例如 luguode.com ,等等,這類聊天非常的多。
簡要說明一下由伺服器自動配對的做法:
第一步:擷取哪些使用者仍是單身:
先說明一下狀態碼表示:
online表中的status 欄位:為0表示使用者單身,為3表示使用者已經配對。1,2留著有其他用處。
$sql="SELECT * FROM online WHERE sid !='$sid' AND status=0 ";
假如我們取出一個數組:$single_onlines;
下面我們取出一個隨機的使用者:
srand((double)microtime()*1000000);
// 初始化隨機數種子,php 4以後版本據說已不在需要,
// 但很多時候,我還是需要這句才能得到正確的隨機數,shit!
$target_index = rand( 0 , count($single_onlines)-1) ; //隨機下標
$target_sid = $single_onlines[$target_index]['sid'] ;
$target_ip = $single_onlines[$target_index]['ip'] ;
// 擷取到目標的sid 了,下面同時更新使用者和我!
$sql="UPDATE
online SET status='3', link_sid='$target_sid', link_ip='$target_ip'
WHERE sid='$sid' AND ip='$ip' LIMIT 1 ";// 更新我的狀態
$sql="UPDATE online SET status='3', link_sid='$sid', link_ip='$ip'
WHERE sid='$target_sid' AND ip='$target_ip' LIMIT 1 "; // 更新目標狀態
當然了,在更新前,需要做一些簡要的判斷,例如,我自己是不是已經是3了阿,是表示我已經是和別人建立單聊了,那麼就返回一個錯誤了。
說到這類不知道您發現問題了沒?就是,當我沒有請求和別人單聊的時候,也可能會被別人啪的連上了。
簡單解釋一下:例如現在有三個人 A,B,C,我是A,這三個人都是單身,當我向伺服器發出請求串連的時候,
這個時候,伺服器隨機找出了B,而B這個時候並沒有向伺服器發出串連請求,也會被啪連上了。
所以我們就可以加一個狀態判斷。例如:status=1表示使用者正在向伺服器發出串連請求。
0,表示使用者什麼也沒有做,也不想和別人單聊。
而這個時候,就從那些狀態為1的使用者裡面找出一些,隨機串連。
如果沒有單身了,那麼先把使用者狀態更新為1,返回沒有找到的提示。
感覺很羅嗦。所以我就自己設計了一個串連--伺服器處理--用戶端處理--的這樣一個模式。
請求地址:chat.php?ac=linkto
post來的變數:
$linkto=$_POST['linkto'];// 表示要和哪個uid的使用者建立串連
如果使用者是單身狀態,那麼串連請求可用,使用者的線上列表左邊將會出現一個--串連的按鈕
這個時候,uid就派上用場了,為什麼不用session_id?因為你不可能把使用者的session_id都發送給
用戶端吧?只有發送uid了,
這個uid可用是隨機字母,也可用是隨機數字,我這裡採用的是6為隨機數字。
這裡要稍微改動一下,使用者第一次訪問的情況。
srand((double)microtime()*1000000);
$uid=rand(100000,999999);
$sql="SELECT uid FROM online WHERE uid='$uid'";
while( mysql_fetch_array( mysql_query($sql) )){
$uid=rand(100000,999999);
}// 直到產生一個唯一的uid為止.
迴圈來驗證當前產生的uid是否已經在表中存在了?如果存在,那麼繼續產生,如果不存在,那就用這個了。
定義一下狀態:
0:使用者單身
1:使用者正在發出串連請求。
2:使用者正在被串連請求。
3:使用者已經配對。
當串連請求發出的時候,需要判斷這樣幾種情況?
自己狀態是:
status=1,
我已經在請求了,這個時候可用根據需要是否允許使用者進行多次請求。
一般是允許的。因為如果不允許的話,你發送過去,對方沒有即使處理,那麼就要一直掛著等了。這樣顯然不好。當然也可以這麼做了。
status=2:
我在被請求,提示錯誤
status=3:已經成對,提示錯誤。
第二步,查詢對象是否存在,或已經掉線?
$sql=SELECT * FROM online WHERE uid='$linkto' LIMIT 1;
$user2=mysql_fetch_array(mysql_query($sql));
如果不存在$user2,證明已經掉線,返回錯誤資訊。
否則進入下一步:
目標狀態:
status=0或1
更新目標使用者,返回傳送請求成功。
$sql="UPDATE online SET link_uid='$uid2' AND link_sid='$sid2' AND link_ip='$ip2' ";
status=2,
目標已經有人請求,返回請求錯誤資訊。
status=3,目標已經成對,返回錯誤資訊
4.處理串連:
每次重新整理資訊時候,返回使用者自己的資訊:
在用戶端,用js檢測使用者狀態資訊是否為2?
如果為2,則彈出對話方塊,提示使用者是否接收或拒絕串連請求。
接收的時候,仍需要注意幾種情況:
自己的狀態判斷,和目標的狀態判斷。
最後需要增加一個重新整理使用者狀態,
if($user['status']==3){ // 重新整理單點串連
$linksid=$user['linksid'];
$linkip=$user['linkip'];
$sql="SELECT uid FROM online WHERE sid='$linksid' AND ip='$linkip' LIMIT 1";
if(!mysql_fetch_array( mysql_query($sql) ) ){
$sql="UPDATE online SET status='0',info='$info',linkuid='',linksid='',linkip='' WHERE sid='$sid' AND ip='$ip' LIMIT 1";
//更新使用者狀態,使其重新變為0。
}
}
然後還需要提供一個手動中斷連線的處理,這就比較簡單啦,直接把自己的狀態,和對方狀態更新為0,同時清楚掉link_uid....之類的
下面說說前台,基於JQuery的!
前台比較簡單。
Chat={};
Chat.get=function(){
$.post('chat.php?ac=get',{'nick':$('#nick').val()},function(data){
// 這裡把取到資料 data對象裡面的訊息追加到聊天視窗。
$.each(data.msg,function(i,msg){
//比較簡單就不多寫了。
});
//scroll('im'); 這裡加一個滾動效果,可用把視窗自動滾動到最底部。
}
setTimeout('Chat.get()',3000);
},'json');
}
function scroll(id){
var
scrollTop=document.getElementById(id).scrollHeight -
document.getElementById(id).clientHeight >= 0 ?
document.getElementById(id).scrollHeight -
document.getElementById(id).clientHeight : 0;
document.getElementById(id).scrollTop=scrollTop;
}
其他的請求就略過了,因為比較簡單。原理都差不多了。
好了,到這裡就基本講解完了一個ajax聊天室的原理,如果是註冊聊天,實行起來會比這個容易些!