起因
公司系统每次更新代码并部署会影响到业务人员使用系统,甚至会造成数据丢失,因此想在发版本前在页面上通知到每个使用者,便着手做个推送消息的功能。
环境
springboot+vue+nginx+springsecurity
springboot相关代码
pom
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.3.18</version>
</dependency>
// 注意大部分网上的文章都是使用的以下依赖,我使用的话会启动报错,就改为上面的依赖了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
springsecurity放开websocket限制
.authorizeRequests()
.antMatchers("/websocket/*").permitAll()
WebSocketConfig 配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket类,用于连接、接收、发送消息
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocket {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 用户ID
*/
private Integer userId;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
// 注:底下WebSocket是当前类名
private static CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();
// 用来存在线连接用户信息
private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<Integer, Session>();
/**
* 链接成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value="userId") Integer userId) {
try {
this.session = session;
this.userId = userId;
if (userId != 0 && !sessionPool.containsKey(userId)){
webSockets.add(this);
sessionPool.put(userId, session);
}
log.info("【websocket消息】有新的连接,总数为:"+webSockets.size());
} catch (Exception e) {
}
}
/**
* 链接关闭调用的方法
*/
@OnClose
public void onClose() {
try {
webSockets.remove(this);
sessionPool.remove(this.userId);
log.info("【websocket消息】连接断开,总数为:"+webSockets.size());
} catch (Exception e) {
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message
*/
@OnMessage
public void onMessage(@PathParam(value="userId") Integer userId,String message) {
log.info("【websocket消息】收到客户端消息:"+message);
}
/** 发送错误时的处理
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误,原因:"+error.getMessage());
error.printStackTrace();
}
// 此为广播消息
public void sendAllMessage(String message) {
log.info("【websocket消息】广播消息:"+message);
for(WebSocket webSocket : webSockets) {
try {
if(webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息
public void sendOneMessage(Integer userId, String message) {
Session session = sessionPool.get(userId);
if (session != null&&session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:"+message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 此为单点消息(多人)
public void sendMoreMessage(Integer[] userIds, String message) {
for(Integer userId:userIds) {
Session session = sessionPool.get(userId);
if (session != null&&session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:"+message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
接下来你可以单独根据业务定义一个接口接收前端的要传的消息,目标接收人等,然后调用websocket去发消息。为什么要单独定义一个接口不直接使用websocket通讯,因为可能有些逻辑要处理,这样更方便。
@RestController
@RequestMapping("/msg")
public class sendMsgController {
@Resource
private MsgService msgService;
@RequestMapping(value = "/sendWsMsg", method = RequestMethod.POST)
@ApiOperation("sendWsMsg")
@PreAuthenticated
public CommonResp<Void> sendWsMsg(@RequestBody SendWsMsgParam sendWsMsgParam){
msgService.sendWsMsg(sendWsMsgParam);
return new CommonResp<>();
}
}
@Data
@ApiModel("ws消息内容")
public class SendWsMsgParam {
/**
* 消息类型
*/
@ApiModelProperty(value = "消息类型", example = "1:全局消息 2:专人消息", required = true)
private Integer type;
/**
* 接收人id
*/
@ApiModelProperty(value = "接收人", example = "[12,13]")
private List<Integer> receivePerson = new ArrayList<>();
/**
* 内容
*/
@ApiModelProperty(value = "内容", example = "警告")
private String content;
}
@Service
@Slf4j
public class LoanFlowService {
@Resource
private WebSocket webSocket;
public void sendWsMsg(SendWsMsgParam sendWsMsgParam) {
if (sendWsMsgParam.getType() == 1){
webSocket.sendAllMessage("【" + SecurityFrameworkUtils.getLoginUser().getNickname() + "】:" + sendWsMsgParam.getContent());
}else {
for (int i = 0; i < sendWsMsgParam.getReceivePerson().size(); i++) {
webSocket.sendOneMessage(sendWsMsgParam.getReceivePerson().get(i),"【" + SecurityFrameworkUtils.getLoginUser().getNickname() + "】:" + sendWsMsgParam.getContent());
}
}
}
}
vue相关代码
我这里定义了一个悬浮按钮组件,点击按钮弹出编辑框,可以编辑发送类型,接收人,内容。
悬浮按钮组件
<template>
<div>
<el-tooltip class="item" effect="dark" content="发送消息" placement="top">
<el-button
type="info"
circle
icon="el-icon-message"
style="position: fixed; left:95%; top:15%"
@click="editWsMsgVisible"
/>
</el-tooltip>
<editWsMsg v-if="editVisible" :visible.sync="editVisible" :user-list="userList" @closeIt="closeIt" />
</div>
</template>
<script>
import editWsMsg from '@/components/Websicket/wizard/editWsMsg'
import { listSimpleUsers } from '@/api/system/user'
export default {
name: 'Websocket1',
components: {
editWsMsg
},
data() {
return {
editVisible: false,
userList: []
}
},
created() {
this.initWebSocket()
this.listSimpleUsers()
},
methods: {
initWebSocket() {
// 初始化weosocket
var host = window.location.origin
// userId 为websocket消息的唯一标识
console.log(host)
// 有nginx 可以这么写,其次需要配置nginx
/* location ~/websocket/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 36000s; #10小时未传输数据则关闭连接
}
*/
// 如果是没有nginx的情况下,就直接使用原本服务的地址+原本服务的端口号即可,比如本机的时候,就应该是ws://localhost:8080/websocket/this.userId
var wsHost = host.startsWith('https') ? host.replace('https', 'wss') : host.replace('http', 'ws')
this.websock = new WebSocket(wsHost + '/websocket/' + this.userId)
this.websock.onmessage = this.websocketonmessage
this.websock.onerror = this.websocketonerror
this.websock.onopen = this.websocketonopen
this.websock.onclose = this.websocketclose
},
websocketonopen() {
// 连接建立之后执行send方法发送数据
console.log('WebSocket连接成功')
},
websocketonerror() {
console.log('WebSocket连接失败')
},
websocketonmessage(e) {
console.log(e.data)
// 数据接收
var message = e.data
console.log(message)
// 弹窗出来
this.$notify.success({
title: '消息提示',
message: message,
offset: 300,
duration: 0
})
this.value = this.value + 1
},
websocketsend(Data) {
// 数据发送
this.websock.send(Data)
},
websocketclose(e) {
// 关闭
console.log('已关闭连接', e)
},
editWsMsgVisible() {
this.editVisible = true
},
closeIt() {
this.editVisible = false
},
listSimpleUsers() {
listSimpleUsers().then(res => {
if (res.code === 0) {
this.userList = res.data
}
}).finally(() => {
this.$store.dispatch('app/setLoading', false)
})
}
}
}
</script>
<style lang="scss" scoped>
.s-bg-color {
background-color: rgba(83,83,83, $alpha: 0.08);
}
.s-border-color-lighter {
border-color: #e7e9ec;
}
.s-text_color_normal {
color: #6c6c6c
}
.g-create-layout-main {
// min-height: calc(100vh-50px);
background-color: #ededf4;
position: absolute;
top: 84px;
bottom: 0;
left: 0;
right: 0;
margin: 0;
padding: 0;
}
.g-create-layout-header {
height: 60px;
width: 100%;
line-height: 60px;
border-bottom-style: solid;
border-bottom-width: 1px;
position: relative;
background-color: #ffffff;
& .back {
cursor: pointer;
display: inline-block;
padding-left: 20px;
line-height: 12px;
border-right-style: solid;
border-right-width: 1px;
&:hover {
color: $--color-primary;
}
& > i {
color: $--color-primary;
padding-right: 4px;
}
& > span {
padding-right: 20px;
}
}
& .title {
text-align: center;
display: inline-block;
padding: 0 20px;
font-size: 1.4rem;
}
}
.g-create-layout-tabs {
display: inline-block;
}
.g-create-layout-content {
max-height: calc(100vh - 84px);
overflow: auto;
}
.g-content-max-height {
max-height: calc(100vh - 84px - 74px);
}
.g-create-layout-top-right {
float: right;
line-height: 1.2rem;
position: relative;
top: 50%;
transform: translateY(-50%);
}
.g-create-layout-footer {
position: absolute;
bottom: 0;
height: 70px;
width: 100%;
margin: 0;
padding: 20px;
z-index: 998;
text-align: right;
box-shadow: 0 0 10px 10px rgba($color: #000000, $alpha: 0.05);
background-color: #ffffff;
}
</style>
消息编辑框
<template>
<el-dialog
title="新建消息"
v-bind="$attrs"
width="700px"
:close-on-click-modal="false"
:close-on-press-escape="false"
v-on="$listeners"
>
<el-form
ref="form"
:model="form"
:rules="ruleList"
label-width="180px"
label-position="left"
:status-icon="true"
:inline-message="true"
>
<el-form-item label="消息类型" prop="auditType">
<el-select v-model="form.type" clearable placeholder="如退回/终止则请选择">
<el-option label="全局消息" :value="1" />
<el-option label="专人消息" :value="2" />
</el-select>
</el-form-item>
<el-form-item v-if="form.type === 2" label="接收人" prop="receivePerson">
<el-select v-model="form.receivePerson" multiple filterable>
<el-option v-for="item in userList" :key="item.id" :label="item.nickname" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input v-model="form.content" type="textarea" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button v-if="isEdit" type="primary" @click="conform">确 定</el-button>
</div>
</el-dialog>
</template>
<script>
import { sendWsMsg } from '@/api/loan'
export default {
name: 'EditWsMsg',
props: {
id: {
type: Number,
default: null
},
userList: {
type: Array,
default: () => {
return []
}
},
isEdit: {
type: Boolean,
default: true
}
},
data() {
return {
form: {
type: 1,
receivePerson: [],
content: ''
},
ruleList: {
content:
{ required: true, message: '不填内容你想让我猜吗?', trigger: ['blur', 'change'] }
}
}
},
methods: {
conform() {
this.$refs.form.validate((valid, object) => {
if (valid) {
console.log(this.form)
sendWsMsg(this.form)
// this.$emit('closeIt', false) // 发送后要手动关闭
}
})
},
cancel() {
this.$emit('closeIt', false) // 发送后要手动关闭
}
}
}
</script>
nginx配置
因为一般情况下,前端访问的地址会根据nginx转发到真正的服务地址,因此在nginx中必须要配置转发websocket的逻辑。比如假设我这里本来访问的是ws://123.1.0.25:9071/,要转发到http://127.0.0.1:8080(当然如果是没有nginx的情况下,就直接使用原本服务的地址+原本服务的端口号即可,比如本机的时候,就应该是ws://localhost:8080/websocket/this.userId,注意这个8080是本身的服务端口)
location ~/websocket/ {
proxy_pass http://127.0.0.1:8080;//你自己的转发目标地址
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 36000s; #10小时未传输数据则关闭连接
}
然后点击按钮发送消息就可以了,屏幕边上就会弹出消息框。