最近在试验一个基于长连接的聊天程序先以基本的数据库方案用户在页面上保持着一个长连接去监听是否有新的聊天数据,理论上,获取到聊天数据后长连接中断,然后将聊天数据在页面上显示后进行下一轮长连接。但问题是,用户在保持着长连接的同时,如果用户想发言,必然需要再发送一条新的连接请求。而这条请求却会因为长连接的存在而阻塞,于是用户发言失败。页面代码<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>无标题文档</title>
</head>
<body>
<a href="javascript:void(0);" id="aaa">aaa</a>&nbsp;
<a href="javascript:void(0);" id="bbb">bbb</a>&nbsp;
<a href="javascript:void(0);" id="ccc">ccc</a>
<input type="text" id="ddd" />
<script type="text/javascript">
function test(a){
this.classname = a;
}test.prototype.load_js = function(url){ // 在当前页面head里加载js
var head = document.getElementsByTagName("head")[0];
var creater = document.createElement("script");
creater.src = url;
head.appendChild(creater);
creater.onload = creater.onreadystatechange = function(){
if(!(creater.readyState) || creater.readyState == "loaded"){
head.removeChild(creater);
}
};
}test.prototype.frame_load_js = function(url){ // 在iframe的head里加载js
var head = this.iframe.contentDocument ? this.iframe.contentDocument.getElementsByTagName("head")[0] : this.iframe.document.getElementsByTagName("head")[0];
var creater = document.createElement("script");
creater.src = url;
head.appendChild(creater);
creater.onload = creater.onreadystatechange = function(){
if(!(creater.readyState) || creater.readyState == "loaded"){
head.removeChild(creater);
}
};
}test.prototype.listen = function(){ // 长连接
var url = "b.php?classname="+this.classname+"&ram="+Math.round(Math.random()*10000);
this.frame_load_js(url);
}
test.prototype.get_alert = function(res){ // 长连接返回的结果
this.save_result = res;
alert(res["words"]);
}test.prototype.speak = function(){ // 发送消息的连接
var words = document.getElementById("ddd").value;
var url = "c.php?words="+encodeURIComponent(words)+"&classname="+this.classname+"&ram="+Math.round(Math.random()*10000);
this.frame_load_js(url);
}test.prototype.init = function(){ // 初始化
var _this = this;
// 创建一个iframe,用iframe
this.iframe = document.createElement("iframe");
this.iframe.style.display = "none";
document.getElementsByTagName("body")[0].appendChild(this.iframe);
document.getElementById("aaa").onclick = function(){ _this.listen();};
document.getElementById("bbb").onclick = function(){ _this.speak();};
}var zz = new test("zz");
zz.init();
</script>
</body>
</html>
数据库结构CREATE TABLE IF NOT EXISTS `tester` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `words` varchar(255) NOT NULL,
  `saytime` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
b.php,长连接的程序<?php
$host = '127.0.0.1';
$user = 'root';
$pwd = '123456';
$db = 'chatter';
$charset = 'utf8';
$conn = mysql_connect($host, $user, $pwd) or die('Cannot connect database!');
mysql_set_charset($charset);
mysql_select_db($db);
session_start();
$data = array();
while (empty($data)){
$time_flag = !empty($_SESSION['time_flag']) ? $_SESSION['time_flag'] : 0;
$sql = 'SELECT * FROM tester WHERE saytime >= '.$time_flag;
$query = mysql_query($sql);
$rows = mysql_num_rows($query);
if (!empty($rows)){
$rs = mysql_fetch_assoc($query);
$data = $rs;
if (!empty($data)){
echo 'parent.'.$_GET['classname'].'.get_alert('.json_encode($data).');';

$_SESSION['time_flag'] = time();
exit();
}
}
$_SESSION['time_flag'] = time();
sleep(2);
}?>c.php 用于接收发言信息的程序<?php
$host = '127.0.0.1';
$user = 'root';
$pwd = '123456';
$db = 'chatter';
$charset = 'utf8';
$conn = mysql_connect($host, $user, $pwd) or die('Cannot connect database!');
mysql_set_charset($charset);
mysql_select_db($db);
session_start();
$words = trim($_GET['words']);
$sql = 'INSERT INTO tester (words, saytime) VALUES ("'.$words.'", "'.time().'")';
$query = mysql_query($sql);
echo 'alert(1);';
?>
用ajax的方式肯定会有阻塞。于是换成了动态创建script的方式进行连接。先点击了aaa链接之后,开始保持长连接,此时点击bbb链接进行发言,则被阻塞,发言失败。协议应该是在同一域名下,最多只能有2个连接,但似乎第2个连接已经被阻塞掉了。(页面和处理程序也放在不同服务器上测试过,防止打开的页面也被算进一个连接)如果监听新聊天信息的长连接和发言的连接需要在不同服务器的话。似乎也很怪异。
参考新浪的聊天
http://live.video.sina.com.cn/room/pl
方法应该类似,通过不停往iframe里创建新的script来实现长连接聊天。
但它在保持长连接的同时,似乎后续的请求依然可以处理。问一下解决方案……

解决方案 »

  1.   

    你看看我这个
    http://hi.baidu.com/see7di/blog/item/d143f4117f6e28dea6ef3f5c.html这个好像没有你说的问题,用的是推,接近超时时间的时候重载页面,一直重复.
      

  2.   

    我以前做过和你这个一模一样的,很简单。“用ajax的方式肯定会有阻塞。于是换成了动态创建script的方式进行连接。”谁说肯定会阻塞,用 ajax 池就可以保持长连接的同时发起一个新的 ajax 连接。如果你用上 jQuery 就更方便了,因为 jQuery 的 AJAX 已经实现了 AJAX 池,默认是不阻塞模式。
      

  3.   


    这个方法用的是表单发送至iframe的方式来加载。体验是一个很大的问题,页面将会一直显示载入中。而且处理起来也是很不方便的,一般来说,聊天室并不会一直保持着一个长连接不动,而是在一个长连接获得新消息后,进行处理,然后再开始下一次的长连接。表单发送来防止阻塞有试验过,FF和chrome,前面就算有script的长连接,发送表单到一个iframe里,可以突破阻塞
    但IE和opera则表单一样会被阻塞掉导致发不出去。
    我不了解JQ,不过按我的理解,AJAX池其实就是一个AJAX的序列,将一堆AJAX请求进行排序,一个一个发出去,等第一个收到结果的时候,再发送第二个请求。
    如果是这样的话,似乎就不对了,因为第一个请求保持着长连接还未得到结果,第二个请求貌似就会发不出去。
    当然,可以把第一个请求给abort()掉,来达到发送第二个请求的目的。
    但是abort的机制只是客户端放弃接收,其实数据还是有从服务器端发送出来的。聊天室如果人少的话是可以接受,但如果人多的话,必将导致部分聊天数据被客户端抛弃。
    虽然可以设置一个时间戳进行同步,但应该不是最佳方案,例如,如果服务器是nginx,然后用的NGiNX_HTTP_Push_Module进行长连接,在输出聊天数据的话,是无法进行太多选择的,只能导致数据平白被弃。而我看http://live.video.sina.com.cn/room/pl
    并没有发现有哪个连接被abort掉,在第二个请求发送的过程中,第一个长连接总会正常接收到信息。然后再次进行新一轮长连接
      

  4.   

    经试验……似乎是因为session的关系……我把b.php里的session都删除,连session_start();都去掉。
    $time_flag一直保持为0,然后通过别的浏览器来手动删除代码。则发现长连接并不会阻塞后续请求……而如果,b.php里关于session仅保留一个session_start();则仅第一次长连接不会对后续请求阻塞。第二个长依旧会阻塞后续请求……于是就是session问题么?谁来解释一下。有无好的解决方法?(暂不考虑利用客户端传参数代替相应session值的方案的话)
      

  5.   


    你错了,不是队列,jQuery 的 AJAX 池是异步请求的,也就是在长连接没有终止时你仍然可以发起一个新的非阻塞 ajax 连接,同样可以相应请求,当然,这两条请求是完全不干扰的。所以实现起来非常方便自如。而你的回复显然是针对队列提出的疑问,但是 AJAX 池并非队列,原理是创建了多个 XMLHTTP 对象,分别发出请求,在 jQuery 中得到了更加完善的封装和浏览器兼容性支持,所以完全不用担心这些问题,只管用就好。最后重点再说一次:不是队列发送,而是你可以创建多条而互不干扰(当然,根据我试验,似乎最多同时可以6、7条(其他的会在某个结束后立马进行请求),不过这也够多了吧,平时你能同时连接3条就很不太可能了)
      

  6.   


    为什么一定要用 script 块呢,这不是吧简单问题复杂化嘛,参考 5 楼是你的最佳解决方案的。
      

  7.   


    其实这个你直接POST过去就行,顶多后台在做个判断,获取的信息必须是当前时间几小时以内的,否则不返回。这样用户也不会恶意去请求那个URL而获取全部的消息。
    当然,如果必须要用 session,那么可以尝试使用 session_write_close,因为 session_start 后,session 文件就会被锁定,自然就阻塞了,所以写完 session 就 session_write_close 关掉,这样就不会阻塞。你可以试一下循环在开始 session_start,sleep 前 session_write_close。
      

  8.   

    嗯,POST过去是可以,不过么……印象中让iframe打开新界面,容易造成内存泄漏,特别是IE下。
    确实是忘了session是会被锁的……啊,这个杯具。谢了