传统轮询,长轮询,EventSource与WebSocket

转载自:http://blog.zhangruipeng.me/2015/10/22/Web-Connectivity/

构建网络应用的过程中,我们经常需要与服务器进行持续的通讯以保持双方信息的同步。通常这种持久通讯在不刷新页面的情况下进行,消耗一定的内存资源常驻后台,并且对于用户不可见。本文将简要介绍Web通信中常用的四种方式。

传统轮询(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):

<?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):

<?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(本质上仍然是轮询)。SSE的浏览器支持情况如下图所示:
SSE SupportSSE Support

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!");
    });
});

客户端同样可以使用Node.js或者是浏览器实现,这里选用浏览器作为客户端:

// 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);
}

WebSocket同样具有实时性,每次通讯无需重发请求头部,节省带宽,而且它的浏览器支持非常好(详见下图)。
SSE SupportSSE Support

下面总结一下四种通信方式的优缺点:

>传统轮询长轮询服务器发送事件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程序实现和额外端口,客户端实现简单。

最后分享一个通(ji)俗(qi)易(dou)懂(bi)的介绍轮询和WebSocket的文章:知乎:WebSocket 是什么原理?为什么可以实现持久连接?;

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中,可以使用WebSocket和SSE(Server-Sent Events)来实现服务器和浏览器的轮询通信。 1. WebSocket WebSocket是一种基于TCP协议的通信协议,在浏览器和服务器之间建立全双工的通信通道,可以实现实时通信。 在Spring Boot中,可以使用Spring WebSocket模块来实现WebSocket,具体步骤如下: (1)添加依赖 在pom.xml中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` (2)创建WebSocket处理器 创建一个WebSocket处理器,继承自TextWebSocketHandler,并重写onMessage方法,在该方法中处理接收到的消息。 ``` @Component public class MyWebSocketHandler extends TextWebSocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 处理接收到的消息 } } ``` (3)配置WebSocket 在配置类中添加@EnableWebSocket注解,配置WebSocket处理器。 ``` @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private MyWebSocketHandler myWebSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, "/websocket"); } } ``` (4)建立WebSocket连接 在前端页面中,使用JavaScript代码建立WebSocket连接。 ``` var ws = new WebSocket("ws://localhost:8080/websocket"); ws.onmessage = function(event) { // 处理接收到的消息 }; ``` 2. SSE SSE是一种基于HTTP协议的通信协议,在浏览器和服务器之间建立单向的通信通道,可以实现服务器向浏览器推送实时数据。 在Spring Boot中,可以使用Spring WebFlux模块来实现SSE,具体步骤如下: (1)添加依赖 在pom.xml中添加以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> ``` (2)创建SSE控制器 创建一个SSE控制器,使用MediaType.TEXT_EVENT_STREAM_VALUE指定响应类型为text/event-stream,并使用Flux.interval方法生成实时数据。 ``` @RestController public class MySSEController { @GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> getRealTimeData() { return Flux.interval(Duration.ofSeconds(1)).map(i -> "Real-time data " + i); } } ``` (3)建立SSE连接 在前端页面中,使用EventSource对象建立SSE连接,并监听message事件。 ``` var es = new EventSource("/sse"); es.onmessage = function(event) { // 处理接收到的消息 }; ``` 以上就是使用Spring Boot实现服务器和浏览器轮询的两种方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值