- 浏览: 90179 次
- 性别:
文章分类
最新评论
Comet:基于HTTP长连接的“服务器推”技术
一、背景
在做某类程序应用时,需要将服务器的响应自动的反馈给浏览器,而不是采用用户请求的方式。基于这样的应用还是比较多的,比如说网页聊天、实时消息提醒等等。所以我们需要这样一种技术来实现服务器主动的将信息推送到浏览器
二、解决方案思考
1、定时请求。定义一个timer,浏览器每隔一段时间去请求服务器,然后配合ajax将结果显示到客户浏览器上。
这种方式的缺点在于,第一效率低,对服务器的压力比较大;第二在于timer的时间不好确定。基于这两种考虑,该方式不宜采用
2、基于HTTP长连接方式,将服务器响应的消息主动的推送到客户浏览器,然后配合ajax将结果显示到客户浏览器上
这种方式基本上解决了方案一的两个缺点,这种方式的关键在于维护一个HTTP的长连接对象列表
三、Comet的探索
1、基于方案二的考虑,在Tomcat6以后版本推出了Comet的实现,下面我们就针对Tomcat6.0.29版本来实现这种服务器推的方式
2、Comet的定义
-----------------------------------------------------摘自百度百科----------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------
3、实现方式。目前对于Comet的实现方式有三种,基于长轮询(long polling)、基于iframe“、基于流(stream)三种实现comet的方式。这里我们将采用iframe的方式来编写一个web版的聊天室来测试Comet的效果
四、实施步骤
1、要使用Tomcat的Comet技术必须利用Tomcat的 Advanced I/O 模块,这样就需要在tomcat的配置文件server.xml中进行配置protocol,如下
<Connector connectionTimeout="20000" port="8089" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>
2、新建一个web项目,命名为CometWebChat
3、新建一个聊天室页面,index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <script type="text/javascript" src="jquery-1.4.2.min.js"></script> <title>Comet Chat Demo</title> <script type="text/javascript"> var server = 'http://localhost:8089/CometWebChat/ChatComet?name=<%=request.getParameter("name")%>'; var comet = { connection : false, iframediv : false, initialize: function() { if (navigator.appVersion.indexOf("MSIE") != -1) { comet.connection = new ActiveXObject("htmlfile"); comet.connection.open(); comet.connection.write("<html>"); comet.connection.write("<script>document.domain = '"+document.domain+"'"); comet.connection.write("</html>"); comet.connection.close(); comet.iframediv = comet.connection.createElement("div"); comet.connection.appendChild(comet.iframediv); comet.connection.parentWindow.comet = comet; comet.iframediv.innerHTML = "<iframe id='comet_iframe' src='"+server+"'></iframe>"; } else if (navigator.appVersion.indexOf("KHTML") != -1) { comet.connection = document.createElement('iframe'); comet.connection.setAttribute('id', 'comet_iframe'); comet.connection.setAttribute('src', server); with (comet.connection.style) { position = "absolute"; left = top = "-100px"; height = width = "1px"; visibility = "hidden"; } document.body.appendChild(comet.connection); } else { comet.connection = document.createElement('iframe'); comet.connection.setAttribute('id', 'comet_iframe'); with (comet.connection.style) { left = top = "-100px"; height = width = "1px"; visibility = "hidden"; display = 'none'; } comet.iframediv = document.createElement('iframe'); comet.iframediv.setAttribute('src', server); comet.connection.appendChild(comet.iframediv); document.body.appendChild(comet.connection); } //alert(server); }, //添加用户 newUser:function(data){ var list = document.getElementById('userList'); var li = document.createElement('li'); li.setAttribute("id","u1"+data); li.innerHTML = data; list.appendChild(li); var user = document.getElementById('user'); var option = document.createElement('option'); option.setAttribute("id","u2"+data); option.innerHTML = data; user.appendChild(option); }, //删除用户 deleteUser:function(data){ $('#u1'+data).remove(); $('#u2'+data).remove(); }, //添加公共消息 newMessage:function(data){ var list = document.getElementById('messageList'); var li = document.createElement('li'); li.innerHTML = data; list.appendChild(li); }, //添加私人消息 privateMessage:function(data){ var list = document.getElementById('privateMessage'); var li = document.createElement('li'); li.innerHTML = data; list.appendChild(li); }, //退出 onUnload: function() { if (comet.connection) { comet.connection = false; } } }//comet end if (window.addEventListener) { window.addEventListener("load", comet.initialize, false); window.addEventListener("unload", comet.onUnload, false); } else if (window.attachEvent) { window.attachEvent("onload", comet.initialize); window.attachEvent("onunload", comet.onUnload); } </script> </head> <body> <script type="text/javascript"> function sendAll(){ var list = document.getElementById('privateMessage'); var li = document.createElement('li'); li.innerHTML = "I said to "+$("#user").val()+": " + $("#message").val(); list.appendChild(li); $.ajax({ type: "POST", url: "MessageServlet", data: "message="+$("#message").val()+"&user="+$("#user").val()+"&from=<%=request.getParameter("name")%>" }); } </script> <div class="container_12"> <div class="grid_10"> <div>公共聊天</div> <div id="messageList" style="height:250px;overflow:scroll;border:solid 1px black;"> </div> <br/> <div>个人聊天</div> <div id="privateMessage" style="height:150px;overflow:scroll;border:solid 1px black;"> </div> <br/> <div> <select id="user" style="width:100px;overflow:scroll;"> <option value="all">All</option> </select> <input type="text" id="message" size="40"></input> <input type="button" value="发送" onclick="sendAll()"> </div> </div> <div class="grid_2"> <h3>用户列表</h3> <ol id="userList"> </ol> </div> </div> </body> </html>
这里主要要考虑不同浏览器iframe的实现方式,结合后面服务器端代码就可以理解以上各JS方法的作用了
看以上的代码中有两个Servlet。ChatComet和MessageServlet
4、发送消息的MessageServlet的实现代码
package chat; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MessageServlet extends HttpServlet { private static final long serialVersionUID = -1L; private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); public MessageServlet() { super(); } protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String message = request.getParameter("message"); String user = request.getParameter("user"); String from = request.getParameter("from"); // ChatServlet.messageSender.login(from); if ("all".equals(user)) { log(from + " send message: " + message + " to everyone"); ChatServlet.send("*", from + ": " + message + " [" + sdf.format(new Date()) + "]"); } else { ChatServlet.send(user, from + " said to me: " + message + " [" + sdf.format(new Date()) + "]"); log(from + " send message: " + message + " to " + user); } } }
5、实现ChatComet代码,这里作为Comet的重点
package chat; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.catalina.CometEvent; import org.apache.catalina.CometProcessor; public class ChatServlet extends HttpServlet implements CometProcessor { private static final long serialVersionUID = -1L; // <用户,长连接> protected static Map<String, HttpServletResponse> connections = new HashMap<String, HttpServletResponse>(); // 消息推送线程 protected static MessageSender messageSender = null; public void init() throws ServletException { // 启动消息推送线程 messageSender = new MessageSender(); Thread messageSenderThread = new Thread(messageSender, "MessageSender[" + getServletContext().getContextPath() + "]"); messageSenderThread.setDaemon(true); messageSenderThread.start(); } public void destroy() { connections.clear(); messageSender.stop(); messageSender = null; } public void event(CometEvent event) throws IOException, ServletException { HttpServletRequest request = event.getHttpServletRequest(); HttpServletResponse response = event.getHttpServletResponse(); // 昵称 String name = request.getParameter("name"); if (name == null) { return; } if (event.getEventType() == CometEvent.EventType.BEGIN) { // Http连接空闲超时 event.setTimeout(Integer.MAX_VALUE); log("Begin for session: " + request.getSession(true).getId()); // 创建Comet Iframe PrintWriter writer = response.getWriter(); writer.println("<!doctype html public \"-//w3c//dtd html 4.0 transitional//en\">"); writer.println("<html><head><script type=\"text/javascript\">var comet = window.parent.comet;</script></head><body>"); writer.println("<script type=\"text/javascript\">"); writer.println("var comet = window.parent.comet;"); writer.println("</script>"); writer.flush(); // for chrome if (request.getHeader("User-Agent").contains("KHTML")) { for (int i = 0; i < 100; i++) { writer.print("<input type=hidden name=none value=none>"); } writer.flush(); } // 欢迎信息 writer.print("<script type=\"text/javascript\">"); writer.println("comet.newMessage('Hello " + name + ", Welcome!');"); writer.print("</script>"); writer.flush(); // 通知其他用户有新用户登陆 if (!connections.containsKey(name)) { messageSender.login(name); } // 推送已经登陆的用户信息 for (String user : connections.keySet()) { if (!user.equals(name)) { writer.print("<script type=\"text/javascript\">"); writer.println("comet.newUser('" + user + "');"); writer.print("</script>"); } } writer.flush(); synchronized (connections) { connections.put(name, response); } } else if (event.getEventType() == CometEvent.EventType.ERROR) { log("Error for session: " + request.getSession(true).getId()); synchronized (connections) { connections.remove(name); } event.close(); } else if (event.getEventType() == CometEvent.EventType.END) { log("End for session: " + request.getSession(true).getId()); messageSender.logout(name); synchronized (connections) { connections.remove(name); } PrintWriter writer = response.getWriter(); writer.println("</body></html>"); event.close(); } else if (event.getEventType() == CometEvent.EventType.READ) { InputStream is = request.getInputStream(); byte[] buf = new byte[512]; do { int n = is.read(buf); // can throw an IOException if (n > 0) { log("Read " + n + " bytes: " + new String(buf, 0, n) + " for session: " + request.getSession(true).getId()); } else if (n < 0) { return; } } while (is.available() > 0); } } // 发送消息给所有人 public static void send(String message) { messageSender.send("*", message); } // 向某个连接发送消息 public static void send(String name, String message) { messageSender.send(name, message); } public class MessageSender implements Runnable { protected boolean running = true; protected Map<String, String> messages = new HashMap<String, String>(); public MessageSender() { } public void stop() { running = false; } // 新用户登陆 public void login(String name) { synchronized (messages) { messages.put("Login", name); messages.notify(); } } // 用户下线 public void logout(String name) { synchronized (messages) { messages.put("Logout", name); messages.notify(); } } // 发送消息 public void send(String user, String message) { synchronized (messages) { messages.put(user, message); messages.notify(); } } public void run() { while (running) { if (messages.size() == 0) { try { synchronized (messages) { messages.wait(); } } catch (InterruptedException e) { // Ignore } } synchronized (connections) { synchronized (messages) { // 推送消息队列中的消息 for (Entry<String, String> message : messages .entrySet()) { if (message.getKey().equals("Login")) {// 新用户登陆 log(message.getValue() + " Login"); for (HttpServletResponse response : connections.values()) { try { PrintWriter writer = response.getWriter(); writer.print("<script type=\"text/javascript\">"); writer.println("comet.newMessage('Welcome "+ message.getValue()+ " !');"); writer.println("comet.newUser('"+ message.getValue() + "');"); writer.print("</script>"); writer.flush(); } catch (IOException e) { log("IOExeption execute command", e); } } } else if ("Logout".equals(message.getKey())) {// 用户退出 log(message.getValue() + " Logout"); for (HttpServletResponse response : connections.values()) { try { PrintWriter writer = response.getWriter(); writer.print("<script type=\"text/javascript\">"); writer.println("comet.newMessage('88, "+ message.getValue() + "');"); writer.println("comet.deleteUser('"+ message.getValue() + "');"); writer.print("</script>"); writer.flush(); } catch (IOException e) { log("IOExeption execute command", e); } } } else if ("*".equals(message.getKey())) {// 群发消息 log("Send message: " + message.getValue()+ " to everyone."); for (HttpServletResponse response : connections.values()) { try { PrintWriter writer = response.getWriter(); writer.print("<script type=\"text/javascript\">"); writer.println("comet.newMessage('"+ message.getValue() + "');"); writer.print("</script>"); writer.flush(); } catch (IOException e) { log("IOExeption execute command", e); } } } else {// 向某人发信息 try { HttpServletResponse response = connections.get(message.getKey()); PrintWriter writer = response.getWriter(); writer.print("<script type=\"text/javascript\">"); writer.println("comet.privateMessage('"+ message.getValue() + "');"); writer.print("</script>"); writer.flush(); } catch (IOException e) { log("IOExeption sending message", e); } } // 从消息队列中删除消息 messages.remove(message.getKey()); } } } } } } }
这里有两个关键点。
第一、看到引入类中的org.apache.catalina.CometEvent和org.apache.catalina.CometProcessor,这是实现Comet的关键(如果缺少包,可以将tomcat安装目录下的lib目录中的catalina.jar文件引入到项目中)。主要代码集中在CometProcessor接口的event方法中。CometEvent有几个状态,分别是BEGIN、READ、END和ERROR。根据不同的状态在处理相应的业务动作。
第二是connections对象,它维护了一个HTTP的长连接对象列表,这样有消息时就获取某个连接加以推送
6、配置web.xml,主要是配置代码中所用的两个servlet对象
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>CometWebChat</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>ChatComet</servlet-name> <servlet-class>chat.ChatServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ChatComet</servlet-name> <url-pattern>/ChatComet</url-pattern> </servlet-mapping> <servlet> <description></description> <display-name>MessageServlet</display-name> <servlet-name>MessageServlet</servlet-name> <servlet-class>chat.MessageServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>MessageServlet</servlet-name> <url-pattern>/MessageServlet</url-pattern> </servlet-mapping> </web-app>
7、测试,分别打开两个浏览器页面,分别访问http://localhost:8089/CometWebChat/index.jsp?name=jack和http://localhost:8089/CometWebChat/index.jsp?name=lily
然后分别发送消息进行聊天进行测试
五、总结
1、该方案中的例子还有一些BUG,但是基本上实现了功能,可以根据需求进行修改
2、Comet服务器推技术大大提高了像网页聊天室这样的应用的用户体验,有类似需求的项目可以考虑采用此技术
3、对于Comet的HTTP长连接的优缺点还有待探索研究,希望知者不吝赐教
4、如果加入catalina.jar导致错误org.apache.jasper.JasperException:java.lang.ClassCastException:org.apache.catalina.util.DefaultAnnotationProcessorcannotbecasttoorg.apache.AnnotationProcessor,则在Tomcat的安装目录下的conf目录中找到contex.xml文件,在Context节点内的最前端加入<Loader delegate="true" />
版权声明:本文为博主原创文章,未经博主允许不得转载。
相关推荐
Comet:基于 HTTP 长连接的“服务器推”技术
NULL 博文链接:https://justcoding.iteye.com/blog/1497445
Comet:基于HTTP长连接的“服务器推”技术[收集].pdf
Comet:基于_HTTP_长连接的“服务器推”技术 Comet:基于_HTTP_长连接的“服务器推”技术
C#实现基于http长连接“服务器推”-Comet技术 很多应用譬如监控、即时通信、即时报价系统都需要将后台发生的变化实时传送到客户端而无须客户端不停地刷新、发送请求。 本项目基于 AJAX 的长轮询方式实现。 ...
基于php的服务器推送实例,演示在个客户端之间传递数据!
(2)基于Ajax推送Ajax主要是基于浏览器发送异步请求,提高用户操作的响应性 (1)基于 AJAX 的长轮询方式长轮询:HTTP的连接保持,服务器端会阻塞请
Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式。 功能特性 推送消息广播。 推送定向消息。 提供连接上线前、上线、...
借用横刀天笑的解释这个Comet概念:“像彗星那样拖着长长的尾巴的http长连接”。事实上大家都知道,http是不可以与服务器持久连接的,要是每个请求都与服务器持久连接的话,那服务器早就宕掉了,就像前段时间像...
Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式 文件包含comet4j-tomcat6.jar , comet4j-tomcat7.jar , comet4j.js...
Comet4J Comet for Java 是一个纯粹基于AJAX XMLHTTPRequest 的服务器推送框架 消息以JSON方式传递 具备长轮询 长连接 自动选择三种工作模式
Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式。 功能特性 推送消息广播。 推送定向消息。 提供连接上线前、上线...
Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式。
流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会...
全新的HTML5 标准中引入了WebSocket,WebSocket 实现了服务器与浏览器间的双向连接,基于事件方式,效率高,服务器负担轻。本文使用Node.js 平台和Socket.IO 组件设计并实现了WebSocket 实时消息推送网页应用。
4.采用轮询服务器方式传输数据,相比comet方式,服务器压力小许多,而且轮询间隔时间可以调节。 5.显示在线人数 6.敏感词屏蔽 7.基于ip地址的地理位置显示 安装方法: 1.编辑common.php文件,填写数据库连接信息 2....
它又分为BIO Http Connector与NIO Http Connector两种,后者提供非阻塞IO与长连接Comet支持。 AJP Connector, 基于AJP协议,AJP是专门设计用来为tomcat与http服务器之间通信专门定制的协议,能提供较高的通信速度...