COMET探索系列一【COMET实践笔记】
这几天在给公司的一个点对点聊天系统升级,之前只是使用简单的ajax轮询方式实现,每5秒钟取一次数据,延时太长,用户体验不是很好,因此打算采用服务器推送技术,故此整理了以下文档,将自己找到的一些资料及心得与大家在此分享。本文主要综述了Comet相关的概念、应用场景、常用的两种实现模型、及PHP实现代码。
概 念:Comet,基于 HTTP 长连接的“服务器推”技术,是一种 Web 应用程序的架构。基于这种架构开发的应用中,服务器端会主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。
其他别名:服务器推送技术(Server Push),反向Ajax
应用场景:Comet 架构非常适合事件驱动的 Web 应用以及对交互性和实时性要求很强的应用,如实时监控、股票交易行情分析、聊天室和 Web 版在线游戏等。
实现模型:基于 Comet 架构的 Web 应用使用客户端和服务器端之间的 HTTP 长连接来作为数据传输的通道。每当服务器端的数据因为外部的事件而发生改变时,服务器端就能够及时把相关的数据推送给客户端。通常来说,有两种实现长连接的模型:
1. 基于 Iframe 的流方式(streaming)
基本原理: 在页面中加入一个Iframe标签,Iframe的src发起到服务器的连接,服务器端将其搁置,这样就建立了一个服务器端到客户端的通道,每当服务器端有数据时直接将数据经由此通道发送。
数据处理:这种方式服务器端返回的数据一般为类似“<script type="text/javascript">js_func(“json_data”)</script>”的JS脚本,其中js_func为写在母页面中的一个用来处理返回结果的回调函数,服务器端将返回的数据作为回调函数的参数,浏览器在收到数据后就会执行这段JS脚本。
优点: 具有高兼容特性
缺点: 使用 iframe 请求一个长连接有一个很明显的不足之处是有些浏览器会显示加载没有完成,一直处加页面加载中。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。
2. 基于 AJAX 长轮询方式(long-polling)
基本原理: 客户端发起一个ajax请求,服务器端将该请求搁置(pending)或者说挂起,直到服务器端有数据需要推送时返回数据并断开连接,客户端在接收ajax返回后处理数据,同时再次发起下一个ajax请求。
数据处理:通过 ajax 的回调函数来进行数据处理。
优点: 兼容性较高,实现简单
缺点: 对于php这种语言来说,如果要做到实时,那么服务端就要承受大得多的压力,因为搁置到什么时候往往是不确定的,这就要php脚本每次搁置都进行一个while循环。
注意: 浏览器有连接数限制。我得出的结论是如果当前页面上有一个ajax请求处于等待返回状态,那么其他ajax请求都会被搁置(Chrome, Firefox已测)。如果页面有一般ajax需求怎么办?解决方法是开个框架,框架中使在另一个域名下进行Comet长轮询,需要注意跨域问题。
未来方向:在HTML5标准中,定义了客户端和服务器通讯的WebSocket方式,在得到浏览器支持以后,WebSocket将会取代Comet成为服务器推送的方法,目前chrome、Firefox、Opera、Safari等主流版本均支持,Internet Explorer从10开始支持。
注意事项:对于一个实际的应用而言,系统的稳定性和性能是非常重要的。将 HTTP 长连接用于实际应用,很多细节需要考虑。
1. 不要在同一客户端同时使用超过两个的 HTTP 长连接
HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接,超过之后,新的连接会被阻塞。HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web 应用带来如下现象:在客户端如果打开超过两个的 IE 窗口去访问同一个使用了长连接的 Web 服务器,第三个 IE 窗口的 HTTP 请求被前两个窗口的长连接阻塞。所以在开发长连接的应用时, 必须注意在使用了多个 frame 的页面中,不要为每个 frame 的页面都建立一个 HTTP 长连接,这样会阻塞其它的 HTTP 请求,在设计上考虑让多个 frame 的更新共用一个长连接。
2. 控制信息与数据信息使用不同的 HTTP 连接
使用长连接时,存在一个很常见的场景:客户端需要关闭页面,而服务器端还处在读取数据的阻塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP 连接,才能使控制请求不会被阻塞。在实现上,如果是基于 iframe 流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被阻塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX 的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest 请求,通知服务器端关闭数据连接。
补充:关于如何通知服务器关闭数据连接请看,络络的另一篇文章:http://www.cnblogs.com/hackboy/p/3735070.html
3. SESSION锁定问题
对于上述第二点并不是十分理解,如何通过向服务器端发送控制请求?如何通知服务器端关闭数据连接?服务器端又怎么样关闭数据连接?经过我的实践发现,在服务器端如果使用了session,刷新页面时,页面会阻塞直到长连接超时返回方能重新加载页面,不知道这是不是上一点中所描述的问题?在PHP手册中查到一段英文,翻译过来大致是这样子的:默认情况下,session会在脚本执行完毕之后自动进行存储,脚本在操作session时session会被锁定,这意味着在同一时刻只有一个脚本可以操作session。由于该锁定的存在,当长连接被服务器端搁置期间,session会被一直锁定占用,从而导至其它请求无法打开session,从而造成页面阻塞。你当然可以关闭网站的session功能,但这不现实,那么有没有一种方法可以在session操作完之后就及时将其存储并关闭呢?通过session_write_close函数,你可以在对session的操作完成之后但脚本尚未执行完毕之前及时将session存储并释放。于是在服务器端程序进行搁置前加入session_write_close函数之后成功解决了长连接导致的页面阻塞问题。
4. 在客户和服务器之间保持心跳信息
在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性,因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源。因此需要一种机制使双方知道大家都在正常运行。在实现上:服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。
代码示例1.1 Comet-ajax-backend.php(ajax长轮询服务器端代码)
<?php
//不限制超时时间
set_time_limit(0);
//在开启session的应用中这个函数非常重要,防止页面因session占用阻塞
session_write_close();
//用来存放数据的文件
$filename = './data.txt'; // 如果有传递将消息存入文件
$msg = isset($_GET['msg']) ? $_GET['msg'] : '';
if ($msg != '') {
file_put_contents($filename, $msg);
die();
} $content = file_get_contents($filename);
// $msg是否为空
while ($content=='')
{
sleep(1);
$content = file_get_contents($filename);
}
//清空data.txt
file_put_contents($filename, '');
// 取到消息返回
$response['msg'] = $content;
echo json_encode($response); ?>
代码示例1.2 Comet-ajax-index.html(ajax长轮询客户端代码)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Comet demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script type="text/javascript" src="jquery.js"></script>
<script> var comet = {
url:'backend.php',
error:false,
connect : function(){
$.ajax({
url: comet.url,
type: 'post',
dataType: 'json',
timeout: 0,
success: function (response) {
comet.error = false;
$("#content").append('<div>' + response.msg + '</div>');
}, error: function () {
comet.error = true;
}, complete: function () {
if (comet.error) {
setTimeout(function () {
comet.connect();
}, 5000);
} else {
//alert(comet.timestamp);
comet.connect();
}
}
})
}
} // 发送消息函数
function send(msg) {
$.ajax({
data: {'msg': msg},
type: 'get',
url: comet.url
})
} $(document).ready(function () {
//页面加载完毕创建长连接
comet.connect();
});
</script>
</head>
<body>
<div id="content">
</div>
<p>
<form action="" method="get" onsubmit="send($('#word').val());$('#word').val('');return false;">
<input type="text" name="word" id="word" value=""/> <input type="submit" name="submit" value="Send"/>
</form>
</p>
</body>
</html>
代码示例2.1 Comet-iframe-backend.php(iframe服务器端代码)
<?php header("Cache-Control: no-cache, must-revalidate");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
//关闭PHP缓存
ob_end_flush();
//将内容强制冲刷到浏览器
flush();
//不限制超时时间
set_time_limit(0);
//在开启session的应用中这个函数非常重要,防止页面因session占用阻塞
session_write_close(); ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Comet php backend</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body> <script type="text/javascript">
// KHTML browser don't share javascripts between iframes
var is_khtml = navigator.appName.match("Konqueror") || navigator.appVersion.match("KHTML");
if (is_khtml)
{
var prototypejs = document.createElement('script');
prototypejs.setAttribute('type','text/javascript');
prototypejs.setAttribute('src','prototype.js');
var head = document.getElementsByTagName('head');
head[0].appendChild(prototypejs);
}
// load the comet object
var comet = window.parent.comet;
</script> <?php while(1) {
echo '<script type="text/javascript">';
echo 'comet.printServerTime('.time().');';
echo '</script>'; // 强制将数据发送到浏览器
flush();
// 休息一秒钟
sleep(1);
} ?> </body>
</html>
代码示例2.2 Comet-iframe-index.html(iframe客户端代码)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Comet-iframe-demo</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<div id="content">The server time will be shown here</div> <script type="text/javascript">
var comet = {
connection : false,
iframediv : false,
iframesrc : './backend.php', initialize: function() {
if (navigator.appVersion.indexOf("MSIE") != -1) { // 创建争取IE浏览器的iframe
comet.connection = new ActiveXObject("htmlfile");
comet.connection.open();
comet.connection.write("<html>");
comet.connection.write("<script>document.domain = '"+document.domain+"'");
comet.connection.write("</html>");
comet.connection.close();
comet.iframediv = comet.connection.createElement("div");
comet.connection.appendChild(comet.iframediv);
comet.connection.parentWindow.comet = comet;
comet.iframediv.innerHTML = "<iframe id='comet_iframe' src='"+comet.iframesrc+"'></iframe>"; } else if (navigator.appVersion.indexOf("KHTML") != -1) { // 创建争对KHTML浏览器的iframe
comet.connection = document.createElement('iframe');
comet.connection.setAttribute('id', 'comet_iframe');
comet.connection.setAttribute('src', comet.iframesrc);
with (comet.connection.style) {
position = "absolute";
left = top = "-100px";
height = width = "1px";
visibility = "hidden";
}
document.body.appendChild(comet.connection); } else { // 创建争对其它浏览器的iframe
comet.connection = document.createElement('iframe');
comet.connection.setAttribute('id', 'comet_iframe');
with (comet.connection.style) {
left = top = "-100px";
height = width = "1px";
visibility = "hidden";
display = 'none';
}
comet.iframediv = document.createElement('iframe');
comet.iframediv.setAttribute('src', comet.iframesrc);
comet.connection.appendChild(comet.iframediv);
document.body.appendChild(comet.connection); }
}, // 重新加载页面时释放iframe,防止IE浏览器出错
onUnload: function() {
if (comet.connection) {
comet.connection = false;
}
}, // 数据处理回调函数
printServerTime: function (time) {
$('#content').html(time);
} } $(function(){
//页面加载完毕后创建iframe到服务器的长连接
comet.initialize();
$(window).unload(function(){comet.onUnload();});
}) </script> </body>
</html>
本文文档及代码示例下载:http://yun.baidu.com/s/1c0ovV6w
COMET探索系列一【COMET实践笔记】的更多相关文章
- COMET探索系列三【异步通知服务器关闭数据连接实现思路】
在小编络络 COMET实践笔记一文中注意事项中有这么一段话 使用长连接时, 存在一个很常见的场景:客户端需要关闭页 面,而服务器端还处在读取数据的阻塞状态,客户端需要及时通知服务器端关闭数据连接.服务 ...
- COMET探索系列二【Ajax轮询复用模型】
写在前面:Ajax轮询相信大家都信手拈来在用,可是有这么一个问题,如果一个网站中同时有好多个地方需要用到这种轮询呢?就拿我们网站来说,有一个未读消息数提醒.还有一个时实时加载最新说说.昨天又加了一个全 ...
- .net EntityFramework用法探索系列 1
EntityFramework用法探索系列 (一)DatabaseFirst (二)CodeFirst (三)CodeFirst流畅API (四)Repository和UnitOfWork (五)引入 ...
- 《python编程从入门到实践》读书实践笔记(一)
本文是<python编程从入门到实践>读书实践笔记1~10章的内容,主要包含安装.基础类型.函数.类.文件读写及异常的内容. 1 起步 1.1 搭建环境 1.1.1 Python 版本选择 ...
- hadoop2.5.2学习及实践笔记(二)—— 编译源代码及导入源码至eclipse
生产环境中hadoop一般会选择64位版本,官方下载的hadoop安装包中的native库是32位的,因此运行64位版本时,需要自己编译64位的native库,并替换掉自带native库. 源码包下的 ...
- RxJava系列7(最佳实践)
RxJava系列1(简介) RxJava系列2(基本概念及使用介绍) RxJava系列3(转换操作符) RxJava系列4(过滤操作符) RxJava系列5(组合操作符) RxJava系列6(从微观角 ...
- Python编程从入门到实践笔记——异常和存储数据
Python编程从入门到实践笔记——异常和存储数据 #coding=gbk #Python编程从入门到实践笔记——异常和存储数据 #10.3异常 #Python使用被称为异常的特殊对象来管理程序执行期 ...
- Python编程从入门到实践笔记——文件
Python编程从入门到实践笔记——文件 #coding=gbk #Python编程从入门到实践笔记——文件 #10.1从文件中读取数据 #1.读取整个文件 file_name = 'pi_digit ...
- Python编程从入门到实践笔记——类
Python编程从入门到实践笔记——类 #coding=gbk #Python编程从入门到实践笔记——类 #9.1创建和使用类 #1.创建Dog类 class Dog():#类名首字母大写 " ...
随机推荐
- python3.4+Django+pymysql
pip install Pymysql 修改app里面的__init__.py import pymysqlpymysql.install_as_MySQLdb()
- JAVAEE 和项目开发(第一课:浏览器和服务器的交互模式和HTTP协议的概念和介绍)
互联网的发展非常迅速,但是万变不离其宗.学习 web 开发,需要我们对互 联的交互机制有一定的了解.为了更好的理解并掌握 Servlet,在正式学习 Servlet之前需要对 web 开发中客户端和服 ...
- Bugku 社工
1.密码 姓名:张三 生日:19970315 猜想KEY是:zs19970315. 结果就是如此.
- 安装adobe reader阅读器
首先 在我的网盘里有那个软件. 安装的教程在这个歌网址:http://www.zhanshaoyi.com/6730.html
- python刷LeetCode:3.无重复字符的最长子串
难度等级:中等 题目描述: 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度. 示例 1: 输入: "abcabcbb"输出: 3 解释: 因为无重复字符的最长子串是 ...
- Thread--线程间通信--管道
在Java语言中提供了各种各样的输入/输出流Stream,使我们能够方便的对数据进行操作,其中管道流是一种特殊的流,用于在不同线程间直接传送数据.一个线程发送数据到输出管道,另一个线程从输入管道中读数 ...
- 30 docker swarm service 的创建维护和水平拓展
运行环境在上两篇文章中已经搭建 1. 创建一个service (与 docker run 类似 ,创建一个 container) docker service create --name demo b ...
- 三大PLM厂商
西门子的叫Teamcenter(汽车.通用机械) 法国达索的Enovia(航空\汽车\高铁等交通运输行业和机械行业) PTC的叫windchill(船舶.电子)
- 函数返回值return
#函数后面如果没有return系统会默认return none def ff(): print("打印return") return 15 # 函数在执行中遇到return就会停止 ...
- Java机器学习软件介绍
Java机器学习软件介绍 编写程序是最好的学习机器学习的方法.你可以从头开始编写算法,但是如果你要取得更多的进展,建议你采用现有的开源库.在这篇文章中你会发现有关Java中机器学习的主要平台和开放源码 ...