客户端与服务器持续同步解析(轮询,comet,WebSocket)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

在B/S模型的Web应用中,客户端常常需要保持和服务器的持续更新。这种对及时性要求比较高的应用比如:股票价格的查询,实时的商品价格,自动更新的twitter timeline以及基于浏览器的聊天系统(如GTalk)等等。由于近些年AJAX技术的兴起,也出现了多种实现方式。本文将对这几种方式进行说明,并用jQuery+tornado进行演示,需要说明的是,如果对tornado不了解也没有任何问题,由于tornado的代码非常清晰且易懂,选择tornado是因为其是一个非阻塞的(Non-blocking IO)异步框架(本文使用2.0版本)。

 

在开始之前,为了让大家有个清晰的认识,首先列出本文所要讲到的内容大概。本文将会分以下几部分:

  1. 普通的轮询(Polling)
  2. Comet:基于服务器长连接的“服务器推”技术。这其中又分为两种:
    1. 基于AJAX和基于IFrame的流(streaming)方式
    2. 基于AJAX的长轮询(long-polling)方式
  3. WebSocket

 

 

古老的轮询

轮询最简单也最容易实现,每隔一段时间向服务器发送查询,有更新再触发相关事件。对于前端,使用js的setInterval以AJAX或者JSONP的方式定期向服务器发送request。

var polling = function(){
$.post('/polling', function(data, textStatus){
$("p").append(data+"<br>");
});
};
interval = setInterval(polling, 1000);

 

后端我们只是象征性地随机生成一些数字,并且返回。在实际应用中可以访问cache或者从数据库中获取内容。

复制代码
复制代码
import random
import tornado.web

class PollingHandler(tornado.web.RequestHandler):
def post(self):
num = random.randint(1, 100)
self.write(str(num))
复制代码
复制代码

 

可以看到,采用polling的方式,效率是十分低下的,一方面,服务器端不是总有数据更新,所以每次问询不一定都有更新,效率低下;另一方面,当发起请求的客户端数量增加,服务器端的接受的请求数量会大量上升,无形中就增加了服务器的压力。

 

Comet:基于HTTP长连接的“服务器推”技术

看到 这个标题有的人可能就晕了,其实原理还是比较简单的。基于Comet的技术主要分为流(streaming)方式和长轮询(long-polling)方式。
 

首先看Comet这个单词,很多地方都会说到,它是“彗星”的意思,顾名思义,彗星有个长长的尾巴,以此来说明客户端发起的请求是长连的。即用户发起请求后就挂起,等待服务器返回数据,在此期间不会断开连接。流方式和长轮询方式的区别就是:对于流方式,客户端发起连接就不会断开连接,而是由服务器端进行控制。当服务器端有更新时,刷新数据,客户端进行更新;而对于长轮询,当服务器端有更新返回,客户端先断开连接,进行处理,然后重新发起连接。
 

会有同学问,为什么需要流(streaming)和长轮询(long-polling)两种方式呢?是因为:对于流方式,有诸多限制。如果使用AJAX方式,需要判断XMLHttpRequest 的 readystate,即readystate==3时(数据仍在传输),客户端可以读取数据,而不用关闭连接。问题也在这里,IE 在 readystate 为 3 时,不能读取服务器返回的数据,所以目前 IE 不支持基于 Streaming AJAX,而长轮询由于是普通的AJAX请求,所以没有浏览器兼容问题。另外,由于使用streaming方式,控制权在服务器端,并且在长连接期间,并没有客户端到服务器端的数据,所以不能根据客户端的数据进行即时的适应(比如检查cookie等等),而对于long polling方式,在每次断开连接之后可以进行判断。所以综合来说,long polling是现在比较主流的做法(如fb,Plurk)。

 

接下来,我们就来对流(streaming)和长轮询(long-polling)两种方式进行演示。

 

流(streaming)方式

从上图可以看出每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。

流方式首先一种常用的做法是使用AJAX的流方式(如先前所说,此方法主要判断readystate==3时的情况,所以不能适用于IE)。
 

服务器端代码像这样:

复制代码
复制代码
class StreamingHandler(tornado.web.RequestHandler):
'''使用asynchronus装饰器使得post方法变成无阻塞'''
@tornado.web.asynchronous
def post(self):
self.get_data(callback=self.on_finish)

def get_data(self, callback):
if self.request.connection.stream.closed():
return

num = random.randint(1, 100) #生成随机数
callback(num) #调用回调函数

def on_finish(self, data):
self.write("Server says: %d" % data)
self.flush()

tornado.ioloop.IOLoop.instance().add_timeout(
time.time()+3,
lambda: self.get_data(callback=self.on_finish)
)
复制代码
复制代码

对于服务器端,仍然是生成随机数字,由于要不断输出数据,于是在回调函数里延迟3秒,然后继续调用get_data方法。在这里要注意的是,不能使用time.sleep(),由于tornado是单线程的,使用sleep方法会block主线程。因此要调用IOLoop的add_timeout方法(参数0:执行时间戳,参数1:回调函数)。于是服务器端会生成一个随机数字,延迟3秒再生成随机数字,循环往复。
 

于是前端js就是:

复制代码
复制代码
try {
var request = new XMLHttpRequest();
} catch (e) {
alert("Browser doesn't support window.XMLHttpRequest");
}

var pos = 0;
request.onreadystatechange = function () {
if (request.readyState === 3) { //在 Interactive 模式处理
var data = request.responseText;
$("p").append(data.substring(pos)+"<br>");
pos = data.length;
}
};
request.open("POST", "/streaming", true);
request.send(null);
复制代码
复制代码


对于tornado来说,调用flush方法,会将先前write的所有数据都发送客户端,也就是response的数据处于累加的状态,所以在js脚本里,我们使用了pos变量作为cursor来存放每次flush数据结束位置。

 

另外一种常用方法是使用IFrame的streaming方式,这也是早先的常用做法。首先我们在页面里放置一个iframe,它的src设置为一个长连接的请求地址。Server端的代码基本一致,只是输出的格式改为HTML,用来输出一行行的Inline Javascript。由于输出就得到执行,因此就少了存储游标(pos)的过程。服务器端代码像这样:

复制代码
复制代码
class IframeStreamingHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def get(self):
self.get_data(callback=self.on_finish)

def get_data(self, callback):
if self.request.connection.stream.closed():
return

num = random.randint(1, 100)
callback(num)

def on_finish(self, data):
self.write("<script>parent.add_content('Server says: %d<br />');</script>" % data)
# 输出的立刻执行,调用父窗口js函数add_content
self.flush()

tornado.ioloop.IOLoop.instance().add_timeout(
time.time()+3,
lambda: self.get_data(callback=self.on_finish)
)
复制代码
复制代码


在客户端我们只需定义add_content函数:

var add_content = function(str){
$("p").append(str);
};


由此可以看出,采用IFrame的streaming方式解决了浏览器兼容问题。但是由于传统的Web服务器每次连接都会占用一个连接线程,这样随着增加的客户端长连接到服务器时,线程池里的线程最终也就会用光。因此,Comet长连接只有对于非阻塞异步Web服务器才会产生作用。这也是为什么选择tornado的原因。

使用iframe方式一个问题就是浏览器会一直处于加载状态。

 

长轮询(long-polling)方式


长轮询是现在最为常用的方式,和流方式的区别就是服务器端在接到请求后挂起,有更新时返回连接即断掉,然后客户端再发起新的连接。于是Server端代码就简单好多,和上面的任务类似:

复制代码
复制代码
class LongPollingHandler(tornado.web.RequestHandler):
@tornado.web.asynchronous
def post(self):
self.get_data(callback=self.on_finish)

def get_data(self, callback):
if self.request.connection.stream.closed():
return

num = random.randint(1, 100)
tornado.ioloop.IOLoop.instance().add_timeout(
time.time()+3,
lambda: callback(num)
) # 间隔3秒调用回调函数

def on_finish(self, data):
self.write("Server says: %d" % data)
self.finish() # 使用finish方法断开连接
复制代码
复制代码


Browser方面,我们封装成一个updater对象:

复制代码
复制代码
var updater = {
poll: function(){
$.ajax({url: "/longpolling",
type: "POST",
dataType: "text",
success: updater.onSuccess,
error: updater.onError});
},
onSuccess: function(data, dataStatus){
try{
$("p").append(data+"<br>");
}
catch(e){
updater.onError();
return;
}
interval = window.setTimeout(updater.poll, 0);
},
onError: function(){
console.log("Poll error;");
}
};
复制代码
复制代码


要启动长轮询只要调用

updater.poll();


可以看到,长轮询与普通的轮询相比更有效率(只有数据更新时才返回数据),减少不必要的带宽的浪费;同时,长轮询又改进了streaming方式对于browser端判断并更新不足的问题。

 

WebSocket:未来方向

以上不管是Comet的何种方式,其实都只是单向通信,直到WebSocket的出现,才是B/S之间真正的全双工通信。不过目前WebSocket协议仍在开发中,目前Chrome和Safri浏览器默认支持WebSocket,而FF4和Opera出于安全考虑,默认关闭了WebSocket,IE则不支持(包括9),目前WebSocket协议最新的为“76号草案”。有兴趣可以关注http://dev.w3.org/html5/websockets/
 

在每次WebSocket发起后,B/S端进行握手,然后就可以实现通信,和socket通信原理是一样的。目前,tornado2.0版本也是实现了websocket的“76号草案”。详细可以参阅文档。我们还是只是在通信打开之后发送一堆随机数字,仅演示之用。

复制代码
复制代码
import tornado.websocket

class WebSocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
for i in xrange(10):
num = random.randint(1, 100)
self.write_message(str(num))

def on_message(self, message):
logging.info("getting message %s", message)
self.write_message("You say:" + message)
复制代码
复制代码

客户端代码也很简单和直观:

复制代码
复制代码
var wsUpdater = {
socket: null,
start: function(){
if ("WebSocket" in window) {
wsUpdater.socket = new WebSocket("ws://localhost:8889/websocket");
}
else {
wsUpdater.socket = new MozWebSocket("ws://localhost:8889/websocket");
}
wsUpdater.socket.onmessage = function(event) {
$("p").append(event.data+"<br>");
};
}
};
wsUpdater.start();
复制代码
复制代码

 

总结:本文对Browser和Server端持续同步的方式进行了介绍,并进行了演示。在实际生产中,有一些框架。包括Java的Pushlet,NodeJS的socket.io,大家请自行查阅资料。

本文参考文章:

  1. Browser 與 Server 持續同步的作法介紹 (Polling, Comet, Long Polling, WebSocket) (可能要FQ)
  2. Comet:基于 HTTP 长连接的“服务器推”技术 

本文出自残阳似血博文:Browser和Server持续同步的几种方式(jQuery+tornado演示)

目录
相关文章
|
18天前
|
存储 开发工具 git
[Git] 深入理解 Git 的客户端与服务器角色
Git 的核心设计理念是分布式,每个仓库既可以是客户端也可以是服务器。通过 GitHub 远程仓库和本地仓库的协作,Git 实现了高效的版本管理和代码协作。GitHub 作为远程裸仓库,存储项目的完整版本历史并支持多客户端协作;本地仓库则通过 `.git` 文件夹独立管理版本历史,可在离线状态下进行提交、回滚等操作,并通过 `git pull` 和 `git push` 与远程仓库同步。这种分布式特性使得 Git 在代码协作中具备强大的灵活性和可靠性。
47 18
[Git] 深入理解 Git 的客户端与服务器角色
|
2月前
|
存储 人工智能 自然语言处理
ChatMCP:基于 MCP 协议开发的 AI 聊天客户端,支持多语言和自动化安装 MCP 服务器
ChatMCP 是一款基于模型上下文协议(MCP)的 AI 聊天客户端,支持多语言和自动化安装。它能够与多种大型语言模型(LLM)如 OpenAI、Claude 和 OLLama 等进行交互,具备自动化安装 MCP 服务器、SSE 传输支持、自动选择服务器、聊天记录管理等功能。
249 15
ChatMCP:基于 MCP 协议开发的 AI 聊天客户端,支持多语言和自动化安装 MCP 服务器
|
3月前
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
61 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
4月前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
194 7
|
4月前
|
网络协议 Unix Linux
一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
120 4
|
4月前
|
安全 区块链 数据库
|
7月前
|
前端开发 网络协议 JavaScript
在Spring Boot中实现基于WebSocket的实时通信
在Spring Boot中实现基于WebSocket的实时通信
|
4月前
|
开发框架 前端开发 网络协议
Spring Boot结合Netty和WebSocket,实现后台向前端实时推送信息
【10月更文挑战第18天】 在现代互联网应用中,实时通信变得越来越重要。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为客户端和服务器之间的实时数据传输提供了一种高效的解决方案。Netty作为一个高性能、事件驱动的NIO框架,它基于Java NIO实现了异步和事件驱动的网络应用程序。Spring Boot是一个基于Spring框架的微服务开发框架,它提供了许多开箱即用的功能和简化配置的机制。本文将详细介绍如何使用Spring Boot集成Netty和WebSocket,实现后台向前端推送信息的功能。
979 1
|
4月前
|
前端开发 Java C++
RSocket vs WebSocket:Spring Boot 3.3 中的两大实时通信利器
本文介绍了在 Spring Boot 3.3 中使用 RSocket 和 WebSocket 实现实时通信的方法。RSocket 是一种高效的网络通信协议,支持多种通信模式,适用于微服务和流式数据传输。WebSocket 则是一种标准协议,支持全双工通信,适合实时数据更新场景。文章通过一个完整的示例,展示了如何配置项目、实现前后端交互和消息传递,并提供了详细的代码示例。通过这些技术,可以大幅提升系统的响应速度和处理效率。
|
6月前
|
开发框架 网络协议 Java
SpringBoot WebSocket大揭秘:实时通信、高效协作,一文让你彻底解锁!
【8月更文挑战第25天】本文介绍如何在SpringBoot项目中集成WebSocket以实现客户端与服务端的实时通信。首先概述了WebSocket的基本原理及其优势,接着详细阐述了集成步骤:添加依赖、配置WebSocket、定义WebSocket接口及进行测试。通过示例代码展示了整个过程,旨在帮助开发者更好地理解和应用这一技术。
501 1

推荐镜像

更多