【DWR系列02】-DWR逆向Ajax即服务器推送
一、简单例子直观认识
1.1 模拟场景
假定项目中需要新增一个功能,管理员发布某些信息,这些信息需要推送到所有已经登录的普通用户页面。
1.2 创建Web项目
简单起见,复用上一篇博客的项目例子,【DWR系列】-DWR简介及入门例子。即在原项目上直接新增测试。项目结构图如下:
1.3 修改web.xml
修改web.xml
,使DWR
支持逆向Ajax
,为接收DWR
请求的servlet
简单的增加一个参数即可:
<init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param>
最终web.xml
如下:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <display-name>testweb</display-name> <servlet> <servlet-name>dwr-invoker</servlet-name> <!-- 接收js的Ajax请求的servlet --> <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class> <!-- 启用逆向Ajax --> <init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dwr-invoker</servlet-name> <!-- 拦截指定的URL --> <url-pattern>/dwr/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
org.directwebremoting.servlet.DwrServlet
可以设置为随服务器启动而加载。
1.4 新增被推送页面
新增被推送页面normal.jsp
:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>My JSP 'index.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <script type='text/javascript' src='dwr/engine.js'></script> <script type='text/javascript' src='dwr/util.js'></script> </head> <body> <h2>逆向Ajax页面,服务器推送</h2> <span>推送信息:</span><span id="push"></span> </body> <script type="text/javascript"> window.onload = function(){ dwr.engine.setActiveReverseAjax(true); } </script> </html>
注意需要引入两个js文件,engine.js
和util.js
,并在页面加载完后声明启用逆向Ajax。
1.5 服务端推送代码
复用上一篇博客的HelloWorld
,在有参无返回值方法内,将传递给后台的信息,推送给登录的页面,这也符合开始的需求:
/** * 有参无返回值 */ public void helloYN(final String name){ System.out.println(new Date().toLocaleString() + " js访问helloYN方法,name=" + name); //将接收到的内容推送到所有的浏览器 Browser.withAllSessions(new Runnable(){ @Override public void run(){ Util.setValue("push",name); } }); }
这样,就可以用第一篇博客的例子进行测试了。
1.6 测试
首先启动项目,然后访问index.jsp
模拟管理员登录,再开两个浏览器或者标签页,登录normal.jsp
模拟普通用户登录,登录如下:
第一个为上一个篇博客的js调用Java方法页面,下面两个模拟普通用户登录,然后在有参无返回值输入框输入文本,点击按钮发送观察下面了个页面,发现内容几乎立即显示出来:
二、逆向Ajax简介
2.1 简介
逆向Ajax
俗称服务端推送
,但是实际意义上的服务端推送在现有条件下是实现不了的,可以设想一下,若服务端可以主动推送内容到客户端,那么当访问恶意网站的时候,会有可能被推送病毒或者木马。所以一般所谓的服务器端推送都是通过其它方式来实现的,比如说轮询或者长连接。
DWR的逆向Ajax(Reverse Ajax)有三种模式:
- Polling:轮询模式,DWR会以一个固定时间为周期去服务器获取数据,这种方式和自己编写循环执行Ajax一样。
- Comet:长连接模式,就是服务端持有请求,并不断的发送数据信息。上面的例子即是Comet模式。
- Piggyback:捎带模式,即当有推送需求时,等待下一次Ajax请求一并把数据发送过去。
2.2 各种模式选择
实现简单的对这三种模式进行比较:
- 响应速度:
Comet
(几乎瞬时)>Polling
(可自由设置轮询时间)>Piggyback
(因其不确定性) - 对服务器压力(连接数较高时):
Comet
>Polling
>Piggyback
通过比较可以发现,高性能等价于高消耗,当系统的主要功能需要用推送来完成且实时性要求高连接数不大的情况下可以使用Comet,连接数较大且对实时性没有较高要求(一分钟或以上)可以使用Polling,不建议使用Piggyback。
三、逆向推送进阶
不管使用哪种逆向推送都会面临一个问题,那就是被推送客户端的选择问题,大多数情况消息需要被推送到指定的客户端或指定角色的一系列客户端。首先通过下面一个例子进行直观认识。
3.1 创建Web项目
依然为了简便起见,复用原有项目。在上面进行简单修改。
3.2 设置不同属性值
既然要选择不同的客户端进行推送,就要有选择的依据,Web项目中常用的选择依据就是根据用户不同,进行区分。但是要有用户就要有登录模块,再次通过其它方式进行模拟。
3.2.1 通过URL将用户传入
在访问被推送页面的时候,将用户ID通过参数传递给JSP,例如:
http://localhost:8080/dwr/normal.jsp?userId=yiwangzhibujian
3.2.2 设置到session中
再将获取到的userId
放到session
,至此就模拟完登录过程:
String userId=request.getParameter("userId");
session.setAttribute("userId",userId);
正常系统的登录操作一般都会讲用户ID放到session中,此处进行简单模拟。
3.3 新增js
之前已经在normal.jsp
中开启了逆向Ajax功能,现在则需要开启关闭页面提醒服务器功能:
dwr.engine.setNotifyServerOnPageUnload(true);
然后将此进行注册(注册说法有些不妥当,先这么理解,后续会对功能进行详细解释):
HelloWorld.regist();
注册复用了HelloWorld
类,在里面新增了一个方法。
最终normal.jsp
如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>My JSP 'index.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> <meta http-equiv="keywords" content="keyword1,keyword2,keyword3"> <meta http-equiv="description" content="This is my page"> <script type='text/javascript' src='dwr/engine.js'></script> <script type='text/javascript' src='dwr/util.js'></script> <script type='text/javascript' src='dwr/interface/HelloWorld.js'></script> <% //模拟登录 //获取登录用户 String userId=request.getParameter("userId"); //将登录用户放到session中 session.setAttribute("userId",userId); %> </head> <body> <h2>逆向Ajax页面,服务器推送,当前用户:${userId }</h2> <span>推送信息:</span><span id="push"></span> </body> <script type="text/javascript"> window.onload = function(){ //开启逆向Ajax功能 dwr.engine.setActiveReverseAjax(true); //开启关闭页面提醒服务器功能 dwr.engine.setNotifyServerOnPageUnload(true); //对当前用户进行注册 HelloWorld.regist(); } </script> </html>
后续将对注释内容进行详解。
3.4 修改服务端类
依然复用HelloWorld
类,实际项目中最好不这么做:
3.4.1 新增注册方法
/** * 当页面开启时注册用户 */ public void regist(){ // 获取当前的scriptSession ScriptSession scriptSession=WebContextFactory.get().getScriptSession(); //获取HttpSession 并获得其中的userId HttpSession session=WebContextFactory.get().getSession(); String userId=(String) session.getAttribute("userId"); // 对当前scriptSession的key设置指定的值 scriptSession.setAttribute("key",userId); }
这个方法实际工作是将不同的属性值放置到ScriptSession
中供过滤器使用,实际工作中可以使用监听器ScriptSessionListener
来完成这个工作。
3.4.2 新增推送方法
推送方法我们复用HelloWorld
的有参有返回值方法,这样可以将传入的参数进行推送,传入参数限定格式为,推送用户 推送内容(忽略校验,请按格式输入):
/** * 有参有返回值 */ public String helloYY(final String name){ //获得传入的值进行分解,推送用户 推送内容 final String[] param=name.split("[ ]{1,}"); System.out.println(new Date().toLocaleString() + " js访问helloYY方法,name=" + name); //对符合条件的用户进行推送 Browser.withAllSessionsFiltered(new ScriptSessionFilter(){ @Override public boolean match(ScriptSession session){ boolean isYou=param[0].equals(session.getAttribute("key")); return isYou; } },new Runnable(){ @Override public void run(){ Util.setValue("push",param[1]); } }); return "给" + param[0] + "成功推送一条消息"; }
最终的HelloWorld类内容如下:
package yiwangzhibujian; import javax.servlet.http.HttpSession; import org.directwebremoting.*; import org.directwebremoting.ui.dwr.Util; import java.util.Date; /** * @author yiwangzhibujian */ @SuppressWarnings("deprecation") public class HelloWorld{ /** * 无参无返回值 */ public void helloNN(){ System.out.println(new Date().toLocaleString() + " js访问helloNN方法"); } /** * 有参无返回值 */ public void helloYN(final String name){ System.out.println(new Date().toLocaleString() + " js访问helloYN方法,name=" + name); // 将接收到的内容推送到所有的浏览器 Browser.withAllSessions(new Runnable(){ @Override public void run(){ Util.setValue("push",name); } }); } /** * 无参有返回值 */ public String helloNY(){ System.out.println(new Date().toLocaleString() + " js访问helloNY方法"); return "Hello World!"; } /** * 有参有返回值 */ public String helloYY(final String name){ // 获得传入的值进行分解,推送用户 推送内容 final String[] param=name.split("[ ]{1,}"); System.out.println(new Date().toLocaleString() + " js访问helloYY方法,name=" + name); // 对符合条件的用户进行推送 Browser.withAllSessionsFiltered(new ScriptSessionFilter(){ @Override public boolean match(ScriptSession session){ boolean isYou=param[0].equals(session.getAttribute("key")); return isYou; } },new Runnable(){ @Override public void run(){ Util.setValue("push",param[1]); } }); return "给" + param[0] + "成功推送一条消息"; } /** * 当页面开启时注册用户 */ public void regist(){ // 获取当前的scriptSession ScriptSession scriptSession=WebContextFactory.get().getScriptSession(); // 获取HttpSession 并获得其中的userId HttpSession session=WebContextFactory.get().getSession(); String userId=(String) session.getAttribute("userId"); // 对当前scriptSession的key设置指定的值 scriptSession.setAttribute("key",userId); } }
3.5 测试
依然和上面的测试一样,登录三个页面,第一个为推送信息页面,后两个为用户页面,注意带上用户参数:
然后在第一个页面的有参有返回值框进行输入测试,测试结果如下:
测试结果通过,可以进行精准推送。