springBoot 整合websocket
前言:最近開發公司的一個會員卡,酒店預訂房間的功能,考慮到要實現後臺管理的訂單提示,所以在專案中引用websocket,事實上在spring boot中引入webSocket 還是非常簡單的,但是我還是爬了很多坑,在此記錄一下希望可以幫助一些可能同樣會爬坑的同學。
1:jar包的引入,只引這個的前提是使用tomcat7.0以上的版本,我用的是tomcat8,所以用這個就夠了,若是低版本的還需引入javaee-api。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
2:配置檔案,這裡要注意一下,使用springboot的內建tomcat執行和打war包釋出到外部Tomcat執行時配置是不一樣的。
1.先建立一個WebSocketConfig.java
2.使用spring boot內建tomcat執行時的配置。
import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Component public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
這裡的配置是ServerEndpointExporter的注入,配置該出會為我們後面使用到@ServerEndPoint註解的地方自動註冊Websocket endpoint。
3.使用外部的tomcat釋出時 WebSocketConfig.java 中就不需要 ServerEndpointExporter 的注入,因為這是它是由外部容器自己提供和管理的,如果你在使用外部容器釋出時注入這個bean,專案啟動的時候會報 javax.websocket.DeploymentException: Multiple Endpoints may not be deployed to the same path xxxx錯誤,別問我是怎麼知道的(/"≡ _ ≡)/~┴┴。
所以這個時候的配置是這樣的:
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Component
public class WebSocketConfig {
// @Bean
// public ServerEndpointExporter serverEndpointExporter(){
// return new ServerEndpointExporter();
// }
}
3:上面都配置好後我們就來看websocket使用的重頭戲@ServerEndpoint的使用,前面有說。之前的配置就是為了是專案能在使用了註解的地方自動註冊Websocket endpoint,在這裡我們就能實現websocket的連結、關閉、傳送和接收訊息的操作,這檔案看起來有點像controller,但又有些不同,所以我就把他放在service層了這個有參考慕課網廖師兄的操作。
1.建立一個WebSocket.java。
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint(value = "/webSocket")
@Slf4j
public class WebSocket {
private Session session;
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(Session session){
this.session = session;
webSocketSet.add(this);
log.info("[WebSocket訊息]有新的連線,總數:{}", webSocketSet.size());
}
@OnClose
public void onClose(Session session){
webSocketSet.remove(this);
log.info("[WebSocket訊息]連線斷開,總數:{}", webSocketSet.size());
}
@OnMessage
public void onMessage(String message){
if("123456789".equals(message)){
sendMessage(message);
}
log.info("[WebSocket訊息]接收到客戶端的訊息:{}", message);
}
public void sendMessage(String message){
for (WebSocket webSocket:webSocketSet){
log.info("【websocket訊息】廣播訊息,message=:{}",message );
try {
webSocket.session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
其實看註解名字也能猜到各個方法的操作的是什麼。
@OnOpen:在前端訪問websocket時開一個新的連線。這裡有用一個集合來記錄連線,方便檢視管理;
@OnClose:斷開連線;
@OnMessage:接收訊息介面;
sendMessage:傳送訊息的介面;
到目前為止服務端的準備就做好了。
現在開始前端呼叫,這裡使用的是原生Html5來呼叫如下:
<script>
var lockReconnect = false;//避免重複連線
var wsUrl = "ws://localhost:8008/webSocketTest/webSocket";
var ws;
var tt;
createWebSocket();
function createWebSocket() {
try {
ws = new WebSocket(wsUrl);
init();
} catch(e) {
console.log('catch'+e);
reconnect(wsUrl);
}
}
function init() {
ws.onclose = function () {
console.log('連結關閉');
reconnect(wsUrl);
};
ws.onerror = function() {
console.log('發生異常了');
reconnect(wsUrl);
};
ws.onopen = function () {
console.log('建立連線');
//心跳檢測重置
heartCheck.start();
};
ws.onmessage = function (event) {
console.log('接收到訊息');
if(event.data!="123456789"){
console.log('收到訊息:'+event.data);
//彈窗提醒, 播放音樂
$('#myModal').modal('show');
document.getElementById('notice').play();
}
heartCheck.start();
//拿到任何訊息都說明當前連線是正常的
}
}
window.onbeforeunload = function () {
ws.close();
}
var lockReconnect = false;//避免重複連線
function reconnect(url) {
if(lockReconnect) {
return;
};
lockReconnect = true;
//沒連線上會一直重連,設定延遲避免請求過多
tt && clearTimeout(tt);
tt = setTimeout(function () {
createWebSocket(url);
lockReconnect = false;
}, 4000);
}
//心跳檢測
var heartCheck = {
timeout: 60000,
timeoutObj: null,
serverTimeoutObj: null,
start: function(){
console.log('start');
var self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function(){
//這裡傳送一個心跳,後端收到後,返回一個心跳訊息,
//onmessage拿到返回的心跳就說明連線正常
console.log('55555');
ws.send("123456789");
self.serverTimeoutObj = setTimeout(function() {
console.log(111);
console.log(ws);
ws.close();
// createWebSocket();
}, self.timeout);
}, this.timeout)
}
}
</script>
其實這裡和上面ServerEndpoint一樣,想必大家也是見文知意。
無非是初始化一個Websocket,建立連線,關閉連線,接收訊息,傳送訊息,這些就不多說了,主要在這裡說一下幾個需要注意的點。
1:初始化時 new WebSocket(wsUrl ); 這個url就是請求連線@ServerEndpoint配置的地方,所以說它的使用時候是不是有點像Controller的使用,拿上面的請求路徑做個說明:
var wsUrl = "ws://localhost:8008/webSocketTest/webSocket";
ws:是websocket的協議,websocket用的不是http協議,這裡我就不對這兩個協議的區別做詳細的說明,有興趣自己搜一下。需要注意的是如果請求頭的是http的就用ws,若請求的是https則使用wss。
localhost:就是服務的ip或域名。
8008:埠號。
webSocketTest:釋出的專案名。
webSocket:@ServerEndpoint中配置的路徑。
2.websocket長連線有預設的超時時間(proxy_read_timeout),就是超過一定的時間沒有傳送任何訊息,連線會自動斷開。所以我們要想保持長連線可以使用心跳包,定時像伺服器傳送心跳保持通訊,上面的js中就有使用,當發生錯誤或斷開連線時我們可以重新連線。
3.最後再提醒一下,如果專案部署到線上是使用了Nginx代理,一定要記得在Nginx配置websocket,前面有說過wesocket使用的不是http協議,如果你用了Nginx又沒有配置websocket的話,前端初始化websocket就會報404。
配置nginx反向代理響應webSocket請求 需要在代理的請求配置中加入下面的配置:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";1
Nginx配置
server {
listen 80;
server_name localhost;
#charset koi8-r;
location / {
root /usr/java/myproject/sell;
index index.html index.htm;
}
location /sell/ {
proxy_pass http://127.0.0.1:8081/sell/;
}
location /sell/webSocket {
proxy_pass http://127.0.0.1:8081/sell/webSocket;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}