SSM框架+WebSocket實現網頁聊天(Spring+SpringMVC+MyBatis+WebSocket)
建站不止於增刪改查,還有很多很有魅力的地方。對於通訊聊天這塊已經青睞好久了,前段時間在做的j2ee專案運用到Spring+SpringMVC+MyBatis的框架集合,是關於一個社交平臺的網站,類似於facebook,twitter,微博等。在做完基本的CURD(例如評論模組)後,開始研究網站通訊並應用於專案中。
提到通訊,大家都知道Socket。確實,運用Socket能在伺服器與客戶端之間建立一個數據交換的通道。之前用java SE寫過的Socket通訊 —模擬使用者登入簡單地實現了伺服器與客戶端傳送訊息。但是再細想一下,如果要在專案中實現網頁聊天功能,把Socket用到j2ee專案中,或許就沒那麼簡單了。這時轉向baidu與google尋找答案,原來,有WebSocket這套協議,關於WebSocket,來自IBM這兩篇文章已經介紹地很詳細了:
Spring Framework 4 includes a new spring-websocket module with comprehensive WebSocket support. It is compatible with the Java WebSocket API standard (JSR-356) and also provides additional value-add as explained in the rest of the introduction.來自Spring官方文件:
非常慶幸的是,在Spring 4.0以上開始支援WebSocket了,並給出一套API供開發者使用。
下面就開始講解WebSocket如何應用於SSM框架,說明其中的工作原理,並在最後給出網頁聊天效果圖。
一:客戶端(js)新建WebSocket物件,指定要進行握手連線的伺服器地址:
var webSocket = new WebSocket("ws://"+socketPath+"/ws"); webSocket.onopen = function(event){ console.log("連線成功"); console.log(event); }; webSocket.onerror = function(event){ console.log("連線失敗"); console.log(event); }; webSocket.onclose = function(event){ console.log("Socket連線斷開"); console.log(event); }; webSocket.onmessage = function(event){ //接受來自伺服器的訊息 //... }
講解:
在新建WebSocket物件時,給出的引數字串中ws表明協議使用的是WebSocket協議,socketPath就是要連線的伺服器地址,在下文會進一步說明。 如果成功連線,就會執行onopen;如果連線失敗,就會執行onerror;如果連線斷開,就會執行onclose,如果伺服器有訊息傳送過來,就會執行onmessage。
二:服務端匯入Spring WebSocket相關jar依賴:
<!--WebSocket 依賴 -->
<dependency>
<groupid>org.springframework</groupid>
spring-messaging</artifactid>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupid>org.springframework</groupid>
spring-websocket</artifactid>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupid>com.google.code.gson</groupid>
gson</artifactid>
<version>2.3.1</version>
</dependency>
講解:
關於SpringMVC,Mybatis的jar包依賴就不列出來了。本文重點為如何在SSM框架上應用WebSocket。
三:伺服器新增WebSocket服務:
package web.webSocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* Component註解告訴SpringMVC該類是一個SpringIOC容器下管理的類
* 其實@Controller, @Service, @Repository是@Component的細化
*/
@Component
@EnableWebSocket
public class MyWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
@Autowired
MyWebSocketHandler handler;
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
//新增websocket處理器,新增握手攔截器
webSocketHandlerRegistry.addHandler(handler, "/ws").addInterceptors(new MyHandShakeInterceptor());
//新增websocket處理器,新增握手攔截器
webSocketHandlerRegistry.addHandler(handler, "/ws/sockjs").addInterceptors(new MyHandShakeInterceptor()).withSockJS();
}
}
講解: 首先說說上文提到的客戶端指定握手連線的伺服器地址:在jsp中定義socketPath為 String socketPath = request.getServerName()+”:”+request.getServerPort()+path+”/”;其中path的定義為 String path = request.getContextPath();相信做SSM專案的你們都很清楚了。getServerName()先得到伺服器機的ip地址;getServerPort()得到相應的埠號;getContextPath()得到的是上下文路徑,其實就是釋出了的專案資料夾的檔名,我釋出了的專案資料夾名為web,在這個資料夾下有META-INF,WEB-INF和一個預設的index.jsp,WEB-INF內的頁面是不允許外界訪問的,所以當我們要訪問裡面的jsp頁面時唯一的方法就是通過springMVC的對映,不是嗎?最後我把專案釋出到遠端伺服器上並通過外網進行測試連線到的路徑為: “ws://139.129.47.176:8089/web//ws” 我在139.129.47.176:8089/web/後面加上一些事先規定好的對映匹配字元就能訪問頁面。因此我總結的就是:139.129.47.176:8089/web/就能得到我Tomcat容器下的SpringIOC容器,裡面都是我寫好的controller,service介面物件。 注意到上面程式碼中有@Component註解,已經給出註釋了,就是相當於告訴SpringMVC這是SpringIOC容器下管理的類,和@Controller註解其實是一樣的,通過139.129.47.176:8089/web/能訪問到Controller並做對映,通過139.129.47.176:8089/web/同樣可以訪問MyWebSocketConfig這個類在SpringIOC下的物件,從而服務端進行WebSocket服務。 寫到這裡,相信熟悉SSM執行流程的你們都應該懂WebSocket的路徑了。 在上面的程式碼中出現到MyWebSocketHandler handler;與new MyHandShakeInterceptor()。其中handler規定了服務端WebSocket的處理。而MyHandShakeInterceptor是客戶端與服務端握手連線前後攔截器。
四:握手攔截器MyHandShakeInterceptor:
package web.webSocket;
import entity.User;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* websocket握手攔截器
* 攔截握手前,握手後的兩個切面
*/
public class MyHandShakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<string, object=""> map) throws Exception {
System.out.println("Websocket:使用者[ID:" + ((ServletServerHttpRequest) serverHttpRequest).getServletRequest().getSession(false).getAttribute("user") + "]已經建立連線");
if (serverHttpRequest instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) serverHttpRequest;
HttpSession session = servletRequest.getServletRequest().getSession(false);
// 標記使用者
User user = (User) session.getAttribute("user");
if(user!=null){
map.put("uid", user.getUserId());//為伺服器建立WebSocketSession做準備
System.out.println("使用者id:"+user.getUserId()+" 被加入");
}else{
System.out.println("user為空");
return false;
}
}
return true;
}
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
}
}
講解:
攔截的概念就是在一個操作前,與在這個操作後的兩個時間切面將要進行的動作。 客戶端與服務端握手連線前將鍵名”uid“,值為使用者id的這個鍵值對加入到指定引數map中。為伺服器建立與相應客戶端連線的WebSocketSession打下基礎。
五:MyWebSocketHandler,WebSocket處理器:
package web.webSocket;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import entity.Message;
import service.youandmeService;
@Component
public class MyWebSocketHandler implements WebSocketHandler{
@Autowired
private youandmeService youandmeService;
//當MyWebSocketHandler類被載入時就會建立該Map,隨類而生
public static final Map<integer, websocketsession=""> userSocketSessionMap;
static {
userSocketSessionMap = new HashMap<integer, websocketsession="">();
}
//握手實現連線後
public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
int uid = (Integer) webSocketSession.getAttributes().get("uid");
if (userSocketSessionMap.get(uid) == null) {
userSocketSessionMap.put(uid, webSocketSession);
}
}
//傳送資訊前的處理
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<!--?--> webSocketMessage) throws Exception {
if(webSocketMessage.getPayloadLength()==0)return;
//得到Socket通道中的資料並轉化為Message物件
Message msg=new Gson().fromJson(webSocketMessage.getPayload().toString(),Message.class);
Timestamp now = new Timestamp(System.currentTimeMillis());
msg.setMessageDate(now);
//將資訊儲存至資料庫
youandmeService.addMessage(msg.getFromId(),msg.getFromName(),msg.getToId(),msg.getMessageText(),msg.getMessageDate());
//傳送Socket資訊
sendMessageToUser(msg.getToId(), new TextMessage(new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().toJson(msg)));
}
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
}
/**
* 在此重新整理頁面就相當於斷開WebSocket連線,原本在靜態變數userSocketSessionMap中的
* WebSocketSession會變成關閉狀態(close),但是重新整理後的第二次連線伺服器建立的
* 新WebSocketSession(open狀態)又不會加入到userSocketSessionMap中,所以這樣就無法傳送訊息
* 因此應當在關閉連線這個切面增加去除userSocketSessionMap中當前處於close狀態的WebSocketSession,
* 讓新建立的WebSocketSession(open狀態)可以加入到userSocketSessionMap中
* @param webSocketSession
* @param closeStatus
* @throws Exception
*/
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
System.out.println("WebSocket:"+webSocketSession.getAttributes().get("uid")+"close connection");
Iterator<map.entry<integer,websocketsession>> iterator = userSocketSessionMap.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<integer,websocketsession> entry = iterator.next();
if(entry.getValue().getAttributes().get("uid")==webSocketSession.getAttributes().get("uid")){
userSocketSessionMap.remove(webSocketSession.getAttributes().get("uid"));
System.out.println("WebSocket in staticMap:" + webSocketSession.getAttributes().get("uid") + "removed");
}
}
}
public boolean supportsPartialMessages() {
return false;
}
//傳送資訊的實現
public void sendMessageToUser(int uid, TextMessage message)
throws IOException {
WebSocketSession session = userSocketSessionMap.get(uid);
if (session != null && session.isOpen()) {
session.sendMessage(message);
}
}
}
講解:
這個處理器的@Component註解就是告訴Spring將這個類的物件注入到IOC容器中,這樣在MyWebSocketConfig中才可以通過@Autowired將其自動裝載,進而使用。 簡單地說說這個處理器,握手實現連線後會執行afterConnectionEstablished()方法,這個方法就是將握手連線後為與客戶端實現通訊而建立的WebSocketSession加入到靜態變數userSocketSessionMap中。 當客戶端斷開連線後會執行afterConnectionClosed(),這時需要將與客戶端對應的WebSocketSession從userSocketSessionMap中移除,原因已在註釋中給出,簡直血的教訓,除錯了好久才發現…. 客戶端一有訊息傳送至伺服器就會自動執行handleMessage()方法,其中Message msg=new Gson().fromJson(webSocketMessage.getPayload().toString(),Message.class);將JSON形式的資料解析成Message物件,Message的定義稍後給出。 伺服器傳送資訊至客戶端只需要一句話,就是通過在伺服器中WebSocketSession的sendMessage()方法,詳情都在程式碼中。
六:客戶端傳送資訊與接受資訊:
傳送:
var data = {};//新建data物件,並規定屬性名與相應的值
data['fromId'] = sendUid;
data['fromName'] = sendName;
data['toId'] = to;
data['messageText'] = $(".contactDivTrue_right_input").val();
webSocket.send(JSON.stringify(data));//將物件封裝成JSON後傳送至伺服器
接收:
var message = JSON.parse(event.data);//將資料解析成JSON形式
講解:
傳送資訊時需要將物件轉換為JSON形式的資料,因為伺服器本來就是將JSON資料轉換成物件的。 客戶端接收資訊時將資料解析成JSON形式後就能在js中獲取相應的資料。
七:Message類:
package entity;
import java.sql.Timestamp;
import java.util.Date;
/**
* Created by Administrator on 2016/8/15.
*/
public class Message {
private int messageId;
private int fromId;
private String fromName;
private int toId;
private String messageText;
private Timestamp messageDate;
public Message() {
}
public int getMessageId() {
return messageId;
}
public void setMessageId(int messageId) {
this.messageId = messageId;
}
public int getFromId() {
return fromId;
}
public void setFromId(int fromId) {
this.fromId = fromId;
}
public String getFromName() {
return fromName;
}
public void setFromName(String fromName) {
this.fromName = fromName;
}
public int getToId() {
return toId;
}
public void setToId(int toId) {
this.toId = toId;
}
public String getMessageText() {
return messageText;
}
public void setMessageText(String messageText) {
this.messageText = messageText;
}
public Timestamp getMessageDate() {
return messageDate;
}
public void setMessageDate(Timestamp messageDate) {
this.messageDate = messageDate;
}
@Override
public String toString() {
return "Message{" +
"messageId=" + messageId +
", fromId=" + fromId +
", fromName='" + fromName + '\'' +
", toId=" + toId +
", messageText='" + messageText + '\'' +
", messageDate=" + messageDate +
'}';
}
}
寫到這裡,如何在SSM中運用WebSocket基本已經講完了,接下來就是實戰了。這裡沒有給出具體程式碼包。原因有二:
具體程式碼是整個專案的程式碼,大家下載後在本地測試還涉及資料庫等多方面問題,比較麻煩,只會增加大家的負擔。 程式設計師要注重自己實現,自己debug查錯查問題,舉一反三的能力。關於如何應用的步驟已經給得很詳細了,授之於魚不如授之以漁。
最後給出一些專案的WebSocket聊天實現圖:
楊千嬅跟我是黃復貴的聊天:
Hill跟我是黃復貴的聊天:
我是黃復貴同時跟楊千嬅,Hill聊天: