web即时通信2-基于Spring websocket实现web聊天室

web即时通信2--基于Spring websocket实现web聊天室

          本文使用Spring4和websocket搭建一个web聊天室,框架基于SpringMVC+Spring+Hibernate的Maven项目,后台使用spring websocket进行消息转发和聊天消息缓存。客户端使用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("服务器启动成功");
        //这里设置的simple broker是指可以订阅的地址,也就是服务器可以发送的地址
        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;
	//用户名
	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聊天的相应接收方法和转发方法
	 * 客户端通过app/userChat调用该方法,并将处理的消息发送客户端订阅的地址
	 * @param userChat     关于用户聊天的各个信息
	 */
	@MessageMapping("/userChat")
	public void userChat(ChatMessage chatMessage) {
		// 找到需要发送的地址(客户端订阅地址)
		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进入后,输入聊天内容后,退出。web即时通信2-基于Spring websocket实现web聊天室


     聊天室好友tester1进入并发言

web即时通信2-基于Spring websocket实现web聊天室
   文中代码部分参考了Spring WebSocket教程(一)Spring WebSocket教程(二)。

     下一篇文章将讲解在此网页聊天室上添加视频聊天功能。