学习记录680@springboot+vue+nginx+springsecurity环境下的websocket实战

起因

公司系统每次更新代码并部署会影响到业务人员使用系统,甚至会造成数据丢失,因此想在发版本前在页面上通知到每个使用者,便着手做个推送消息的功能。

环境

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小时未传输数据则关闭连接
  }

然后点击按钮发送消息就可以了,屏幕边上就会弹出消息框。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值