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聊天室的原理,如果是注册聊天,实行起来会比这个容易些!
具体效果参考:nbajax.com
本来没想到会有这么多人要源码,呵呵,只好放到网站上,让你们自己下载啦,自己修改一下数据库配置文件config.php,里面有sql建表。
http://www.cnscene.com/nbajax/chat/chat.zip