前言
前面文章也有websocket相關(guān)的文章,為什么這次又要重新寫一篇呢?第一這篇文章需求業(yè)務(wù)場(chǎng)景有些不同,第二這篇文章websocket基本上完全基于注解操作簡(jiǎn)單脏里。
其實(shí)能實(shí)現(xiàn)定時(shí)消息推送的技術(shù)有很多,Dwr虹曙、goeasy迫横、comer4j 、netPush等技術(shù)也可以完全實(shí)現(xiàn)這個(gè)功能.
- DWR之前文檔的消息推送也有使用到酝碳,但是在實(shí)際項(xiàng)目中表現(xiàn)的并不是很好矾踱,畢竟技術(shù)相對(duì)較老,對(duì)于一些瀏覽器版本兼容性不是很好疏哗,而且容易出現(xiàn)消息丟失的情況呛讲,研究半天源碼改動(dòng)很多無(wú)法解決這個(gè)問(wèn)題。
- GoEasy 其實(shí)在消息推送方面表現(xiàn)還是比較良好返奉,但是其是收費(fèi)的贝搁,我們項(xiàng)目基本使用開(kāi)源產(chǎn)品對(duì)應(yīng)收費(fèi)產(chǎn)品合規(guī)性檢測(cè)肯定無(wú)法通過(guò),因此沒(méi)有考慮芽偏。
- 后面兩個(gè)技術(shù)沒(méi)有具體研究過(guò)雷逆,目前覺(jué)得webSocket可以完美解決我現(xiàn)在業(yè)務(wù)需求。
websocket簡(jiǎn)介
websocket協(xié)議是在http協(xié)議上的一種補(bǔ)充協(xié)議污尉,是html5的新特性膀哲,是一種持久化的協(xié)議。其實(shí)websocket和http關(guān)系并不是很大被碗,不過(guò)都是屬于應(yīng)用層的協(xié)議某宪。關(guān)于更多概念大家可以參考下面文章講述的很詳細(xì),接下來(lái)我們就開(kāi)始實(shí)戰(zhàn)锐朴。
websocket 定時(shí)推送
本教程基于springboot為腳手架缩抡,沒(méi)使用過(guò)springboot同學(xué)可以看往期文章,或者直接去spring官網(wǎng)拉一個(gè)springboot基礎(chǔ)項(xiàng)目下來(lái)。
加入依賴
在springboot的項(xiàng)目中添加一下webSocket依賴瞻想,一般一項(xiàng)新技術(shù)的引入在springboot中也只是引用一個(gè)此技術(shù)starter的依賴,其他配置基本springboot幫我們解決了娩嚼。
<!-- websocket 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置
新建一個(gè)Java配置類蘑险,注入ServerEndpointExporter 配置,如果是使用springboot內(nèi)置的tomcat此配置必須岳悟,如果是使用的是外部tomcat容器此步驟請(qǐng)忽略佃迄。看spring源碼中這樣描述贵少,使用此配置可以關(guān)閉servlet容器對(duì)websocket端點(diǎn)的掃描呵俏,這個(gè)暫時(shí)沒(méi)有深入研究。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
核心代碼
接下來(lái)最核心的類其實(shí)就是提供一個(gè)前后端交互的類實(shí)現(xiàn)消息的接收推送滔灶。
- @ServerEndpoint(value = "/wsdemo") 前端通過(guò)此URI 和后端交互普碎,建立連接
- @Component 不用說(shuō)將此類交給spring管理
- @OnOpen websocket建立連接的注解,前端觸發(fā)上面URI時(shí)會(huì)進(jìn)入此注解標(biāo)注的方法录平,和之前關(guān)于DWR文章中的onpage方法類似
- @OnClose 顧名思義關(guān)閉連接麻车,銷毀session
- @OnMessage 收到前端傳來(lái)的消息后執(zhí)行的方法
@ServerEndpoint(value = "/wsdemo")
@Component
public class MyWebSocket {
/**靜態(tài)變量,用來(lái)記錄當(dāng)前在線連接數(shù)斗这。應(yīng)該把它設(shè)計(jì)成線程安全的动猬。*/
private static int onlineCount = 0;
/** concurrent包的線程安全Set,用來(lái)存放每個(gè)客戶端對(duì)應(yīng)的MyWebSocket對(duì)象表箭。
在外部可以獲取此連接的所有websocket對(duì)象赁咙,并能對(duì)其觸發(fā)消息發(fā)送功能,我們的定時(shí)發(fā)送核心功能的實(shí)現(xiàn)在與此變量 */
private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
/**與某個(gè)客戶端的連接會(huì)話免钻,需要通過(guò)它來(lái)給客戶端發(fā)送數(shù)據(jù)*/
private Session session;
/**
* 連接建立成功調(diào)用的方法
*
* 類似dwr的onpage方法彼水,參考之前文章中demo有
* */
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在線數(shù)加1
System.out.println("有新連接加入!當(dāng)前在線人數(shù)為" + getOnlineCount());
try {
sendMessage("連接已建立成功.");
} catch (Exception e) {
System.out.println("IO異常");
}
}
/**
* 連接關(guān)閉調(diào)用的方法
*
* 參考dwrsession摧毀方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //連接關(guān)閉后伯襟,將此websocket從set中刪除
subOnlineCount(); //在線數(shù)減1
System.out.println("有一連接關(guān)閉猿涨!當(dāng)前在線人數(shù)為" + getOnlineCount());
}
/**
* 收到客戶端消息后調(diào)用的方法
*
* @param message 客戶端發(fā)送過(guò)來(lái)的消息*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("來(lái)自客戶端的消息:" + message);
}
// 錯(cuò)誤提示
@OnError
public void onError(Session session, Throwable error) {
System.out.println("發(fā)生錯(cuò)誤");
error.printStackTrace();
}
// 發(fā)送消息,在定時(shí)任務(wù)中會(huì)調(diào)用此方法
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
MyWebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
MyWebSocket.onlineCount--;
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
public static CopyOnWriteArraySet<MyWebSocket> getWebSocketSet() {
return webSocketSet;
}
public static void setWebSocketSet(CopyOnWriteArraySet<MyWebSocket> webSocketSet) {
MyWebSocket.webSocketSet = webSocketSet;
}
}
定時(shí)任務(wù)
使用spring的Schedule建立定時(shí)任務(wù)
- @EnableScheduling 開(kāi)啟spring定時(shí)任務(wù)功能
- @Scheduled(cron = "0/10 * * * * ?") 用于標(biāo)識(shí)定時(shí)執(zhí)行的方法姆怪,此處主要方法返回值一定是void叛赚,沒(méi)有入?yún)ⅰ?duì)應(yīng)定時(shí)時(shí)間配置可以百度cron語(yǔ)法稽揭,根據(jù)自己的業(yè)務(wù)選擇合適的周期
在這類中俺附,我們通過(guò)上面MyWebSocket提供的靜態(tài)方法獲取其中的webSocketSet ,來(lái)獲取所有此業(yè)務(wù)相關(guān)的所有websocketsession溪掀,可以在定時(shí)任務(wù)中對(duì)session內(nèi)容進(jìn)行驗(yàn)證判斷(權(quán)限驗(yàn)證等)事镣,進(jìn)行發(fā)送消息
@Component
@EnableScheduling
public class TimeTask
{
private static Logger logger = LoggerFactory.getLogger(TimeTask.class);
@Scheduled(cron = "0/1 * * * * ?") //每分鐘執(zhí)行一次
public void test(){
System.err.println("********* 定時(shí)任務(wù)執(zhí)行 **************");
CopyOnWriteArraySet<MyWebSocket> webSocketSet =
MyWebSocket.getWebSocketSet();
int i = 0 ;
webSocketSet.forEach(c->{
try {
c.sendMessage(" 定時(shí)發(fā)送 " + new Date().toLocaleString());
} catch (IOException e) {
e.printStackTrace();
}
});
System.err.println("/n 定時(shí)任務(wù)完成.......");
}
}
前端頁(yè)面
前端頁(yè)面可以參考使用,主要要更改調(diào)用的url為自己項(xiàng)目URL
<!DOCTYPE HTML>
<html>
<head>
<title>My WebSocket</title>
</head>
<body>
Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
//判斷當(dāng)前瀏覽器是否支持WebSocket ,主要此處要更換為自己的地址
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8886/wsdemo");
}
else{
alert('Not support websocket')
}
//連接發(fā)生錯(cuò)誤的回調(diào)方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//連接成功建立的回調(diào)方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
//接收到消息的回調(diào)方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//連接關(guān)閉的回調(diào)方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//監(jiān)聽(tīng)窗口關(guān)閉事件揪胃,當(dāng)窗口關(guān)閉時(shí)璃哟,主動(dòng)去關(guān)閉websocket連接氛琢,防止連接還沒(méi)斷開(kāi)就關(guān)閉窗口,server端會(huì)拋異常随闪。
window.onbeforeunload = function(){
websocket.close();
}
//將消息顯示在網(wǎng)頁(yè)上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//關(guān)閉連接
function closeWebSocket(){
websocket.close();
}
//發(fā)送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>