使用Tomcat实现基于iframe streaming的Comet聊天室

首先,无图无真相,先上图:

这是一个基于Comet实现的聊天室Demo,功能类似于QQ群聊。聊天过程中如果有新想消息,那么就需要服务器推送消息到浏览器,所以这里可以使用Comet技术。

Comet一般有两种实现方式:长轮询(long-polling)、流(streaming)。而本文中的这个Demo的实现方式是基于流(streaming),前端使用了一个隐藏的iframe,这也是比较常用的一种方式。不过由于使用iframe流,导致浏览器上面的进度一直在转,这是因为iframe一直在加载的原因,先不要在意这些细节。

Tomcat提供了Comet相关的API,用Servlet实现CometProcessor接口就可以很简单的实现Comet了。

1、准备工作

1.1、首先,需要配置Tomcat连接为NIO,否则无法使用Tomcat Comet。

Tomcat目录下conf/server.xml,protocol更改为org.apache.coyote.http11.Http11NioProtocol:

<Connector connectionTimeout="20000" port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>

配置好后启动Tomcat应该是这样:

1.2、在开发过程中,需要用到Tomcat的catalina.jar包,在Tomcat的lib目录下。程序在Tomcat中运行时再去掉。

2、Java后台

2.1、CometServlet

这个Servlet是处理Comet Http长连接的Servlet,这个Servlet实现Tomcat提供的CometProcessor接口,通过event方法来处理Http长连接周期内的多种事件:

BEGIN事件:有新的HTTP连接;
END事件:连接关闭,例如浏览器关闭;
ERROR事件:连接错误,例如timeout。

有关事件更详细介绍在Tomcat官方文档中有:http://tomcat.apache.org/tomcat-7.0-doc/aio.html

public class CometServlet extends HttpServlet implements CometProcessor {

	// 所有正在等待响应的HTTP长连接
	private ArrayList<HttpServletResponse> connections = null;

	// 用于发送消息的线程
	private MessageSender messageSender = null;

	// 启动消息处理线程
	public void init() {
		connections = new ArrayList<HttpServletResponse>();
		messageSender = new MessageSender(connections);
		Thread messageSenderThread = new Thread(messageSender);
		messageSenderThread.start();
	}

	public void event(CometEvent event) throws IOException, ServletException {

		HttpServletResponse response = event.getHttpServletResponse();
		response.setCharacterEncoding("UTF-8");

		if (event.getEventType() == CometEvent.EventType.BEGIN) {
			System.out.println("BEGIN");

			// 一段大于1024的字符串,针对某些浏览器缓存
			PrintWriter out = response.getWriter();
			StringBuilder sb = new StringBuilder();
	        for(int i = 0; i < 1024; i++) {
	            sb.append(‘a‘);
	        }
	        out.println("<!-- " + sb.toString() + " -->"); // 注意加上HTML注释
	        out.flush();

			synchronized(connections) {
                connections.add(response);
                System.out.println("当前在线用户:" + connections.size());
            }

		} else if (event.getEventType() == CometEvent.EventType.ERROR) {
			System.out.println("ERROR");

			synchronized(connections) {
                connections.remove(response);
                System.out.println("当前在线用户:" + connections.size());
            }
			event.close();

		} else if (event.getEventType() == CometEvent.EventType.END) {
			System.out.println("END");

			synchronized(connections) {
                connections.remove(response);
                System.out.println("当前在线用户:" + connections.size());
            }
			event.close();

		}
	}

}

在这个Servlet中,ArrayList<HttpServletResponse> connections用于保存正在等待响应的HTTP长连接,是HttpServletResponse对象,可以理解为所有在线用户。在BEGIN事件中会向connections中添加一个连接,在END和ERROR事件中会将对应的连接删除。

在Servlet初始化init的时候,启动一个线程用于处理聊天消息,并把connections传过去。

在BEGIN事件中,先通过response的输出流输出了一段大于1024的字符串,这是由于浏览器的缓存原因,如果没有的话在某些浏览器下会有要等到流写到一定字节数后再显示的情况。这段字符串没有实际意义,所以可以随便写什么,但不要忘了加上HTML注释。

2.2、MessageSender

MessageSender是处理聊天消息的一个线程,实现Runnable接口。当有新的聊天信息时,它通过HttpServletResponse的输出流立即将信息发送到所有连接的客户端,没有新的信息则处于阻塞状态。

处理聊天消息的时候使用了java.util.concurrent中的阻塞队列ArrayBlockingQueue。ArrayBlockingQueue.take()方法用于获取并移除队列中的一个元素,当队列为空时该方法阻塞当前线程,直到有其他线程向这个队列中添加新元素。当然这里也可以用wait/notify来替代。

实际上可以将其理解成一个生产者消费者问题,有用户发送消息到服务器相当于生产一条消息,而这个线程将消息发送给所以用户相当于消费一条消息,而这个阻塞队列即是缓冲区。

public class MessageSender implements Runnable {

	// 所有正在等待响应的HTTP长连接
	private ArrayList<HttpServletResponse> connections;

	// 未发送给客户端的消息集合
	public static ArrayBlockingQueue<String> messages = new ArrayBlockingQueue<String>(10);

	public MessageSender(ArrayList<HttpServletResponse> connections) {
		this.connections = connections;
	}

	public void run() {

		while(true) {

			// 消息阻塞队列中获取一条消息,如果队列为空则阻塞
			String message = null;
			try {
				message = messages.take();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			// 给每个客户端发送消息
			synchronized (connections) {

            	for(HttpServletResponse response : connections) {
                    try {
                        PrintWriter out = response.getWriter();

                        // 输出一段脚本,调用JS将消息显示在页面上
                        out.println("<script>parent.addMsg(‘" + message + "<br>‘)</script>");
                        out.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }

		}

	}

}

2.3、AjaxMessageServlet

这个Servlet用于处理用户发送信息的请求,这是一个普通的Http请求而不是长连接。点击页面中的“发送”按钮时,就会通过Ajax向这个Servlet提交聊天信息。

当接受到新的消息时,向MessageSender中的阻塞队列ArrayBlockingQueue中put添加一条数据。当有新的数据,队列不为空时,MessageSender线程不再阻塞,会立即将消息发送到客户端浏览器。这就相当于通知MessageSender线程发送消息给客户端。

public class AjaxMessageServlet extends HttpServlet {

	public void doPost(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {

		request.setCharacterEncoding("UTF-8");
		try {
			// 这就相当于通知MessageSender线程发送消息给客户端
			MessageSender.messages.put("[" + request.getParameter("name") + "]: " + request.getParameter("msg"));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void doGet(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
		doPost(request, response);
	}
}

3、Web前端

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<script type="text/javascript">

	// 向HTML追加message,这个函数是给服务器向iframe中添加的javascript脚本调用
	function addMsg(msg) {
		var msgElement = document.getElementById("msg");
		msgElement.innerHTML += msg;
	}

	// 点击“发送”按钮后Ajax发送消息
	function sendMsg() {
		var xmlhttp = new XMLHttpRequest();
		xmlhttp.open("POST", "sendMsg"); // sendMsg是AjaxMessageServlet对应的URL
		xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

		var name = document.getElementById("input-name").value;
		var msg = document.getElementById("input-msg").value;
		xmlhttp.send("name=" + encodeURIComponent(name) + "&msg=" + encodeURIComponent(msg));
		document.getElementById("input-msg").value = "";
	}

	// 服务器timeout后再重新加载iframe
	function iframeRefresh() {
		var iframeElement = document.getElementById("iframe");
		iframeElement.src = iframeElement.src;
	}

</script>
</head>
<body>

	<!-- 隐藏的iframe,src="comet"是CometServlet对应的URL,用于加载服务器推送的JS脚本 -->
	<iframe id="iframe" style="display: none;" src="comet" onload="iframeRefresh();"></iframe>

	<div id="msg" style="height: 400px; width: 400px; overflow: scroll;"></div>

	姓名:<input type="text" id="input-name"><br>
	消息:<br>
	<textarea id="input-msg" cols="30" rows="4"></textarea><br>
	<input type="button" value="发送" onclick="sendMsg();">

</body>
</html>

JS中,addMsg函数是提供给MessageSender中输出的js脚本来调用的,用于将消息显示在页面上。

sendMsg函数是“发送”按钮点击事件,将聊天信息发送到AjaxMessageServlet。

iframeRefresh函数是在服务器超时的时候reload重新加载iframe,timeout对服务器来说是超时,对客户端来说是加载完成,所以在iframe的onload中调用。设置timeout超时时间可以在BEGIN事件中用event.setTimeout(30*1000)或event.getHttpServletRequest().setAttribute("org.apache.tomcat.comet.timeout", new Integer(30 * 1000))来设置。

页面上的iframe设置成display: none也就是不显示,src是CometServle对应的URL,当有新的信息时,MessageSender会向iframe中输出一段JS:

out.println("<script>parent.addMsg(‘" + message + "<br>‘)</script>");

浏览器加载到这段JS后会立即运行,调用addMsg函数将信息显示在页面上。

4、源码

需要DEMO源码的同学回复中留下E-mail。

作者:叉叉哥   转载请注明出处:http://blog.csdn.net/xiao__gui/article/details/38487117

使用Tomcat实现基于iframe streaming的Comet聊天室

时间: 2025-01-04 12:33:43

使用Tomcat实现基于iframe streaming的Comet聊天室的相关文章

基于Select模型的混乱聊天室v1.0

最近在无聊完成了一个简单的基于select模型的匿名聊天室程序,均使用C++开发 服务器工作原理: 每接收一条客户端的信息,就将遍历所有的socket,并将该信息发给所有的客户端. 客户端使用两条线程,一个是接收服务端信息的线程,一个是等待阻塞输入的线程,获得输入时,将输入发送到服务器. 项目源码:https://github.com/coderguang/Chat 版本为v2.0的release. 其中ComLib也在github上 服务器核心代码: int main(int argc,cha

基于LINUX的多功能聊天室

原文:基于LINUX的多功能聊天室 基于LINUX的多功能聊天室 其实这个项目在我电脑已经躺了多时,最初写完项目规划后,我就认认真真地去实现了它,后来拿着这个项目区参加了面试,同样面试官也拿这个项目来问我,当然我是做过一遍了,而且为了面试,我将什么strcpy,strlen等最常用的函数都自己实现了一遍,说着,我感觉自己有点挺用功的样子呢! 后来,工作也定下来了,等三方,然后继续帮助我的导师做项目,经过老师的威逼利诱下,我屈服了,又把智能家居系统作为项目,同时也是我的毕业设计,而且功能还要十分完

基于Server-Sent Event的简单聊天室 Web 2.0时代,即时通信已经成为必不可少的网站功能,那实现Web即时通信的机制有哪些呢?在这门项目课中我们将一一介绍。最后我们将会实现一个基于Server-Sent Event和Flask简单的在线聊天室。

基于Server-Sent Event的简单聊天室 Web 2.0时代,即时通信已经成为必不可少的网站功能,那实现Web即时通信的机制有哪些呢?在这门项目课中我们将一一介绍.最后我们将会实现一个基于Server-Sent Event和Flask简单的在线聊天室.

基于linux的TCP网络聊天室设计与实现

利用Linux实现基于TCP模式的网络聊天程序 主要完成的两大组成部分为:服务器和客户端. 服务器程序主要负责监听客户端发来的消息. 客户端需要登录到服务器端才可以实现正常的聊天功能.该程序是利用进程以及共享内存来实现群发送消息的. 以下简单分析一下服务器端和客户端两个方面所要完成的任务. 服务器的主要功能如下: 在特定的端口上进行监听,等待客户端的连接. 用户可以配置服务器端的监听端口. 向连接的客户端发送登录成功信息. 向已经连接到服务器的客户端的用户发送系统消息. 使用TCP多线程并发服务

基于EPOLL模型的局域网聊天室和Echo服务器

一.EPOLL的优点 在Linux中,select/poll/epoll是I/O多路复用的三种方式,epoll是Linux系统上独有的高效率I/O多路复用方式,区别于select/poll.先说select/poll的缺点,以体现epoll的优点. select: (1)可监听的socket受到限制,在32位的系统中,默认最大值为1024. (2)采用轮询方式,当要监听的sock数量很大时,效率低. (3)随着要监听socket数据的增加,要维护一个存放大量fd的数据结构,系统开销太大. pol

《基于Node.js实现简易聊天室系列之详细设计》

一个完整的项目基本分为三个部分:前端.后台和数据库.依照软件工程的理论知识,应该依次按照以下几个步骤:需求分析.概要设计.详细设计.编码.测试等.由于缺乏相关知识的储备,导致这个Demo系列的文章层次不是很清楚,索性这一章将所有的过程(前后端以及数据库)做一个介绍,下一章写完总结就OK了吧. (1)前端部分 涉及到的技术:html.css.bootstrap.jquery.jquery UI 登录/注册界面使用的是bootstrap响应式布局,即支持不同尺寸的客户端,以此提高用户的体验.在这之前

《基于Node.js实现简易聊天室系列之引言》

简述:这个聊天室是基于Node.js实现的,完成了基本的实时通信功能.在此之前,对node.js和mongodb一无所知,但是通过翻阅博客,自己动手基本达到了预期的效果.技术,不应该是闭门造车,而是学会分享总结才能进步.因此,我想记录下我的学习心得.不喜勿喷. 技术选型: 数据库:mongodb 实时通信:socket.io 服务器:node.js 后台:node.js 前端js库:jquery 数据库之所以选择mongodb,是因为mongodb是介于关系型与非关系型之间的一款产品,存储格式为

通信——基于Xmpp协议实现的聊天室

前段时间写了个自定义通信协议的聊天室(即用\r\n标记字符串,作为一句话),总感觉自己弄的那个协议实现虽然简单,但是拓展性就太差了,只适合于发送聊天的内容,难以包含更多的信息.基于上述几点,于是就开始接触Xmpp协议,并自己定义了一个简单的Xmpp协议用以实现聊天室. 首先有2点要知道: 1.什么是Xmpp协议?   Xmpp协议是一种通信协议,有自己的规则,即用Xml格式的消息进行通信. 2.Xml串的例子.   <msg><name>laoyang</name>&l

基于WebSocket实现网页版聊天室

WebSocket ,HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,其使用简单,应用场景也广泛,不同开发语言都用种类繁多的实现,仅Java体系中,Tomcat,Jetty,Spring等都提供了对WS的API支持.本篇不做理论探究,仅自娱自乐,简单实现网页版的聊天室功能,在实际开发场景中变通使用即可.废话不叽歪,直接撸出来—— 1  简单页面 <!DOCTYPE html> <html lang="en"> <head> &l