认为长连接就是有个http请求被服务器阻塞了 ,这样的话浏览器就一直等在那,服务器可以随时给浏览器发送信息了,对于servlet 就是一个线程被阻塞在一个servlet实例那里,等待其他servlet线程的通知。
ps:一个servlet实例被无数个线程使用的,阻塞的线程在这个实例上排队
基于上述思想,实现实时聊天,客户端向一个receive.jsp发起一个 ajax 接受信息的请求,服务器判断有信息的话,就 ajax 处理后,再发送请求,否则 receive.jsp wait() ,等待。如果一个 ajax调用了 send.jsp ,则通知 receive.jsp notify 。还要用户退出时,也要 receive.jsp notify ,否则这个线程就永远阻塞了!这就需要sessionlistener
1.HttpSessionListener
用于记录当前在线用户 ,以及当前用户退出时通知其他用户
package hyjc.listener;
import hyjc.common.SequenceUtil;
import java.util.*;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import org.apache.log4j.Logger;
public class CustomSessionListener implements HttpSessionListener, ServletContextListener {
static Logger logger = Logger.getLogger(CustomSessionListener.class);
private Hashtable allSessions = new Hashtable();
//session销毁前需要通知的servlet线程实例
private Map<String, Object> servers = Collections.synchronizedMap(new HashMap<String, Object>());
public CustomSessionListener() {
logger.debug("CustomSessionListener constructed!");
}
public void sessionCreated(HttpSessionEvent arg0) {
HttpSession session = arg0.getSession();
logger.debug("CustomSessionListener sessionCreated " + session.getId());
allSessions.put(session.getId(), session);
}
public void sessionDestroyed(HttpSessionEvent arg0) {
HttpSession session = arg0.getSession();
logger.debug("CustomSessionListener sessionDestroyed " + session.getId());
allSessions.remove(session.getId());
Set<String> keys = servers.keySet();
for (String key : keys) {
logger.debug("CustomSessionListener notify " + key);
Object o = servers.get(key);
synchronized (o) {
try {
o.notifyAll();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 应用关闭
*/
public void contextDestroyed(ServletContextEvent sc) {
ServletContext application = sc.getServletContext();
logger.debug("CustomSessionListener contextDestroyed " + application.getServletContextName());
}
/**
* 应用启动
*/
public void contextInitialized(ServletContextEvent sc) {
ServletContext application = sc.getServletContext();
logger.debug("CustomSessionListener contextInitialized " + application.getServletContextName());
application.setAttribute("allSessions", allSessions);
application.setAttribute("_SESSIONSERVERLETLISTENSERS_", servers);
application.setAttribute("contextInitializedTime", System.currentTimeMillis());
}
}
2. receive.jsp
ajax 接收消息 ,当没有消息时线程阻塞
<%@ page contentType="text/plain; charset=GBK"%>
<%@ page import="java.util.Hashtable"%><%@ page import="java.util.Map"%>
<%
boolean newM=false;
//session销毁前需要通知的servlet线程实例
Map<String, Object> servers =(Map<String, Object>)application.getAttribute("_SESSIONSERVERLETLISTENSERS_");
if(servers.get("_UPDATECHATSERVLET_")==null) {
servers.put("_UPDATECHATSERVLET_",this);
}
Hashtable allSessions = (Hashtable) application.getAttribute("allSessions");
while(!newM) {
//如果已经退出,自己建的全局session hashtable已没有该id,则 直接输出非法json,extjs 不会再连了
if(allSessions.get(session.getId())==null) break;
String im = (String)session.getAttribute("_IM_");
//有消息就调用回调函数
if (im != null)
{
out.println("\n{'msgs':[");
out.println(im);
out.println("\t]");
session.setAttribute("_IM_", null);
out.print("}");
out.flush();
newM=true;
}
//否则继续等待
else
{
//必须必须同步
synchronized (this) {
System.out.println("wait ******************************************s"+session.getId());
try{
//会释放lock
wait();}catch (Exception e){
e.printStackTrace();
newM=true;
}
System.out.println("waked ******************************************s"+session.getId());
}
}
}
%>
3.sendmsgLong.jsp
发送消息,并通知阻塞在接收消息的所有线程
<%@ page contentType="text/html; charset=GBK" %>
<%@ page import="hyjc.common.ConversionUtil,java.sql.Timestamp,java.util.Hashtable" %>
<%@ page import="java.util.Map" %>
<%
Timestamp now = new Timestamp(System.currentTimeMillis());
String sender = request.getParameter("sender");
if (!session.getId().equals(sender)) {
out.println("{'result':'访问拒绝!'}");
return;
}
Hashtable allSessions = (Hashtable) application.getAttribute("allSessions");
String nickname = (String) session.getAttribute("_IM_NICKNAME_");
/*
// 对昵称进行检查
String nickname = request.getParameter("nickname");
nickname = nickname.replace("\\", "\\\\").replace("'", "\\'");
// 2008.07.18 只有变化的时候才检查
if (!nickname.equals(oldNickname)) {
Object[] sessions = allSessions.values().toArray();
for (Object s0 : sessions) {
HttpSession s = (HttpSession) s0;
if (s != session) {
try {
String name = (String) s.getAttribute("_IM_NICKNAME_");
if (nickname.equals(name)) {
//out.println("{'result':'昵称已经存在,请修改!'}");
//return ;
}
} catch (IllegalStateException ex) {
}
}
}
session.setAttribute("_IM_NICKNAME_", nickname);
}
*/
String content = request.getParameter("content");
String receivers = request.getParameter("receivers");
if ("_IM_".equals(receivers)) {
out.println("{'result':'ok'}");
return;
}
String[] sessionIdList = receivers.split(",");
String cur = "\t\t{\n"
+ "\t\t\t'sender':'" + sender + "',\n"
+ "\t\t\t'nickname':'" + nickname + "',\n" // 2008.07.23
+ "\t\t\t'time':'" + ConversionUtil.toEmpty(now) + "',\n"
+ "\t\t\t'content':'" + content.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "") + "',\n"
+ "\t\t\t'receivers':[";
int n = 0;
for (String sid : sessionIdList) {
HttpSession s = (HttpSession) allSessions.get(sid);
if (s != null) {
if (n != 0) cur += ",";
cur += "'" + sid + "'";
++n;
}
}
cur += "]\n"
+ "\t\t}\n";
if (n == 0) {
out.println("{'result':'接收者不在线!'}");
return;
}
for (String sid : sessionIdList) {
HttpSession s = (HttpSession) allSessions.get(sid);
if (s != null && s != session) {
String im = (String) s.getAttribute("_IM_");
if (im == null) {
im = cur;
} else {
im = im + "\t\t," + cur;
}
try {
s.setAttribute("_IM_", im);
} catch (IllegalStateException ex) {
}
}
}
// 对消息进行监控
String idmon = (String) application.getAttribute("_IM_MONITOR_");
if (idmon != null) {
HttpSession s = (HttpSession) allSessions.get(idmon);
if (s != null) {
if (s != session) { // 自己发的消息不需要保存
try {
String im = (String) s.getAttribute("_IM_");
if (im == null) {
im = cur;
} else {
im = im + "\t\t," + cur;
}
s.setAttribute("_IM_", im);
} catch (IllegalStateException ex) {
}
}
} else {
application.removeAttribute("_IM_MONITOR_");
}
}
out.println("{");
out.println("\t'result':'ok',success:true,");
out.println("\t'cur':" + cur + ",");
out.println("\t'msgs':[");
/*
String im = (String) session.getAttribute("_IM_");
if (im != null) {
out.println(im);
session.setAttribute("_IM_", null);
}*/
out.println("\t],");
out.println("\t'dummy':''");
out.println("}");
Map<String, Object> servers = (Map<String, Object>) application.getAttribute("_SESSIONSERVERLETLISTENSERS_");
Object o = servers.get("_UPDATECHATSERVLET_");
//必须必须同步,唤醒 等待接受消息的servlet线程实例
synchronized (o) {
o.notifyAll();
}
System.out.println("notified ******************************************s");
%>
4. chatWinLong.js
聊天引擎,只要访问一个长连jsp,返会处理后重新连接即可
Ext.onReady(function () { var chatWin = new Ext.Window({ width: 800, height: 500, title: 'Ext聊天窗口测试版', renderTo: document.body, border: false, hidden: true, layout: 'border', closeAction: 'hide', collapsible: true, constrain: true, iconCls: 'my-userCommentIcon', maximizable: true, items: [{ region: 'west', id: 'chat-west-panel', title: '用户面板', split: true, width: 170, minSize: 100, maxSize: 200, collapsible: true, constrain: true, //margins:'0 0 0 5', layout: 'accordion', layoutConfig: { animate: true }, items: [{ items: new Ext.tree.TreePanel({ id: 'im-tree', rootVisible: false, lines: false, border: false, dataUrl: 'chat/getUserFirst.jsp', singleExpand: true, selModel: new Ext.tree.MultiSelectionModel(), root: new Ext.tree.AsyncTreeNode({ text: 'Online', children: [{ text: 'Sunrise', id: 'SunriseIm', nodeType: 'async', singleClickExpand: true, expandable: true, expanded: true }] }) }), title: '在线人员', //layout:'form', border: false, autoScroll: true, iconCls: 'im_list', tools: [{ id: 'refresh', qtip: '刷新在线信息', // hidden:true, handler: function (event, toolEl, panel) { imRootNode.reload(); //reloadUser(); } }, { id: 'close', qtip: '清除选定', // hidden:true, handler: function (event, toolEl, panel) { Ext.getCmp('im-tree').getSelectionModel().clearSelections(); } }] }, { title: 'Settings', html: '<p>Some settings in here.</p>', border: false, iconCls: 'settings' }] }, { region: 'center', layout: 'border', items: [{ region: 'center', title: '历史记录 ', id: 'history_panel', autoScroll: true, iconCls: 'my-userCommentIcon', tools: [{ id: 'refresh', qtip: '注意:如果长时间没有收到对方回应,试一下', // hidden:true, handler: function (event, toolEl, panel) { // refresh logic } }] }, { region: 'south', title: '聊天啦', layout: 'fit', iconCls: 'user_edit', autoScroll: true, height: 200, collapsible: true, //margins:'0 0 0 0', items: { xtype: 'form', baseCls: 'x-plain', autoHeight: true, autoWidth: true, bodyStyle: 'padding:10 10px 0;', defaults: { anchor: '95%' }, items: [{ xtype: 'htmleditor', height: 130, id: 'htmleditor', hideLabel: true }] }, bbar: [{ text: '发送请输入Ctrl-Enter', handler: function () { sendmsg(); }, iconCls: 'my-sendingIcon' }, '-', { text: '清除', handler: function () { Ext.getCmp("htmleditor").reset(); } }] }] }] }); var tree = Ext.getCmp('im-tree'); var imRootNode = tree.getNodeById('SunriseIm'); var query = location.search.substring(1); //获取查询串 var sessionId = SESSION; //Ext.urlDecode(query).sid; // 发送消息 function sendmsg() { Ext.getCmp("htmleditor").syncValue(); var content_value = Ext.getCmp("htmleditor").getValue(); if (content_value.trim() == '') { alert("您没有输入消息文本内容!"); Ext.getCmp("htmleditor").focus(true); return; } var receivers_values = []; var tree = Ext.getCmp('im-tree'); var receivers = tree.getSelectionModel().getSelectedNodes(); for (var i = 0; i < receivers.length; ++i) { receivers_values.push(receivers[i].attributes.sessionId); } if (receivers_values.length == 0) { alert("您没有选择接收者!"); tree.focus(); return; } //alert(receivers_values.length); if (receivers_values.length > 1) { if (!confirm("您选择了多个接收者,是否继续?")) { return; } } var nickname_value = 'forget'; var pars = { "content": content_value, "receivers": "" + receivers_values, "sender": sessionId // "nickname":'forget' }; var conn = new Ext.data.Connection(); // 发送异步请求 conn.request({ // 请求地址 url: 'chat/sendmsgLong.jsp', method: 'post', params: pars, // 指定回调函数 callback: msgsent }); } function msgsent(options, success, response) { requestCount--; if (success) { try { var jsonObj = Ext.util.JSON.decode(response.responseText); } catch(e) {} if (jsonObj && jsonObj.success) { var cur = jsonObj.cur; var sessions = []; var c = imRootNode.childNodes; for (var i = 0; i < c.length; i++) { sessions[c[i].attributes.sessionId] = c[i].attributes; //alert(c[i].attributes.sessionId); } if (cur) { var a = []; for (var j = 0; j < cur.receivers.length; j++) { //alert(cur.receivers[j]); a.push(sessions[cur.receivers[j]].loginName); } var msg = '<div style="margin:20px 5px 10px 5px"> <img src="js/ext/user_comment.png"/> {0} <b>{1}</b> 对 <b>{2}</b> 说:<br> </div>'; var chat_record = new Ext.Element(document.createElement('div')); chat_record.addClass('chat_record'); chat_record.update('<span style="margin:0px 5px 0px 5px">' + cur.content + '</span>'); Ext.getCmp("history_panel").body.appendChild(chat_record); var canvas = new Ext.Element(document.createElement('canvas')); var size_chat = chat_record.getSize(); if (!Ext.isIE && size_chat.height < 100) { chat_record.setHeight(100); size_chat.height = 100; } canvas.setSize(size_chat.width - 30, size_chat.height); //canvas.setSize(size_chat.width-,40); chat_record.appendChild(canvas); if (window['G_vmlCanvasManager']) { G_vmlCanvasManager.initElement(canvas.dom); } draw_m(chat_record.dom.lastChild, '#FFB100'); var mc = String.format(msg, cur.time, sessions[cur.sender].loginName, a); Ext.getCmp("history_panel").body.insertHtml('beforeEnd', mc); Ext.getCmp("history_panel").body.scroll('b', 10000, { duration: 0.1 }); } Ext.getCmp("htmleditor").reset(); } else if (response.responseText.trim()) alert(response.responseText); } else { if (response.responseText.trim()) alert(response.responseText); } } //event for source editing mode new Ext.KeyMap(Ext.getCmp("htmleditor").getEl(), [{ key: 13, ctrl: true, stopEvent: true, fn: sendmsg }]); //event for normal mode Ext.getCmp("htmleditor").onEditorEvent = function (e) { this.updateToolbar(); var keyCode = (document.layers) ? keyStroke.which : e.keyCode; if (keyCode == 13 && e.ctrlKey) sendmsg(); //it'a my handler } var requestCount = 0; function getMsgs() { var conn = new Ext.data.Connection({ timeout: 24 * 3600 * 1000 }); // 发送异步请求 conn.request({ // 请求地址 url: 'chat/updateChatLong.jsp', method: 'post', // 指定回调函数 callback: getMsgsCallback }); } function getUsers() { var conn = new Ext.data.Connection({ timeout: 24 * 3600 * 1000 }); // 发送异步请求 conn.request({ // 请求地址 url: 'chat/getUserLong.jsp', method: 'post', // 指定回调函数 callback: getUserLongCallback }); } function getUserLongCallback(options, success, response) { if (success) { try { var jsonObj = Ext.util.JSON.decode(response.responseText); } catch(e) {} if (jsonObj) { //不是退出时notify if (jsonObj.nodes) { imRootNode.reload(); getUsers(); } } } else { if (response.responseText.trim()) alert(response.responseText); } } //回调函数 function getMsgsCallback(options, success, response) { if (success) { try { var jsonObj = Ext.util.JSON.decode(response.responseText); } catch(e) {} if (jsonObj) { var msgs = jsonObj.msgs; var msg = '<div style="margin:20px 5px 10px 5px"> <img src="js/ext/user_comment.png"/> {0} <b>{1}</b> 对 <b>{2}</b> 说:<br> </div>'; var sessions = []; var c = imRootNode.childNodes; for (var i = 0; i < c.length; i++) { sessions[c[i].attributes.sessionId] = c[i].attributes; } if (msgs) { for (var i = 0; i < msgs.length; i++) { var a = []; for (var j = 0; j < msgs[i].receivers.length; j++) { a.push(sessions[msgs[i].receivers[j]].loginName); } var chat_record = new Ext.Element(document.createElement('div')); chat_record.addClass('chat_record'); chat_record.update('<span style="margin:0px 5px 0px 5px">' + msgs[i].content + '</span>'); Ext.getCmp("history_panel").body.appendChild(chat_record); var canvas = new Ext.Element(document.createElement('canvas')); var size_chat = chat_record.getSize(); if (!Ext.isIE && size_chat.height < 100) { chat_record.setHeight(100); size_chat.height = 100; } canvas.setSize(size_chat.width - 10, size_chat.height); //canvas.setSize(size_chat.width-,40); chat_record.appendChild(canvas); if (window['G_vmlCanvasManager']) { G_vmlCanvasManager.initElement(canvas.dom); } draw_m(chat_record.dom.lastChild, '#FFB100'); var mc = String.format(msg, msgs[i].time, sessions[msgs[i].sender].loginName, a); Ext.getCmp("history_panel").body.insertHtml('beforeEnd', mc); Ext.getCmp("history_panel").body.scroll('b', 10000, { duration: 0.1 }); } if (!chatWin.isVisible()) { self.focus(); Ext.example.msg('叮当', '您有新的短消息 <a href="javascript:window.startChatWin()">查看</a>'); } getMsgs(); } } else if (response.responseText.trim()) alert(response.responseText); } else { if (response.responseText.trim()) alert(response.responseText); } } //chatWin.show(); //chatWin.setSize(0,0); //chatWin.hide(); if (!Ext.isIE) { chatWin.collapse(); } /* var chatTask = { run:reloadUser, //scope:this, interval: 5000 //1 second }; time_pro = new Ext.util.TaskRunner(); time_pro.start(chatTask); */ //长连接方式 getMsgs(); //长连接方式 getUsers(); //chatWin.hide(); window.startChatWin = function () { chatWin.show(); chatWin.center(); //Ext.getCmp('htmleditor').focus(); }; function draw_m(canvas, color) { var context = canvas.getContext("2d"); var width = canvas.width; var height2 = canvas.height - 4.5; var height = canvas.height; context.beginPath(); context.strokeStyle = color; context.moveTo(0.5, 0.5 + 5); context.arc(5.5, 5.5, 5, -Math.PI, -Math.PI / 2, false); context.lineTo(width - 0.5 - 5, 0.5); context.arc(width - 0.5 - 5, 5.5, 5, -Math.PI / 2, 0, false); context.lineTo(width - 0.5, height2 - 5); context.arc(width - 0.5 - 5, height2 - 5, 5, 0, Math.PI / 2, false); context.lineTo(width / 2 + 3, height2); context.lineTo(width / 2, height); context.lineTo(width / 2 - 3, height2); context.lineTo(0.5 + 5, height2); context.arc(0.5 + 5, height2 - 5, 5, Math.PI / 2, Math.PI, false); context.lineTo(0.5, 0.5 + 5); context.stroke(); } });
图中可以看到:updateChatlong.jsp 一直在 load 状态 ,因为服务器端 wait 了,在等待send.jsp notify,这样反映速度就很快了。
ps : pushlet简介
http://www.ibm.com/developerworks/cn/web/wa-lo-comet/
Extjs 聊天窗口 -续3 用pushlet来实现