本帖最后由 西安Java组 于 2017-12-30 21:54 编辑
在java服务器端程序开发中,通常有这样的需求,就是服务器主动给客户端推送数据。在B/S架构中,通常是客户端向服务器发送请求,然后服务器给客户端做出响应。如果要做一个Web聊天工具,A客户端向服务器发送聊天数据,另一个B客户端需要请求服务器才能获取数据(流程图如下)。
再比如一些股票、证券数据,客户端需要时刻去关注服务器的数据,按照传统的方式,客户端需要时刻刷新客户端,才能获取服务器数据。如下图
以上问题就需要使用服务器反推技术解决。 以聊天功能为例:B客户端如果要时刻获取A发送到服务器的数据。
原始的解决方案:b客户端不停的向服务端通过AJAX发送请求。来获取服务器的响应。 以下模拟A客户端的聊天数据
以下模拟B客户端如何不刷新页面获取A客户端的数据。
但是以上方案缺点太明显,即客户端不停的向服务器发送请求,服务器压力太大。 除了以上原始方式外,就需要使用一新种技术,我们把这种技术叫comet,
comet即基于长连接Http的服务器反推技术 服 务器端会主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。Comet 架构非常适合事件驱动的 Web 应用,以及对交互性和实时性要求很强的应用,如股票交易行情分析、聊天室和 Web 版在线游戏等。
要使用基于长连接的服务器反推技术有两种,第一种是基于流的方式。原理如下。
第一步:修改Tomcat的协议
将原来server.xml中 <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> 改为 <Connector connectionTimeout="20000" port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>
第二步服务器的代码: 首先服务器的代码就不能直接继承HttpServlet,因为直接继承HttpServlet后,不能和客户端进行长时间的链接。所以需要继承CometProcessor. 访问的url为comet1,然后给客户端循环发送数据。 package cn.itcast.servlet;
import java.awt.peer.ComponentPeer; import java.io.IOException; import java.util.Random;
import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.comet.CometEvent; import org.apache.catalina.comet.CometProcessor; import org.springframework.beans.factory.annotation.Autowired; import org.apache.catalina.comet.CometEvent.EventSubType; import org.apache.catalina.comet.CometEvent.EventType;
public class AChatCometServlet extends HttpServlet implements CometProcessor { private static final long serialVersionUID = 1L;
@Autowired @Override public void event(CometEvent event) throws IOException, ServletException { HttpServletResponse resp = event.getHttpServletResponse(); HttpServletRequest request = event.getHttpServletRequest(); if(event.getEventType() == EventType.BEGIN) { //连接已经建立 //模拟发送数据给客户端
System.out.println("服务器和客户端建立了连接"+"客户端的sessionid是" + request.getSession().getId()); new Thread(){ public void run() { Random random = new Random(); while(true) { try { int num = random.nextInt(100); Thread.sleep(5000); System.out.println(num); resp.getWriter().println(String.valueOf(num)); resp.getWriter().flush(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }
}; }.start();
System.out.println("线程开启"); }else if(event.getEventType() == EventType.END) { //客户端连接已经断开 event.close(); }else if(event.getEventType() == EventType.ERROR) { System.out.println("服务器出错"); event.close(); }
}
}
客户端如何来做? 在ajax中,客户端和服务器的通信状态有一下几种。
我们需要做的就是在3这一步接受服务器响应的部分数据。
注意:客户端接受数据的状态并不是在4,4表示服务器全部发送完毕。而客户端接受数据应该是3 服务器发送一部分数据后就需要获取。这样就不断获取服务器的数据,但是每次发送数据后responseText中的数据就会变多,直接展示会重复。需要截取,截取的代码优化如下。
以上就是基于长连接的流的方式处理服务器反推问题。
程序运行后:
基于长连接流的方式存在一些问题: *客户端API必须使用原始的AJAX,因为只有原生的ajax才有状态的API。 *有些浏览器不支持原始ajax API
基于长连接的轮询方式,原理就是客户端和服务器连接后,服务器如果响应完毕数据给客户端,客户端要立即和服务器再次进行连接。
第一步:服务器的代码 package cn.itcast.servlet;
import java.awt.peer.ComponentPeer; import java.io.IOException; import java.util.Random;
import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.comet.CometEvent; import org.apache.catalina.comet.CometProcessor; import org.springframework.beans.factory.annotation.Autowired; import org.apache.catalina.comet.CometEvent.EventSubType; import org.apache.catalina.comet.CometEvent.EventType;
public class AChatCometServlet2 extends HttpServlet implements CometProcessor { private static final long serialVersionUID = 1L;
@Override public void event(CometEvent event) throws IOException, ServletException { final HttpServletResponse resp = event.getHttpServletResponse(); HttpServletRequest request = event.getHttpServletRequest(); if(event.getEventType() == EventType.BEGIN) { //连接已经建立 //模拟发送数据给客户端
System.out.println("服务器和客户端建立了连接"+"客户端的sessionid是" + request.getSession().getId()); new Thread(){ public void run() { Random random = new Random(); while(true) { try { int num = random.nextInt(100); Thread.sleep(5000); System.out.println(num); resp.getWriter().println(String.valueOf(num)); //注意:服务器端需要关闭response,否则就是一个长连接。 resp.getWriter().flush(); resp.getWriter().close();
} catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }
}; }.start();
System.out.println("线程开启"); }else if(event.getEventType() == EventType.END) { //客户端连接已经断开 event.close(); }else if(event.getEventType() == EventType.ERROR) { System.out.println("服务器出错"); event.close(); }
}
}
客户端代码: 客户端在状态为4的时候,获取数据重新发起请求。
以上代码就是关于基于comet长连接的轮询方式,但是该代码存在一些问题需要修复,下一次讲解如何修复一些问题,以及如何利用comet实现web聊天。
运行上面代码会发现以下问题: 以上代码为什么存在空指针呢?而且第二次请求的Response和第一次一样。 *由于客户端向服务器发送数据后,服务器响应完毕数据后,关闭response,但是关闭可能还没有完毕,客户端又来连接(两次请求的是同一个连接,但是服务器的response已经处于关闭)。
*每一个客户端看到的数据不同,因为连接后每次都创建一个新的线程来发送数据。
基于以上问题需要创建一个专门发送消息的线程类。该类的作用就是输出消息给客户端。 *该线程启动后首先判断客户端和服务器的链接有没有创建起来,如果没有,当前线程就挂起。 *如果服务器创建连接后调用setConnetion设置好连接后,唤醒发送消息的线程
/** 发送消息的工具类,该类要想能够发送消息,必须满足2个要求 1.客户端和服务器建立好连接 2.有消息 */
public class MessageSender implements Runnable{
private boolean isRunning = true; private String message; private boolean hashMessage; //HttpServletResponse就是連接 //private HttpServletResponse connection; private Map<String,HttpServletResponse> connetions = new HashMap<>(); private Object key = new Object(); private boolean isPoll = true;
//当客户端和服务器创建好链接后,唤醒发送功能发送消息 public void setConnetion(String sessionID,HttpServletResponse connection) { synchronized (key) { connetions.put(sessionID, connection); key.notify(); }
}
public synchronized void setMessage(String message) { this.message = message; hashMessage = true; this.notify(); }
@Override public void run() { System.out.println("启动消息发送线程"); while(isRunning) { System.out.println("开始发送消息"); //当客户端与服务器创建起来链接后就发送消息,如果没有就挂起。 if(connetions.size() <= 0) { synchronized (key) { try { key.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
}
//消息必须有才能发送 if(!hashMessage) {
try { synchronized (this) { this.wait(); }
} catch (Exception e) { e.printStackTrace(); }
}
try { String message = this.message; hashMessage = false; this.message = null; for(HttpServletResponse connection : connetions.values()){ connection.getWriter().write(message); }
if(isPoll) { for(HttpServletResponse connection : connetions.values()){ connection.getWriter().close(); connection = null;
} connetions.clear();
} } catch (IOException e) { e.printStackTrace(); }
} }
public void close() { isRunning = false; }
}
以上发送消息的线程类写好之后,我们就可以基于该线程类来完成Web聊天。
最后我们使用长轮询的技术完成web聊天 步骤一:定义我们的登录页面和登录的Servlet。 这个比较简单
步骤二:定义聊天的界面chat.jsp
步骤3::轮询的Servlet
下面及长轮询的Servlet URL就是comet.action
package cn.itcast.servlet;
import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map;
import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.comet.CometEvent; import org.apache.catalina.comet.CometEvent.EventType; import org.apache.catalina.comet.CometProcessor;
/** * 长连接的轮询Servlet * */ public class ChatServlet extends HttpServlet implements CometProcessor { private static final long serialVersionUID = 1L; /* * MessageSender是用来发送消息的线程 */ private MessageSender sender; /* * 用来存放聊天的人 */ private Map<String,String> users = new HashMap<>();
/** * */
@Override public void init(ServletConfig config) throws ServletException { //初始化消息发送线程 //该线程必须有连接,并且有数据才发送给客户端数据 sender = new MessageSender(); new Thread(sender).start();
}
@Override public void event(CometEvent event) throws IOException, ServletException { HttpServletResponse resp = event.getHttpServletResponse(); resp.setContentType("application/json;charset=utf-8"); HttpServletRequest request = event.getHttpServletRequest(); request.setCharacterEncoding("utf-8");
if(request.getSession().getAttribute("loginUser") == null) { request.getRequestDispatcher("/chat.jsp").forward(request, resp); return; }
if(event.getEventType() == EventType.BEGIN) { //连接已经建立 //模拟发送数据给客户端 System.out.println("服务器和客户端建立了连接"+"客户端的sessionid是" + request.getSession().getId()); System.out.println("线程开启"); sender.setConnetion(request.getSession().getId(),resp);
String sessionID = request.getSession().getId(); String loginUser = (String) request.getSession().getAttribute("loginUser"); //如果用户是新登录来到这个Servlet就将用户信息以及sessionId放到users
MessageBean messageBean = new MessageBean(); List<String> us = new ArrayList<>(); //表示该用户是第一次登录 if(!users.containsKey(sessionID)) { users.put(sessionID, loginUser); us.addAll(users.values()); messageBean.setUsers(us); messageBean.setChat("欢迎" + loginUser +"登录"); //如果客户端是第一次登录 //将数据发送给客户端 sender.setMessage(JSONUtil.pojo2JSON(messageBean)); }else {
//如果客户端不是第一次登录,且发送了数据,就通过输出消息线程,将消息信息响应给客户端 String talk = request.getParameter("talk"); if(talk != null && !talk.equals("")) { us.addAll(users.values()); messageBean.setUsers(us); messageBean.setChat(loginUser + "说" + talk); sender.setMessage(JSONUtil.pojo2JSON(messageBean)); //如果客户端说话了 }
//否则就不做任何事情 }
}else if(event.getEventType() == EventType.END) { //客户端连接已经断开 //event.close(); }else if(event.getEventType() == EventType.ERROR) { System.out.println("服务器出错"); //event.close(); }
}
} JSON 的辅助工具类 消息POJO类
最后效果
|