认为长连接就是有个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来实现
本文介绍了一种基于长连接的实时聊天实现方案,通过客户端发起AJAX请求到服务器端的特定页面并保持连接,直到有新消息到达。服务器端采用Java Servlet处理逻辑,包括监听会话事件、管理在线用户和消息推送。
7347

被折叠的 条评论
为什么被折叠?



