WebSocket 出现前
构建网络应用的过程中,我们经常需要与服务器进行持续的通讯以保持双方信息的同步。通常这种持久通讯在不刷新页面的情况下进行,消耗一定的内存资源常驻后台,并且对于用户不可见。在 WebSocket 出现之前,我们有以下解决方案:
传统轮询(Traditional Polling)
当前Web应用中较常见的一种持续通信方式,通常采取 setInterval 或者 setTimeout 实现。例如如果我们想要定时获取并刷新页面上的数据,可以结合Ajax写出如下实现:
setInterval(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
});
}, 10000);
上面的程序会每隔10秒向服务器请求一次数据,并在数据到达后存储。这个实现方法通常可以满足简单的需求,然而同时也存在着很大的缺陷:在网络情况不稳定的情况下,服务器从接收请求、发送请求到客户端接收请求的总时间有可能超过10秒,而请求是以10秒间隔发送的,这样会导致接收的数据到达先后顺序与发送顺序不一致。于是出现了采用 setTimeout 的轮询方式:
function poll() {
setTimeout(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
// 发起下一次请求
poll();
});
}, 10000);
}
程序首先设置10秒后发起请求,当数据返回后再隔10秒发起第二次请求,以此类推。这样的话虽然无法保证两次请求之间的时间间隔为固定值,但是可以保证到达数据的顺序。
长轮询(Long Polling)
上面两种传统的轮询方式都存在一个严重缺陷:程序在每次请求时都会新建一个HTTP请求,然而并不是每次都能返回所需的新数据。当同时发起的请求达到一定数目时,会对服务器造成较大负担。这时我们可以采用长轮询方式解决这个问题。
长轮询与以下将要提到的服务器发送事件和WebSocket不能仅仅依靠客户端JavaScript实现,我们同时需要服务器支持并实现相应的技术。
长轮询的基本思想是在每次客户端发出请求后,服务器检查上次返回的数据与此次请求时的数据之间是否有更新,如果有更新则返回新数据并结束此次连接,否则服务器 hold住此次连接,直到有新数据时再返回相应。而这种长时间的保持连接可以通过设置一个较大的HTTP timeout` 实现。下面是一个简单的长连接示例:
服务器(PHP):
// 示例数据为data.txt
$filename= dirname(__FILE__)."/data.txt";
// 从请求参数中获取上次请求到的数据的时间戳
$lastmodif = isset( $_GET["timestamp"])? $_GET["timestamp"]: 0 ;
// 将文件的最后一次修改时间作为当前数据的时间戳
$currentmodif = filemtime($filename);
// 当上次请求到的数据的时间戳*不旧于*当前文件的时间戳,使用循环"hold"住当前连接,并不断获取文件的修改时间
while ($currentmodif <= $lastmodif) {
// 每次刷新文件信息的时间间隔为10秒
usleep(10000);
// 清除文件信息缓存,保证每次获取的修改时间都是最新的修改时间
clearstatcache();
$currentmodif = filemtime($filename);
}
// 返回数据和最新的时间戳,结束此次连接
$response = array();
$response["msg"] =Date("h:i:s")." ".file_get_contents($filename);
$response["timestamp"]= $currentmodif;
echo json_encode($response);
?>
客户端:
function longPoll (timestamp) {
var _timestamp;
$.get("/path/to/server?timestamp=" + timestamp)
.done(function(res) {
try {
var data = JSON.parse(res);
console.log(data.msg);
_timestamp = data.timestamp;
} catch (e) {}
})
.always(function() {
setTimeout(function() {
longPoll(_timestamp || Date.now()/1000);
}, 10000);
});
}
长轮询可以有效地解决传统轮询带来的带宽浪费,但是每次连接的保持是以消耗服务器资源为代价的。尤其对于Apache+PHP 服务器,由于有默认的 worker threads 数目的限制,当长连接较多时,服务器便无法对新请求进行相应。
服务器发送事件(Server-Sent Event)
服务器发送事件(以下简称SSE)是HTML 5规范的一个组成部分,可以实现服务器到客户端的单向数据通信。通过 SSE ,客户端可以自动获取数据更新,而不用重复发送HTTP请求。一旦连接建立,“事件”便会自动被推送到客户端。服务器端SSE通过 事件流(Event Stream) 的格式产生并推送事件。事件流对应的 MIME类型 为 text/event-stream ,包含四个字段:event、data、id和retry。event表示事件类型,data表示消息内容,id用于设置客户端 EventSource 对象的 last event ID string 内部属性,retry指定了重新连接的时间。
服务器(PHP):
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
// 每隔1秒发送一次服务器的当前时间
while (1) {
$time = date("r");
echo "event: ping\n";
echo "data: The server time is: {$time}\n\n";
ob_flush();
flush();
sleep(1);
}
?>
客户端中,SSE借由 EventSource 对象实现。EventSource 包含五个外部属性:onerror, onmessage, onopen, readyState、url,以及两个内部属性:reconnection time与 last event ID string。在onerror属性中我们可以对错误捕获和处理,而 onmessage 则对应着服务器事件的接收和处理。另外也可以使用 addEventListener 方法来监听服务器发送事件,根据event字段区分处理。
客户端:
var eventSource = new EventSource("/path/to/server");
eventSource.onmessage = function (e) {
console.log(e.event, e.data);
}
// 或者
eventSource.addEventListener("ping", function(e) {
console.log(e.event, e.data);
}, false);
SSE相较于轮询具有较好的实时性,使用方法也非常简便。然而SSE只支持服务器到客户端单向的事件推送,而且所有版本的IE(包括到目前为止的Microsoft Edge)都不支持SSE。如果需要强行支持IE和部分移动端浏览器,可以尝试 EventSource Polyfill(本质上仍然是轮询)。SS