因爲項目需求,後臺與前端需要保持長期的連接,後臺可以主動地向前端發送數據。因爲WebSocket一直很熱門,因此趁此機會學習一下。
一、後端代碼
(1)添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
(2)WebSocketConfig.java
我看到別人關於這個類的解釋是:注入ServerEndpointExporter,這個bean會自動註冊使用了@ServerEndpoint註解聲明的Websocket endpoint
package com.example.demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(3)WebSocket操作類
在這個類中寫WebSocket連接、斷開、發送數據等操作。
@ServerEndpoint(value = “/websocket/{sessionId}”)中的URL爲前端進行WebSocket連接時會用到的URL。
package com.example.demo;
import org.springframework.stereotype.Component;
import javax.websocket.EncodeException;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
//@ServerEndpoint(value = "/websocket")
@ServerEndpoint(value = "/websocket/{sessionId}")
@Component
public class CustomWebSocket {
//靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
private static int onlineCount = 0;
//concurrent包的線程安全Set,用來存放每個客戶端對應的CumWebSocket對象。
private static CopyOnWriteArraySet<CustomWebSocket> webSocketSet = new CopyOnWriteArraySet<>();
//存儲sessionId
private static Map<String, Session> sessionPool = new HashMap<>();
//與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
//連接建立成功調用的方法
@OnOpen
public void onOpen(Session session, @PathParam(value = "sessionId")String sessionId){
this.session = session;
webSocketSet.add(this);
sessionPool.put(sessionId,session);
//添加在線人數
addOnlineCount();
System.out.println("新連接接入。當前在線人數爲:" + getOnlineCount());
}
//連接關閉調用的方法
@OnClose
public void onClose() {
//從set中刪除
webSocketSet.remove(this);
//在線數減1
subOnlineCount();
System.out.println("有連接關閉。當前在線人數爲:" + getOnlineCount());
}
/**
* 收到客戶端消息後調用
*
* @param message
*
*/
@OnMessage
public void onMessage(String message) {
System.out.println("客戶端發送的消息:" + message);
}
// 此爲廣播消息
public void sendAllMessage(String message) {
for(CustomWebSocket webSocket : webSocketSet) {
System.out.println("【websocket消息】廣播消息:"+message);
try {
webSocket.session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此爲單點消息
public void sendOneMessage(String sessionId, String message) {
Session session = sessionPool.get(sessionId);
if (session != null) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 減少在線人數
*/
private void subOnlineCount() {
CustomWebSocket.onlineCount--;
}
/**
* 添加在線人數
*/
private void addOnlineCount() {
CustomWebSocket.onlineCount++;
}
/**
* 當前在線人數
*
* @return
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
}
(4)Controller
給不同的Vue組件發送不同的消息,這裏只是模擬數據,數據寫成靜態的:
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@RestController
@RequestMapping("/ws")
public class WebSocketController {
@Autowired
private CustomWebSocket webSocket;
@RequestMapping("/sendLocationData")
public String testLocationData() throws Exception{
Random random = new Random();
new Thread() {
@Override
public void run() {
while (true) {
try {
int i = random.nextInt(10);
Location location = new Location(39.930 + i,119.404);
//webSocket.sendAllMessage(JsonUtils.objectToJson(location));
webSocket.sendOneMessage("location",JsonUtils.objectToJson(location));
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
return "websocket羣體發送LocationData!";
}
@RequestMapping("/sendCarData")
public String testCarData() throws Exception{
CarData carData = new CarData(26,0.0001,2.344444,0.56);
//webSocket.sendAllMessage(JsonUtils.objectToJson(carData));
webSocket.sendOneMessage("carData",JsonUtils.objectToJson(carData));
return "websocket羣體發送CarData!";
}
}
二、前端代碼
(1)Map.vue
該組件的功能是接收後端發送的座標參數,在地圖上繪製出一條軌跡。
<template>
<div id="container">
<baidu-map class="map"
:center="{lng: 116.404, lat: 39.915}"
:zoom="15"
:scroll-wheel-zoom="true" >
<bm-polyline
stroke-color="blue"
:stroke-opacity="0.5"
:stroke-weight="12"
:path="path">
</bm-polyline>
</baidu-map>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: "Map",
data() {
return {
path: [
//如果初始爲空,好像數組裏面添加進去也不描繪軌跡
{lng: 116.404, lat: 39.915},
{lng: 117.405, lat: 39.920},
{lng: 118.423493, lat: 39.907445}
],
websocket: null
}
},
destroyed() {
this.websocket.close()
},
methods: {
initWebSocket() {
if(typeof (WebSocket) == 'undefined'){
alert('不支持websocket')
return false
}
const wsuri = 'ws://localhost:8080/websocket/location'
this.websocket = new WebSocket(wsuri)
this.websocket.onopen = this.websocketonopen
this.websocket.onmessage = this.websocketonmessage
this.websocket.onerror = this.websocketonerror
this.websocket.onclose = this.websocketclose
},
//連接成功
websocketonopen(){
console.log('WebSocket連接成功')
},
//接收數據
websocketonmessage(e) {
console.log('array數組:')
console.log(this.path)
console.log("message-----------")
// console.log(e)
//收到的數據:
// {
// "lat" : "40.7838351",
// "lng" : "-74.6143763"
// }
console.log(e.data)
var obj = JSON.parse(e.data)
//轉換後的對象:
//{lat: "40.7838351", lng: "-74.6143763"}
console.log(obj)
this.path.push(obj)
console.log('增加後的array數組:')
console.log(this.path)
},
//連接建立失敗重連
websocketonerror(e){
console.log(`連接失敗的信息:`, e)
this.initWebSocket() // 連接失敗後嘗試重新連接
},
//關閉連接
websocketclose(e){
console.log('斷開連接',e)
},
created() {
//暫時屏蔽
// this.connect()
//測試websocket
this.initWebSocket()
}
}
</script>
<style scoped>
/*#map {*/
/* width: 100%;*/
/* height: 90%;*/
/*}*/
.map {
width: 100%;
height: 850px;
}
</style>
(2)Car.vue
該組件的功能是接收後端發送的傳感器數據並顯示。
<template>
<div id="container">
<div id="data-card">
<el-row :gutter="20">
<el-col :span="6">
<div class="grid-content bg-purple">
<el-card :body-style="{ padding: '0px' }">
<div class="weather-pic">
<img src="../assets/xiugaihou/weather.jpg" class="image">
</div>
<div style="padding: 14px;">
<span class="weather-data">當前溫度:</span>
<span class="data">{{weatherData}}</span>
<span class="data">℃</span>
<div class="bottom clearfix">
</div>
</div>
</el-card>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple">
<el-card :body-style="{ padding: '0px' }">
<div >
<img src="../assets/xiugaihou/rainy.jpg" class="rainy-image">
</div>
<div style="padding: 14px;">
<span class="weather-data">當前溼度:</span>
<span class="data">{{moistData}}</span>
<span class="data">hPa</span>
<div class="bottom clearfix">
</div>
</div>
</el-card>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple">
<el-card :body-style="{ padding: '0px' }">
<img src="../assets/xiugaihou/smoke.jpg" class="image">
<div style="padding: 14px;">
<span class="weather-data">當前煙霧濃度:</span>
<span class="data">{{smokeData}}</span>
<span class="data">ppm</span>
<div class="bottom clearfix">
</div>
</div>
</el-card>
</div>
</el-col>
<el-col :span="6">
<div class="grid-content bg-purple">
<el-card :body-style="{ padding: '0px' }">
<img src="../assets/xiugaihou/wine.jpg" class="image">
<div style="padding: 14px;">
<span class="weather-data">當前酒精濃度:</span>
<span class="data">{{alcoholData}}</span>
<span class="data">%</span>
<div class="bottom clearfix">
</div>
</div>
</el-card>
</div>
</el-col>
</el-row>
</div>
<el-divider><i class="el-icon-s-promotion"></i></el-divider>
<div id="data-table">
<span>現在是北京時間: {{ currentDate }}</span>
<el-divider></el-divider>
<span>當前車上共有: {{ peopleNum }}名乘客</span>
<el-divider></el-divider>
<span>您的駕駛時長:{{ driveTime }}</span>
</div>
</div>
</template>
<script>
export default {
name: "Car",
data() {
return {
currentDate: new Date(),
peopleNum: 0,
driveTime: 0,
weatherData: 0,
moistData: 0,
smokeData: 0,
alcoholData: 0,
websocket: null
};
},
methods: {
initWebSocket() {
if(typeof (WebSocket) == 'undefined'){
alert('不支持websocket')
return false
}
const wsuri = 'ws://localhost:8080/websocket/carData'
this.websocket = new WebSocket(wsuri)
this.websocket.onopen = this.websocketonopen
this.websocket.onmessage = this.websocketonmessage
this.websocket.onerror = this.websocketonerror
this.websocket.onclose = this.websocketclose
},
//連接成功
websocketonopen(){
console.log('WebSocket連接成功')
},
//接收數據
websocketonmessage(e) {
console.log(e.data)
var obj = JSON.parse(e.data)
//轉換後的對象:
//{weatherData: 26, moistData: 0.0001, smokeData: 2.344444, alcoholData: 0.56}
console.log(obj)
//賦值
this.weatherData = obj.weatherData;
this.moistData = obj.moistData
this.smokeData = obj.smokeData
this.alcoholData = obj.alcoholData
},
//連接建立失敗重連
websocketonerror(e){
console.log(`連接失敗的信息:`, e)
this.initWebSocket() // 連接失敗後嘗試重新連接
},
//關閉連接
websocketclose(e){
console.log('斷開連接',e)
},
getCarData() {
}
},
created() {
this.initWebSocket()
},
destroyed() {
this.websocket.close()
}
}
</script>
<style scoped>
/*佈局屬性*/
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.el-col {
border-radius: 4px;
}
.bg-purple-dark {
background: #99a9bf;
}
.bg-purple {
background: #d3dce6;
}
.bg-purple-light {
background: #e5e9f2;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
}
.row-bg {
padding: 10px 0;
background-color: #f9fafc;
}
/*卡片屬性*/
#data-card {
padding-bottom: 35px;
}
.weather-data {
font-size: 13px;
color: #999;
}
.bottom {
margin-top: 13px;
line-height: 12px;
}
.button {
padding: 0;
float: right;
}
.image {
width: 100%;
height: 100%;
/*display: block;*/
object-fit: fill;
}
.weather-pic {
/*padding-top: 10px;*/
}
.rainy-pic {
margin-bottom: 10px;
}
.rainy-image{
width: 100%;
height: 100%;
/*display: block;*/
object-fit: fill;
/*padding-bottom: 5px;*/
}
.data {
font-size: 26px;
color: #7FA0A7;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
</style>