传统轮询、长轮询、服务器发送事件与WebSocket
构建网络应用的过程中,我们经常需要与服务器进行持续的通讯以保持双方信息的同步。通常这种持久通讯在不刷新页面的情况下进行,消耗一定的内存资源常驻后台,并且对于用户不可见。本文将简要介绍Web通信中常用的四种方式。
最后分享一个通(ji)俗(qi)易(dou)懂(bi)的介绍轮询和WebSocket的文章:知乎:WebSocket 是什么原理?为什么可以实现持久连接?;
传统轮询(Traditional Polling)
当前Web应用中较常见的一种持续通信方式,通常采取setInterval或者setTimeout实现。例如如果我们想要定时获取并刷新页面上的数据,可以结合Ajax写出如下实现:setInterval(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
});
}, 10000);
- 1
- 2
- 3
- 4
- 5
- 6
function poll() {
setTimeout(function() {
$.get("/path/to/server", function(data, status) {
console.log(data);
// 发起下一次请求
poll();
});
}, 10000);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
长轮询(Long Polling)
上面两种传统的轮询方式都存在一个严重缺陷:程序在每次请求时都会新建一个HTTP请求,然而并不是每次都能返回所需的新数据。当同时发起的请求达到一定数目时,会对服务器造成较大负担。这时我们可以采用长轮询方式解决这个问题。注意 长轮询与以下将要提到的服务器发送事件和WebSocket不能仅仅依靠客户端JavaScript实现,我们同时需要服务器支持并实现相应的技术。长轮询的基本思想是在每次客户端发出请求后,服务器检查上次返回的数据与此次请求时的数据之间是否有更新,如果有更新则返回新数据并结束此次连接,否则服务器“hold”住此次连接,直到有新数据时再返回相应。而这种长时间的保持连接可以通过设置一个较大的HTTP timeout实现。下面是一个简单的长连接示例: 服务器(PHP):
<?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);
?>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
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);
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
服务器发送事件(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):<?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);
}
?>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
WebSocket
WebSocket同样是HTML 5规范的组成部分之一,现标准版本为RFC 6455。WebSocket相较于上述几种连接方式,实现原理较为复杂,用一句话概括就是:客户端向WebSocket服务器通知(notify)一个带有所有接收者ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。由于WebSocket本身是基于TCP协议的,所以在服务器端我们可以采用构建TCP Socket服务器的方式来构建WebSocket服务器。这里为了略过协议解析的具体细节,我们采用Node.js的ws库来实现简单的WebSocket服务器。 服务器(Node.js):var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: 8080});
wss.on("connection", function(socket) {
socket.on("message", function(msg) {
console.log(msg);
socket.send("Nice to meet you!");
});
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
// WebSocket 为客户端JavaScript的原生对象
var ws = new WebSocket("ws://localhost:8080");
ws.onopen = function (event) {
ws.send("Hello there!");
}
ws.onmessage = function (event) {
console.log(event.data);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
> | 传统轮询 | 长轮询 | 服务器发送事件 | WebSocket |
---|---|---|---|---|
浏览器支持 | 几乎所有现代浏览器 | 几乎所有现代浏览器 | Firefox 6+ Chrome 6+ Safari 5+ Opera 10.1+ | IE 10+ Edge Firefox 4+ Chrome 4+ Safari 5+ Opera 11.5+ |
服务器负载 | 较少的CPU资源,较多的内存资源和带宽资源 | 与传统轮询相似,但是占用带宽较少 | 与长轮询相似,除非每次发送请求后服务器不需要断开连接 | 无需循环等待(长轮询),CPU和内存资源不以客户端数量衡量,而是以客户端事件数衡量。四种方式里性能最佳。 |
客户端负载 | 占用较多的内存资源与请求数。 | 与传统轮询相似。 | 浏览器中原生实现,占用资源很小。 | 同Server-Sent Event。 |
延迟 | 非实时,延迟取决于请求间隔。 | 同传统轮询。 | 非实时,默认3秒延迟,延迟可自定义。 | 实时。 |
实现复杂度 | 非常简单。 | 需要服务器配合,客户端实现非常简单。 | 需要服务器配合,而客户端实现甚至比前两种更简单。 | 需要Socket程序实现和额外端口,客户端实现简单。 |