web即时通讯2--基于Spring websocket达到web聊天室

          如本文所用,Spring4和websocket要构建web聊天室,根据框架SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊天消息缓存。client使用socket.js和stomp.js来进行消息订阅和消息发送。具体实现见以下代码。

         首先在pom.xml中加入对spring websocket的相关依赖包。

         一、加入websocket依赖

<span style="font-size:14px;">             <dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${springframework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-websocket</artifactId>
			<version>${springframework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-messaging</artifactId>
			<version>${springframework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi</artifactId>
			<version>3.9</version>
		</dependency>
             <dependency>  
                <groupId>com.fasterxml.jackson.core</groupId>  
                <artifactId>jackson-core</artifactId>  
                <version>2.3.0</version>  
            </dependency>  
            <dependency>  
                <groupId>com.fasterxml.jackson.core</groupId>  
                <artifactId>jackson-databind</artifactId>  
                <version>2.3.0</version>  
            </dependency>  
            <dependency>  
                <groupId>com.fasterxml.jackson.core</groupId>  
                <artifactId>jackson-annotations</artifactId>  
                <version>2.3.0</version>  
            </dependency>   
</span>

       当中<springframework.version>4.0.3.RELEASE</springframework.version>。

由于spring4以上才支持WebSocket。

        2、配置Spring WebSocket

        该配置能够在Spring MVC的配置文件配置,也能够使用注解方式配置。本文使用注解@Configuration方式进行配置。

<span style="font-size:14px;">package com.test.chat.controller;

import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //加入这个Endpoint。这样在网页中就能够通过websocket连接上服务了
        registry.addEndpoint("/webchat").withSockJS();
    }
     
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        System.out.println("server启动成功");
        //这里设置的simple broker是指能够订阅的地址,也就是server能够发送的地址
        config.enableSimpleBroker("/userChat","/initChat","/initFushionChart","/updateChart","/videoChat");  
        config.setApplicationDestinationPrefixes("/app");   
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
    }

    @Override
    public void configureClientOutboundChannel(ChannelRegistration channelRegistration) {
    }

	@Override
	public void configureWebSocketTransport(
			WebSocketTransportRegistration registry) {
		// TODO Auto-generated method stub
		System.out.println("registry:"+registry);
	}

	@Override
	public boolean configureMessageConverters(
			List<MessageConverter> messageConverters) {
		// TODO Auto-generated method stub
		System.out.println("messageConverters:"+messageConverters);
		return true;
	}
}
</span>

    要使配置文件生效,需在Spring的文件里可以扫描到该文件所在的包。即配置<context:component-scan base-package="com.test.**.controller" />

       3、聊天内容的实体对象和后台关键代码

<span style="font-size:14px;">package com.test.chat.model;

public class ChatMessage {
	//房间号
	private String roomid;
	//username
	private String userName;
	//机构名
	private String deptName;
	//当前系统时间
	private String curTime;
	//聊天内容
	private String chatContent;
	//是否是系统消息
	private String isSysMsg;
	
	public String getIsSysMsg() {
		return isSysMsg;  
	}
	public void setIsSysMsg(String isSysMsg) {
		this.isSysMsg = isSysMsg;
	}
	public String getRoomid() {
		return roomid;
	}
	public void setRoomid(String roomid) {
		this.roomid = roomid;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getDeptName() {
		return deptName;
	}
	public void setDeptName(String deptName) {
		this.deptName = deptName;
	}
	public String getCurTime() {
		return curTime;
	}
	public void setCurTime(String curTime) {
		this.curTime = curTime;
	}
	public String getChatContent() {
		return chatContent;
	}
	public void setChatContent(String chatContent) {
		this.chatContent = chatContent;
	}

}
</span>
 

    后台关键处理代码,用于转发消息并缓存聊天记录

<span style="font-size:14px;">package com.test.chat.controller;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.alibaba.fastjson.JSONObject;
import com.test.chat.model.ChatMessage;
import com.test.chat.model.LimitQueue;
import com.test.chat.model.VideoMessage;
import com.test.framework.common.SessionContainer;
import com.test.framework.service.GenericService;
import com.test.framework.utils.DateUtil;

@Controller
public class UserChatController {
        //每一个聊天室缓存最大聊天信息条数,该值由SpringMVC的配置文件注入,超过该值将清理出缓存
	private int MAX_CHAT_HISTORY;

	public void setMAX_CHAT_HISTORY(int MAX_CHAT_HISTORY) {
		this.MAX_CHAT_HISTORY = MAX_CHAT_HISTORY;
	}

	@Resource
	private GenericService genericService;
	// 用于转发数据 sendTo
	private SimpMessagingTemplate template;
        //消息缓存列表
        private Map<String, Object> msgCache = new HashMap<String, Object>();

	@Autowired
	public UserChatController(SimpMessagingTemplate t) {
		template = t;
	}


	/**
	 * WebSocket聊天的对应接收方法和转发方法
	 * client通过app/userChat调用该方法,并将处理的消息发送client订阅的地址
	 * @param userChat     关于用户聊天的各个信息
	 */
	@MessageMapping("/userChat")
	public void userChat(ChatMessage chatMessage) {
		// 找到须要发送的地址(client订阅地址)
		String dest = "/userChat/chat" + chatMessage.getRoomid();
		// 获取缓存,并将用户最新的聊天记录存储到缓存中
		Object cache = msgCache.get(chatMessage.getRoomid());
		try {
			chatMessage.setRoomid(URLDecoder.decode(chatMessage.getRoomid(),"utf-8"));
			chatMessage.setUserName(URLDecoder.decode(chatMessage.getUserName(), "utf-8"));
			chatMessage.setDeptName(URLDecoder.decode(chatMessage.getDeptName(), "utf-8"));
			chatMessage.setChatContent(URLDecoder.decode(chatMessage.getChatContent(), "utf-8"));
			chatMessage.setIsSysMsg(URLDecoder.decode(chatMessage.getIsSysMsg(),"utf-8"));
			chatMessage.setCurTime(DateUtil.format(new Date(),DateUtil.formatStr_yyyyMMddHHmmss));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace(); 
		}
		// 发送用户的聊天记录
		this.template.convertAndSend(dest, chatMessage);
                ((LimitQueue<ChatMessage>) cache).offer(chatMessage);
	}

	
	@SubscribeMapping("/initChat/{roomid}")
	public LimitQueue<ChatMessage> initChatRoom(@DestinationVariable String roomid) {
		System.out.print("-------新用户进入聊天室------");
		LimitQueue<ChatMessage> chatlist = new LimitQueue<ChatMessage>(MAX_CHAT_HISTORY);
		// 发送用户的聊天记录
		if (!msgCache.containsKey(roomid)) {
			// 从来没有人进入聊天空间
			msgCache.put(roomid, chatlist);
		} else {
			chatlist = (LimitQueue<ChatMessage>) msgCache.get(roomid);
		}
		return chatlist;
	}
	
}
</span>
在Spring的配置文件里注入MAX_CHAT_HISTRORY
<span style="font-size:14px;">    <bean id="userChatController" class="com.test.chat.controller.UserChatController">
        <property name="MAX_CHAT_HISTORY" value="20"/>
    </bean>       </span>

当中缓存队列LimitQueue的实现为:
<span style="font-size:14px;">package com.test.chat.model;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;

public class LimitQueue<E> implements Queue<E> {
	private int limit;
	private Queue<E> queue;

	public LimitQueue(int limit) {
		this.limit = limit;
		this.queue = new LinkedList<E>();
	}

	@Override
	public int size() {
		return queue.size();
	}

	@Override
	public boolean isEmpty() {
		return queue.isEmpty();
	}

	@Override
	public boolean contains(Object o) {
		return queue.contains(o);
	}

	@Override
	public Iterator<E> iterator() {
		return queue.iterator();
	}

	@Override
	public Object[] toArray() {
		return queue.toArray();
	}

	@Override
	public <T> T[] toArray(T[] a) {
		return queue.toArray(a);
	}

	@Override
	public boolean add(E e) {
		return queue.add(e);
	}

	@Override
	public boolean remove(Object o) {
		return queue.remove(0);
	}

	@Override
	public boolean containsAll(Collection<?> c) {
		return queue.containsAll(c);
	}

	@Override
	public boolean addAll(Collection<? extends E> c) {
		return queue.addAll(c);
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		return queue.removeAll(c);
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		return queue.retainAll(c);
	}

	@Override
	public void clear() {
		queue.clear();
	}

	@Override
	public boolean offer(E e) {
		if (queue.size() >= limit) {
			queue.poll();
		}
		return queue.offer(e);
	}

	@Override
	public E remove() {
		return queue.remove();
	}

	@Override
	public E poll() {
		return queue.poll();
	}

	@Override
	public E element() {
		return queue.element();
	}

	@Override
	public E peek() {
		return queue.peek();
	}

	public int getLimit() {
		return this.limit;
	}
}
</span>

         四、前台聊天室的实现(前台界面使用dhtmlx控件)

<span style="font-size:14px;"><%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
	<head>
		<title>聊天室管理</title>
		<meta http-equiv="content-type" content="text/html; charset=utf-8" /> 
		<link rel="stylesheet" type="text/css" href="/dhtmlx/dhtmlxEditor/codebase/skins/dhtmlxeditor_dhx_skyblue.css">
		<script src="/common/js/lib-base.js" type="text/javascript"></script> 
		<script src="/dhtmlx/dhtmlxEditor/codebase/dhtmlxeditor.js" type="text/javascript"></script>
		<!-- <script src="/dhtmlx/dhtmlxEditor/codebase/ext/dhtmlxeditor_ext.js" type="text/javascript"></script> -->
		<!-- web chat 引入相关脚本 -->
		<script src="/common/js/websocket/sockjs-0.3.4.min.js" type="text/javascript"></script>
		<script src="/common/js/websocket/stomp.js" type="text/javascript"></script>
		<!----------------end------------------->
		<script>  
	var chatLayout; 
	var roomid="${roomid}";  
	var roomName=null;
	var friendTree=null;
	var userid=null;
	var username=null; 
	var deptSortName=null;
	var editor=null;
	$(function(){   
		commonForm.initForm(); 
		chatLayout= new dhtmlXLayoutObject(document.body, "3J");
		ajaxPost("/chatroom/findById",{"id":roomid},function(data,status){
			chatLayout.cells("a").setText("<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle' style='margin-right:5px'>"+data.roomName);
			roomName=data.roomName;
			$("#roomRemark").html(data.remark); 
		}) ;  
		chatLayout.cells("a").hideHeader(); 
		chatLayout.cells("a").attachObject("chatMsg");
		chatLayout.cells("c").setHeight(150); 
		chatLayout.cells("c").hideHeader();     
		chatLayout.setAutoSize("a;c","a;b");   
        chatLayout.cells("b").setWidth(180); 
        var friendLayout=chatLayout.cells("b").attachLayout("2E"); 
        friendTree=friendLayout.cells("b").attachTree(); 
        friendLayout.cells("a").setText("<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle' style='margin-right:5px'>群公告");
        friendLayout.cells("a").attachObject("roomRemark");
        friendLayout.cells("a").setHeight(100);
        friendLayout.cells("b").setText("<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle' style='margin-right:5px'>好友列表");
        friendLayout.setAutoSize("a;b","b");  
        //载入好友列表树 
        ajaxPost("/auth/getCurUser",null,function(data,status){
        	userid=data.id;
        	username=data.name;
        	deptSortName=data.deptSortName;
        }) 
        loadChatFriend();
        //载入聊天
        var talkLayout=chatLayout.cells("c").attachLayout("2E");
        talkLayout.cells("a").hideHeader();
        talkLayout.cells("b").hideHeader();
        talkLayout.cells("b").setHeight(29); 
        //dhtmlx.image_path="/dhtmlx/dhtmlxEditor/codebase/imgs/";   
        editor=talkLayout.cells("a").attachEditor(); 
        var toolbar=talkLayout.cells("b").attachToolbar();
        toolbar.setIconsPath("/images/Pub/");
        var tbindex=0; 
		toolbar.addSeparator("sep1", tbindex++);  
		toolbar.addSpacer("sep1"); 
		toolbar.addButton("closeChat", tbindex++, "关闭", "delete.png","delte.png");
		toolbar.addSeparator("sep2",tbindex++);  
		toolbar.addButton("videoChat", tbindex++, "视频", "FrameReLogin.gif","FrameReLogin.gif");
		toolbar.addSeparator("sep3",tbindex++);  
		toolbar.addButton("sendMessage", tbindex++, "发送", "redo.gif","redo.gif");
		
		toolbar.attachEvent("onclick",function(tid){
			switch(tid){
				case "sendMessage":
					if(editor.getContent()=="" )
						return;
					sendMessage("0");
					editor.setContent("");   
					break; 
				case "closeChat":  
					sendMessage("1","离开"); 
					parent.dhxWins.window("chatWin").close();
					break;  
				case "videoChat": 
        			top.openWindow("/video/openVideoChat?

roomid="+roomid,"videoWin","聊天室【"+roomName+"】",650,550,false,false,true); break; default: break; } }) }); function loadChatFriend(){ friendTree.setSkin('dhx_skyblue'); friendTree.setImagePath("/dhtmlx/dhtmlxTree/codebase/imgs/csh_dhx_skyblue/"); ajaxPost("/chatroom/getChatFriends",{"roomid":roomid},function(data,status){ friendTree.deleteChildItems(friendTree.rootId); $.each(data,function(index,item){ var id=item.user.id; var deptName=item.user.corg.shortName; var userName=item.user.name; var isCreator=item.isCreator; friendTree.insertNewItem(friendTree.rootId,id,deptName+"--"+userName+(isCreator=="1"?

"(群主)":""),0,0,0,0,""); if(userid==id) friendTree.setItemColor(id,"red",""); }) }) } //---------------------------------------聊天室关键代码(websocket)--------------------------------------- var stompClient=null;content=null; $(function(){ connect(); }) //connect the server function connect(){ var socket=new SockJS("/webchat"); stompClient=Stomp.over(socket); stompClient.connect('','',function(frame){ console.log('Connected: '+frame); //用户聊天订阅 //alert("hello: "+frame); stompClient.subscribe("/userChat/chat"+roomid,function(chat){ showChat(JSON.parse(chat.body)); }); //初始化 stompClient.subscribe("/app/initChat/"+roomid,function(initData){ //alert("初始化聊天室"); console.log(initData); content=JSON.parse(initData.body); //content=body.document.content; //alert(content+":"+content.document.content); content.forEach(function(item){ showChat(item); }); sendMessage("1","进入"); }); },function(){ connect(); }); } //显示聊天信息 function showChat(message){ var htmlMsg=decodeURIComponent(message.chatContent); var image="<img src='/images/Pub/User.gif' width='16px' height='16px' align='absmiddle'/>"; var userMsg=decodeURIComponent(message.deptName) +"--"+decodeURIComponent(message.userName)+"   "+decodeURIComponent(message.curTime)+"</font>"; htmlMsg=userMsg+"<br/>    "+htmlMsg; if(htmlMsg!="") { if($("#chatMsg").html()!=""){ if(message.isSysMsg=="1") $("#chatMsg").append("<br/><div style='text-align:center'><font color='gray'>"+htmlMsg+"</div>"); else $("#chatMsg").append("<br/>"+image+"<font color='blue'>"+htmlMsg); } else { if(message.isSysMsg=="1") $("#chatMsg").append("<div style='text-align:center'><font color='gray'>"+htmlMsg+"</div>"); else $("#chatMsg").append(image+"<font color='blue'>"+htmlMsg); } $("#chatMsg")[0].scrollTop=$("#chatMsg")[0].scrollHeight; } } function sendMessage(isSysMsg,textMsg){ var chatCont=editor.getContent(); if(isSysMsg=="1"){ chatCont="<font color='gray'>"+textMsg+"聊天室</font>"; } stompClient.send("/app/userChat",{},JSON.stringify({ 'roomid':encodeURIComponent(roomid), 'userName':encodeURIComponent(username), 'deptName':encodeURIComponent(deptSortName), 'chatContent':encodeURIComponent(chatCont), 'isSysMsg':encodeURIComponent(isSysMsg) })) } //--------------------------------------------------------------------------------------------------------------- </script> </head> <body style="width:100%;height:100%;margin:0px;overflow:hidden;"> <div id="roomRemark"></div> <div style="position:relative;width:99%;height:100%;overflow:auto;display:none;margin-left:5px;" id="chatMsg"></div> </body> </html> </span>

       五、实现效果

     创建人tester4进入后,输入聊天内容后,退出。


     聊天室好友tester1进入并发言


   文中代码部分參考了Spring WebSocket教程(一)Spring WebSocket教程(二)。


     下一篇文章将这个网页上介绍加入聊天室视频聊天。

版权声明:本文博主原创文章。博客,未经同意不得转载。

posted @ 2015-09-13 12:18  blfshiye  阅读(362)  评论(0编辑  收藏  举报