`
cyxlgzs
  • 浏览: 90179 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

Comet:基于HTTP长连接的“服务器推”技术

 
阅读更多

一、背景

在做某类程序应用时,需要将服务器的响应自动的反馈给浏览器,而不是采用用户请求的方式。基于这样的应用还是比较多的,比如说网页聊天、实时消息提醒等等。所以我们需要这样一种技术来实现服务器主动的将信息推送到浏览器

二、解决方案思考

1、定时请求。定义一个timer,浏览器每隔一段时间去请求服务器,然后配合ajax将结果显示到客户浏览器上。

这种方式的缺点在于,第一效率低,对服务器的压力比较大;第二在于timer的时间不好确定。基于这两种考虑,该方式不宜采用

2、基于HTTP长连接方式,将服务器响应的消息主动的推送到客户浏览器,然后配合ajax将结果显示到客户浏览器上

这种方式基本上解决了方案一的两个缺点,这种方式的关键在于维护一个HTTP的长连接对象列表

三、Comet的探索

1、基于方案二的考虑,在Tomcat6以后版本推出了Comet的实现,下面我们就针对Tomcat6.0.29版本来实现这种服务器推的方式

2、Comet的定义

-----------------------------------------------------摘自百度百科----------------------------------------------------------

comet 【计】:基于 HTTP 长连接的“服务器推”技术,是一种新的 Web 应用架构。基于这种架构开发的应用中,服务器端会主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。Comet 架构非常适合事件驱动的 Web 应用,以及对交互性和实时性要求很强的应用,如股票交易行情分析、聊天室和 Web 版在线游戏等。
服务器推送技术(Server Push)是最近Web技术中最热门的一个流行术语,它的别名叫Comet(彗星)。它是继AJAX之后又一个倍受追捧的Web技术。服务器推送技术最近的流行与AJAX有着密切的关系。
随着Web技术的流行,越来越多的应用从原有的C/S模式转变为B/S模式,享受着Web技术所带来的各种优势(例如跨平台、免客户端维护、跨越防火墙、扩展性好等)。但是基于浏览器的应用,也有它不足的地方。主要在于界面的友好性和交互性。由于浏览器中的页面每次需要全部刷新才能从服务器端获得最新的数据或向服务器传送数据,这样产生的延迟所带来的视觉感受非常糟糕。因此很多的桌面应用为了获得更友好的界面放弃了Web技术,或者采用浏览器的插件技术(ActiveX、Applet、Flash等)。但是浏览器插件技术本身又有许多问题,例如跨平台问题和插件版本兼容性问题。

-----------------------------------------------------------------------------------------------------------------------------------

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=jackhttp://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 长连接的“服务器推”技术

    Comet:基于 HTTP 长连接的“服务器推”技术

    Comet:基于 HTTP 长连接的“服务器推”技术 (实例)

    NULL 博文链接:https://justcoding.iteye.com/blog/1497445

    Comet:基于HTTP长连接的“服务器推”技术[收集].pdf

    Comet:基于HTTP长连接的“服务器推”技术[收集].pdf

    Comet:基于_HTTP_长连接的“服务器推”技术

    Comet:基于_HTTP_长连接的“服务器推”技术 Comet:基于_HTTP_长连接的“服务器推”技术

    CometAsync_net:C#实现基于http长连接“服务器推”-Comet技术

    C#实现基于http长连接“服务器推”-Comet技术 很多应用譬如监控、即时通信、即时报价系统都需要将后台发生的变化实时传送到客户端而无须客户端不停地刷新、发送请求。 本项目基于 AJAX 的长轮询方式实现。 ...

    Comet(Http长连接)

    基于php的服务器推送实例,演示在个客户端之间传递数据!

    基于comet服务器推技术思路的Pushlet技术实现1

    (2)基于Ajax推送Ajax主要是基于浏览器发送异步请求,提高用户操作的响应性 (1)基于 AJAX 的长轮询方式长轮询:HTTP的连接保持,服务器端会阻塞请

    comet4j一整套官方出品前后端jar包,js,Demo包

    Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式。 功能特性 推送消息广播。 推送定向消息。 提供连接上线前、上线、...

    ASP.NET实现comet的聊天交互功能

    借用横刀天笑的解释这个Comet概念:“像彗星那样拖着长长的尾巴的http长连接”。事实上大家都知道,http是不可以与服务器持久连接的,要是每个请求都与服务器持久连接的话,那服务器早就宕掉了,就像前段时间像...

    comet4j.js,comet4j-tomcat6.jar,comet4j-tomcat7.jar

    Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式 文件包含comet4j-tomcat6.jar , comet4j-tomcat7.jar , comet4j.js...

    comet4j-demo

    Comet4J Comet for Java 是一个纯粹基于AJAX XMLHTTPRequest 的服务器推送框架 消息以JSON方式传递 具备长轮询 长连接 自动选择三种工作模式

    服务推送框架 come4js

    Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式。 功能特性 推送消息广播。 推送定向消息。 提供连接上线前、上线...

    comet4jDemo

    Comet4J(Comet for Java)是一个纯粹基于AJAX(XMLHTTPRequest)的服务器推送框架,消息以JSON方式传递,具备长轮询、长连接、自动选择三种工作模式。

    java websocket

    流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会...

    基于WebSocket的实时消息推送的设计与实现.pdf

    全新的HTML5 标准中引入了WebSocket,WebSocket 实现了服务器与浏览器间的双向连接,基于事件方式,效率高,服务器负担轻。本文使用Node.js 平台和Socket.IO 组件设计并实现了WebSocket 实时消息推送网页应用。

    yuliao-PHP.rar_http://yuliao_woaiyl_www. yuliao .com_yuliao6 . c

    4.采用轮询服务器方式传输数据,相比comet方式,服务器压力小许多,而且轮询间隔时间可以调节。 5.显示在线人数 6.敏感词屏蔽 7.基于ip地址的地理位置显示 安装方法: 1.编辑common.php文件,填写数据库连接信息 2....

    apr库(tomcat优化) for native 20

    它又分为BIO Http Connector与NIO Http Connector两种,后者提供非阻塞IO与长连接Comet支持。  AJP Connector, 基于AJP协议,AJP是专门设计用来为tomcat与http服务器之间通信专门定制的协议,能提供较高的通信速度...

Global site tag (gtag.js) - Google Analytics