1. 程式人生 > >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進入後,輸入聊天內容後,退出。

     聊天室好友tester1進入併發言

     下一篇文章將講解在此網頁聊天室上新增視訊聊天功能。