简介
现在,用户期望可以从 Web 访问快速、动态的应用程序。本 系列 文章展示了如何使用反向 Ajax 技术开发事件驱动的 Web 应用程序。反向 Ajax,第 1 部分:Comet 简介 介绍了反向 Ajax、轮询、流、Comet 和长轮询。您应该已经了解到,使用 HTTP 长轮询的 Comet 是可靠地实现反向 Ajax 的最佳方式,因为现在所有浏览器都提供了这方面的支持。
通过本文,您将学习如何使用 WebSockets 实现反向 Ajax。代码示例有助于说明 WebSockets、FlashSockets、服务器端的制约因素、请求作用域服务和暂停长期请求。您可以下载本文使用的 源代码。
先决条件
在理想的情况下,如果想最大限度地利用本文,您应该了解 JavaScript 和 Java。本文创建的示例是使用 Google Guice 构建的,Google Guice 是用 Java 编写的依赖项注入框架。要理解本文内容,则需要熟悉依赖项注入框架的概念,比如 Guice、Spring 或 Pico。
要运行本文中的示例,还需要使用最新版的 Maven 和 JDK(参阅 参考资料)。
WebSockets
WebSockets 在 HTML5 中出现,是比 Comet 更新的反向 Ajax 技术。WebSockets 支持双向、全双工通信信道,而且许多浏览器(Firefox、Google Chrome 和 Safari)也支持它。连接通过 HTTP 请求(也称为 WebSockets 握手)和一些特殊的标头 (header)。连接一直处于激活状态,您可以用 JavaScript 编写和接收数据,正如您使用原始 TCP 套接字一样。
通过输入 ws://
或 wss://
(在 SSL 上)启动 WebSocket URL。
图 1 中的时间轴展示了如何使用 WebSockets 进行通信。HTTP 握手被发送到带有特定标头的服务器。然后,可在 JavaScript 的服务器或客户端上提供某种类型的套接字。可使用该套接字来通过事件处理器异步接收数据。
图 1. 通过 WebSockets 执行反向 Ajax
在本文的 下载源代码 中有一个 WebSocket 示例。当您运行该示例时,就会到类似于 清单 1 的输出。它显示了事件如何发生在服务器端上,并立即出现在客户端。当客户端发送一些数据时,服务器将其反映在客户端上。
清单 1. 用 JavaScript 编写的 WebSocket 示例
[client] WebSocket connection opened [server] 1 events [event] ClientID = 0 [server] 1 events [event] At Fri Jun 17 21:12:01 EDT 2011 [server] 1 events [event] From 0 : qqq [server] 1 events [event] At Fri Jun 17 21:12:05 EDT 2011 [server] 1 events [event] From 0 : vv
通常,在 JavaScript 中使用 WebSockets 的方式与 清单 2 中展示的相同(如果您的浏览器支持它)。
清单 2. JavaScript 客户端代码
var ws = new WebSocket('ws://127.0.0.1:8080/async'); ws.onopen = function() { // called when connection is opened }; ws.onerror = function(e) { // called in case of error, when connection is broken in example }; ws.onclose = function() { // called when connexion is closed }; ws.onmessage = function(msg) { // called when the server sends a message to the client. // msg.data contains the message. }; // Here is how to send some data to the server ws.send('some data'); // To close the socket: ws.close();
可以发送和接收任何类型的数据。WebSockets 可被看作是 TCP 套接字,因此由客户端和服务器决定要发送的数据类型。这里给出的示例发送的是 JSON 字符串。
在创建了 JavaScript WebSocket 对象后,如果在浏览器的控制台(或 Firebug)深入查看 HTTP 请求中的握手,您应该看到特定于 WebSocket 的包头。清单 3 展示了一个示例。
清单 3. HTTP 请求和响应标头示例
Request URL:ws://127.0.0.1:8080/async Request Method:GET Status Code:101 WebSocket Protocol Handshake Request Headers Connection:Upgrade Host:127.0.0.1:8080 Origin:http://localhost:8080 Sec-WebSocket-Key1:1 &1~ 33188Yd]r8dp W75q Sec-WebSocket-Key2:1 7; 229 *043M 8 Upgrade:WebSocket (Key3):B4:BB:20:37:45:3F:BC:C7 Response Headers Connection:Upgrade Sec-WebSocket-Location:ws://127.0.0.1:8080/async Sec-WebSocket-Origin:http://localhost:8080 Upgrade:WebSocket (Challenge Response):AC:23:A5:7E:5D:E5:04:6A:B5:F8:CC:E7:AB:6D:1A:39
所有标头都被 WebSocket 握手用来授权和建立长期连接。WebSocket JavaScript 对象还包含两个有用的属性:
ws.url
ws.readyState
- CONNECTING = 0
- OPEN = 1
- CLOSED = 2
在服务器端,处理 WebSockets 时更加复杂。还没有 Java 规范提供支持 WebSockets 的标准方式。要使用 Web 容器(如 Tomcat 或 Jetty)的 WebSockets 功能,则需要将应用程序代码紧密聚集到使您能够访问 WebSockets 功能的特定于容器的库中。
示例代码 的 websocket 文件夹中的示例使用了 Jetty 的 WebSocket API,因为我们使用的是 Jetty 容器。清单 4 展示了 WebSocket 处理器。(本系列的第 3 部分将使用不同的后端 WebSocket API。)
清单 4. Jetty 容器的 WebSocket 处理器
public final class ReverseAjaxServlet extends WebSocketServlet { @Override protected WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { return [...] } }
使用 Jetty 时,有许多处理 WebSocket 握手的方法。更简单的方法是为 Jetty 的 WebSocketServlet
创建子类并实现 doWebSocketConnect
方法。该方法要求您返回 Jetty 的 WebSocket 接口的一个实例。您需要实现该接口,并返回某种代表 WebSocket 连接的端点。清单 5 提供了一个示例。
清单 5. WebSocket 实现示例
class Endpoint implements WebSocket { Outbound outbound; @Override public void onConnect(Outbound outbound) { this.outbound = outbound; } @Override public void onMessage(byte opcode, String data) { // called when a message is received // you usually use this method } @Override public void onFragment(boolean more, byte opcode, byte[] data, int offset, int length) { // when a fragment is completed, onMessage is called. // Usually leave this method empty. } @Override public void onMessage(byte opcode, byte[] data, int offset, int length) { onMessage(opcode, new String(data, offset, length)); } @Override public void onDisconnect() { outbound = null; } }
要向客户端发送消息,只需将该消息写入出站即可,如 清单 6 中所示:
清单 6. 向客户端发送一条消息
if (outbound != null && outbound.isOpen()) { outbound.sendMessage('Hello World !'); }
要断开客户端并关闭 WebSocket 连接,可以使用 outbound.disconnect();
。
WebSockets 是一种非常强大的实现双向通信的方法,且无延迟,Firefox、Google Chrome、Opera 及其他现代浏览器都支持它。根据 jWebSocket 网站上的调查:
- Chrome 自 4.0.249 开始便包含本机 WebSockets。
- Safari 5.x 包括本机 WebSockets。
- Firefox 3.7a6 和 4.0b1+ 包含本机 WebSockets。
- Opera 从 10.7.9067 开始便包含本机 WebSockets。
如需关于 jWebSocket 的更多信息,请参阅 参考资料。
优势
WebSockets 提供强大的、双向、低延迟和易于处理的错误。它没有很多连接,比如:Comet 长轮询,而且也没有 Comet 流的缺点。与 Comet 相比,该 API 易于直接使用,无需使用任何其他层,Comet 需要一个很好的库来处理连接、超时、Ajax 请求、确认以及不同的传输(Ajax 长轮询和 jsonp 轮询)。
缺点
WebSockets 的缺点包括:
- 它是来自 HTML5 的新规范,并不是所有浏览器都支持它。
- 无请求作用域。由于 WebSockets 是一个 TCP 套接字,而不是一个 HTTP 请求,因此无法轻松使用请求作用域服务,如 Hibernate 的
SessionInViewFilter
。Hibernate 是一个持久性框架,提供了一个过滤器来处理 HTTP 请求。请求开始时,将建立一个绑定到请求线程的争用(包含事务和 JDBC 连接)。请求结束后,过滤器会破坏该争用。
FlashSockets
对于不支持 WebSockets 的浏览器,一些库能够回退到 FlashSockets(通过 Flash 的套接字)。这些库通常提供相同的官方 WebSocket API,但是它们通过将调用委派给网站上包含的隐藏的 Flash 组件来实现。
优势
FlashSockets 透明地提供 WebSockets 功能,即使在不支持 HTML5 WebSockets 的浏览器上也是如此。
缺点
FlashSockets 具有以下缺点:
- 它需要安装 Flash 插件(通常所有浏览器都有该插件)。
- 它要求打开防火墙的 843 端口,以便 Flash 组件能够执行 HTTP 请求来检索包含域授权的策略文件。
如果无法访问 843 端口,那么库应回退或给出一个错误。所有处理都需要一定的时间(最多 3 秒钟,具体取决于库),这会减慢网站速度。
- 如果客户端在代理服务器后面,那么到 843 端口的连接可能遭到拒绝。
WebSocketJS 项目提供一个网桥。它要求至少提供 Flash 10,并为 Firefox 3、Internet Explorer 8 和 Internet Explorer 9 提供 WebSockets 支持。
建议
与 Comet 相比,WebSockets 带来很多好处。在日常开发过程中,支持 WebSockets 的客户端的速度变得更快,产生的需求也更少(因此,使用的带宽也更少)。但是,由于并非所有浏览器都支持 WebSockets,因此支持反向 Ajax 库的最佳选择将是能够检测到 WebSockets 支持,如果不支持 WebSockets,则回退到 Comet(长轮询)。
由于需要这两种技术来最大程度地利用所有浏览器并保持兼容性,因此建议您使用在这些技术之上提供抽象层的客户端 JavaScript 库。本系列的第 3 部分和第 4 部分将研究一些库,第 5 部分将展示这些库的应用。在服务器端,情况可能更复杂,如上一节所述。
服务器端的反向 Ajax 制约因素
大致了解客户端可用的反向 Ajax 解决方案之后,让我们来看一下服务器上的反向 Ajax 解决方案。到目前为止,示例中主要使用的是客户端 JavaScript 代码。在服务器端,为了接受反向 Ajax 连接,某些技术需要使用特定功能来处理使用期较长的连接(与您熟悉的短 HTTP 请求相比较而言)。为了更好地进行扩展,应该使用新的线程模型,该模型需要使用 Java 中的特定 API 才能够暂停请求。此外,对于 WebSockets,您需要正确管理应用程序中使用的服务的作用域。
线程和非阻塞 I/O
通常情况下,Web 服务器会将每个传入的 HTTP 连接与一个线程或一个进程相关联。这种连接可以是持久的(一直有效),因此多个请求可能使用同一个连接。在本文的示例中,可以将 Apache Web 服务器配置为 mpm_fork 或 mpm_worker 模型来改变这种行为。Java Web 服务器(应用服务器也包括在内)通常为每个传入连接使用一个线程。
生成新的线程会导致内存消耗和资源浪费,因为不能保证生成的线程会被使用。可能会已经建立连接,但没有从客户端或服务器发送数据。无论是否使用该线程,都会消耗内存和 CPU 资源来调度和争用开关。使用线程模型配置服务器时,通常需要配置一个线程池(设置处理传入连接的最大线程数)。如果错误地配置了该值,并且该值过低,那么您将遭遇线程匮乏问题;请求将一直处于等待状态,直到有了可用来处理这些请求的线程。在达到最大并发连接后,响应时间会延长。另一方面,配置较高的线程数可能导致内存不足异常。生成过多的线程会消耗 JVM 的所有堆内存,并导致服务器崩溃。
Java 最近推出了称为非阻塞 I/O 的新的 I/O API。该 API 使用一个选择器,避免每次执行连接到服务器的新 HTTP 连接时都绑定一个线程。有传入数据时,系统会收到一个事件,并分配一个线程来处理请求。因此,这也被称为 “每个请求一个线程” 模型。它允许 WebSphere 和 Jetty 等 Web 服务器进行扩展,并使用固定数量的线程处理越来越多的用户连接。在相同的硬件配置下,在这种模型下运行的 Web 服务器比 “每个连接一个线程” 模型具有更好的扩展性。
在 Philip McCarthy( Comet and Reverse Ajax 的作者)的博客中,他提供了关于两种线程模型的可扩展性的一个有趣基准(请参阅 参考资料 以获得链接)。在 图 2 中,您会发现相同的模型:当使用过多的连接时,线程模型会停止工作。
图 2. 线程模型基准
“每个连接一个线程” 模型(图 2 中的线程)通常会提供更快的响应,因为所有线程都已启用、准备就绪并等待使用,但是当连接数量过多时,则会停止服务。在 “每个请求一个线程” 模型下(图 2 的续图),要使用一个线程为到达的请求提供服务,而连接是通过 NIO 选择器进行处理的。响应时间可能会长一些,但是可以将线程回收利用,因此该解决方案在连接数量较大时扩展性更好一些。
为了了解线程的幕后工作方式,可以将 LEGO™ 块想象成为一个选择器。每个传入请求都连接到该 LEGO 块,并通过引脚识别。LEGO 块/选择器将拥有与连接数量相同的 PIN(和密钥)。然后,在等待新事件发生时,只需要使用一个线程在 PIN 上进行迭代。当发生事件时,选择器线程会检索发生事件的密钥,然后使用一个线程为传入请求提供服务。
"Rox Java NIO Tutorial" 提供了使用了用 Java 编译的 NIO 的良好示例(参阅 参考资料)。
请求作用域服务
许多框架都提供了服务或过滤器,处理到达 servlet 的 Web 请求。例如,过滤器将执行以下操作:
- 将 JDBC 连接绑定到请求线程上,整个请求只使用一个连接。
- 在请求结束时进行变更。
另一个示例是 Google Guice 的 Guice Servlet 扩展(一个依赖项注入库)。与 Spring 一样,Guice 能够在请求的作用域内绑定服务。对每个新请求,一次最多只能创建一个实例(参阅 参考资料 以获得更多相关信息)。
典型用法包括使用来自集群 HTTP 会话的用户 id,缓存从请求中的信息库(如数据库)检索的用户对象。在 Google Guice 中,您可以获得类似于 清单 7 的代码。
清单 7. 请求作用域绑定
@Provides @RequestScoped Member member(AuthManager authManager, MemberRepository memberRepository) { return memberRepository.findById(authManager.getCurrentUserId()); }
在将一个成员注入某个类时,Guice 会尝试从请求中提取它。如果没有找到它,Guice 会执行信息库调用,并将结果放在请求中。
请求作用域的服务可以与任何反向 Ajax 解决方案配套使用,除了 WebSockets。任何其他解决方案,无论是短期还是长期的,都将依赖于 HTTP 请求,因此每个请求都通过 servlet 调度系统,并执行过滤。完成暂停的(长期)HTTP 请求后,您将在本系列的后续部分看到,还有一个选项可以使请求再次通过过滤器链。
对于 WebSockets,与在 TCP 套接字中一样,数据将直接到达 onMessage
回调。因为没有针对该数据而传入的 HTTP 请求,因此没有决定从哪个请求中获得并存储作用域对象的请求争用。因此,使用需要从 onMessage
回调的作用域对象的服务会失败。
下载源代码 中的 guice-and-websocket 示例展示了如何绕过限制,在 onMessage
回调中仍然使用请求作用域对象。当您运行该示例并单击页面上的每个按钮来测试 Ajax 调用(请求作用域)、WebSocket 调用、带有模拟请求作用域的 WebSocket 调用时,您将获得如 图 3 中所示的输出。
图 3. 使用请求作用域服务的 WebSocket 处理器的输出
无论使用以下哪个选项,都可能都会遇到这样的问题:
- Spring。
- Hibernate。
- 任何其他需要请求作用域或 “每个请求” 模型的框架,如
OpenSessionInViewFilter
。 - 使用
ThreadLocal
工具在过滤器中将变量限制在请求线程中并在以后对其进行访问的系统。
Guice 有一个良好的解决方法,如下所示 清单 8:
清单 8. 从 WebSocket onMessage
回调模拟请求作用域
// The reference to the request is hold when the // doWebSocketMethod is called HttpServletRequest request = [...] Map<Key<?>, Object> bindings = new HashMap<Key<?>, Object>(); // I have a service which needs a request to get the session, // so I provide the request, but you could provide any other // binding that may be needed bindings.put(Key.get(HttpServletRequest.class), request); ServletScopes.scopeRequest(new Callable<Object>() { @Override public Object call() throws Exception { // call your repository or any service using the scoped objects outbound.sendMessage([...]); return null; } }, bindings).call();
暂停长期请求
使用 Comet 时有另一个障碍。服务器如何能够在不影响性能的前提下暂停长期请求,然后在服务器事件到达时恢复并完成该请求?
显然,您不能只是保留请求和响应,这会导致线程匮乏和内存消耗过高。除了非阻塞 I/O,暂停长轮询请求还需要一个特定的 API。在 Java 中,Servlet 3.0 规范提供了一个这样的 API(参见本系列的 反向 Ajax,第 1 部分:Comet 简介)。清单 9 展示了一个示例。
清单 9. 通过 Servlet 3.0 定义异步 servlet
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:j2ee="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml /ns/j2ee/web-app_3.0.xsd"> <servlet> <servlet-name>events</servlet-name> <servlet-class>ReverseAjaxServlet</servlet-class> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>events</servlet-name> <url-pattern>/ajax</url-pattern> </servlet-mapping> </web-app>
在定义了异步 servlet 后,您可以使用 Servlet 3.0 API 暂停和恢复请求,如下所示清单 10:
清单 10. 暂停和恢复请求
AsyncContext asyncContext = req.startAsync(); // Hold the asyncContext reference somewhere // Then when needed, in another thread you can resume or complete HttpServletResponse req = (HttpServletResponse) asyncContext.getResponse(); req.getWriter().write("data"); req.setContentType([...]); asyncContext.complete();
在 Servlet 3.0 之前,每个容器都曾拥有(目前依然拥有)自己的机制。Jetty 的后续产品就是一个众所周知的示例;Java 中的许多反向 Ajax 库都依赖于 Jetty 的后续产品。这算不上是阻碍,不要求您一定要在 Jetty 容器中运行您的应用程序。API 可以非常智能地检测您正在运行的容器,如果在 Tomcat 或 Grizzly 等其他容器中运行时,则会回退到 Servlet 3.0 API(如果有的话)。这种情况同样适用于 Comet。但是如果您想要充分利用 WebSockets,那么目前别无选择,只能使用特定于容器的功能。
Servlet 3.0 规范尚未公布,但是许多容器已经实现了该 API,因为它是执行反向 Ajax 的标准方式。
相关推荐
而WebSockets是一种提供双向通信的协议,比反向Ajax更进一步,可以实现持续的连接,适合于需要实时交互的应用。 在实际应用中,反向Ajax技术通常需要与数据库、服务器处理进程等组件协同工作。例如,分布式任务处理...
- **反向AJAX**:DWR支持服务器向客户端推送数据,即所谓的反向AJAX,增强了实时性。 3. **DWR的应用场景**: - **动态更新**:可以实时地更新页面的一部分,而不需要整个页面刷新。 - **数据输入验证**:在...
2. **与WebSocket比较**:WebSocket提供双向通信,DWR的反向AJAX只能实现单向推送,但WebSocket需要浏览器和服务器都支持。 通过以上介绍,我们可以看到DWR在构建动态Web应用时发挥的作用,它为开发人员提供了一种...
5. **Ajax技术**:Python可以通过XMLHttpRequest对象实现Ajax异步通信,更新部分网页内容,无需刷新整个页面,提高用户体验。 6. **RESTful API设计**:Python后端常通过设计RESTful API与前端进行数据交互,遵循...
- **反向Ajax安全**:DWR支持CSRF(跨站请求伪造)防护,通过设置令牌来验证请求的合法性。 - **数据加密**:虽然HTTP本身不提供加密,但DWR可以通过与其他安全协议结合(如HTTPS)来保护数据的安全传输。 DWR的...
1. **反向Ajax**:DWR允许服务器端代码主动触发浏览器端的JavaScript函数,实现了服务器到客户端的实时通信,而不仅仅是客户端请求服务器的传统模式。 2. **自动类型转换**:DWR可以自动将Java对象转换为JavaScript...
4. **AJAX(Asynchronous JavaScript and XML)**:虽然WebSockets更适合实时通信,但在某些情况下,如旧浏览器支持或部分数据传输,开发者可能还需要使用AJAX进行异步数据交换。 5. **推送服务**:对于不支持...
3. **JavaScript**:作为前后端通用的语言,JavaScript 在前端用于构建用户交互界面,通过 AJAX 或 Websocket 实现动态更新;在后端,node.js 利用 JavaScript 处理业务逻辑和数据交互。 4. **前端框架**:可能会...
自托管意味着您可以将这些应用程序部署在自己的服务器上,而不是依赖于第三方服务。这样做的好处包括数据隐私、成本控制和定制能力的提升。文件名“awesome-selfhosted_master.zip”暗示了一个可能包含开源项目列表...
随着Web技术的发展,现代的聊天室往往采用AJAX或者WebSockets来实现即时通信,提供更流畅的用户体验。React、Vue或Angular等前端框架可以帮助开发者高效构建动态和交互性强的聊天室界面。 对于“上嵌网络编程聊天室...
同源策略限制了来自不同源的JavaScript代码对页面资源的访问,比如Ajax请求只能发送到同源的服务器。当 SignalR 的Hub尝试从一个不同的源发起连接时,就会遇到这个问题。 **SignalR 跨域的解决方案:** 1. **CORS...
- Spring Boot AJAX 跨域,包括 JSONP、Node.js与SpringBoot集成使用反向代理 等。 - springboot-websockets - Spring Boot 使用 Websocket - springboot-webflux - Spring Boot 集成 WebFlux 开发反应式 Web...
3. **AJAX与Fetch API**: 虽然WebSockets用于实时通信,但项目可能还使用了AJAX(异步JavaScript和XML)或Fetch API进行非实时的数据交换,比如用户登录验证或者获取初始聊天记录。 4. **JSON数据格式**: 通信过程...
- **AJAX**:Django支持使用jQuery或纯JavaScript进行AJAX请求,更新部分页面内容,提高用户体验。 - **前后端分离**:在现代Web开发中,Django可以作为API后端,通过RESTful API与使用JavaScript库(如React、Vue...
为了保证系统的稳定性和可靠性,系统部署在高性能的服务器上,并采用了反向代理技术进行流量管理和负载均衡。此外,系统还集成了LDAP统一身份认证服务,以便于用户的身份验证。 ##### 5.2 测试结果 经过多轮严格的...
8. **部署与运维**:最后,"chat_app"需要部署到服务器上运行,这可能涉及Nginx作为反向代理和负载均衡器,以及Docker容器化技术进行环境隔离和易于部署。 综上所述,"chat_app"项目涵盖了Python Web开发的多个方面...
这种技术的核心是利用WebSockets、Ajax等技术实现即时通讯,为用户提供类似桌面应用的交互体验。在这个场景下,我们将深入探讨与"webchat"相关的Java技术以及实现细节。 首先,我们要了解的是WebSockets,它是一种...
1. **aiohttp**: aiohttp 是Python中的一个全功能HTTP客户端/服务器库,支持WebSockets和HTTP/2。它的异步特性允许开发人员在一个事件循环中处理多个请求,从而提高了性能和资源利用率。aiohttp的主要组件包括服务器...
同时,理解AJAX异步请求和WebSockets等技术,能够帮助改进Web应用的实时性。 在安全方面,系统管理员需要关注Java的安全性,比如使用最新的Java版本以获取安全更新,配置防火墙和入侵检测系统来保护服务器,以及...