web即時通訊2--基於Spring websocket實現web聊天室
阿新 • • 發佈:2019-02-09
本文使用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進入後,輸入聊天內容後,退出。
聊天室好友tester1進入併發言
下一篇文章將講解在此網頁聊天室上新增視訊聊天功能。