转自:http://blog.csdn.net/jrn1012/article/details/42079821
文中代码部分参考了Spring WebSocket教程(一)和Spring WebSocket教程(二)。
本文使用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>
- <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教程(二)。