在java
服务器端程序开发中,通常有这样的需求,就是服务器主动给客户端推送数据。在
B/S
架构中,通常是客户端向服务器发送请求,然后服务器给客户端做出响应。如果要做一个
Web
聊天工具,
A
客户端向服务器发送聊天数据,另一个
B
客户端需要请求服务器才能获取数据(流程图如下)。
再比如一些股票、证券数据,客户端需要时刻去关注服务器的数据,按照传统的方式,客户端需要时刻刷新客户端,才能获取服务器数据。如下图
以上问题就需要使用服务器反推技术解决。
以聊天功能为例:B
客户端如果要时刻获取
A
发送到服务器的数据。
原始的解决方案:b
客户端不停的向服务端通过
AJAX
发送请求。来获取服务器的响应。
以下模拟A
客户端的聊天数据
以下模拟B
客户端如何不刷新页面获取
A
客户端的数据。
但是以上方案缺点太明显,即客户端不停的向服务器发送请求,服务器压力太大。
除了以上原始方式外,就需要使用一新种技术,我们把这种技术叫comet
,
comet
即基于长连接
Http
的服务器反推技术
服
务器端会主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。Comet
架构非常适合
事件驱动
的 Web
应用,以及对
交互性
和实时性要求很强的应用,如股票交易行情分析、聊天室和 Web
版在线游戏等。
要使用基于长连接的服务器反推技术有两种,第一种是基于流的方式。原理如下。
第一步:修改Tomcat
的协议
将原来server.xml
中
改为
第二步服务器的代码:
首先服务器的代码就不能直接继承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 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 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 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类
最后效果