因为项目需求,后台与前端需要保持长期的连接,后台可以主动地向前端发送数据。因为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>