明天的明天 永远的永远 未知的一切 我与你一起承担 ??

是非成败转头空 青山依旧在 几度夕阳红 。。。
  博客园  :: 首页  :: 管理

详解java WebSocket的实现以及Spring WebSocket

Posted on 2024-10-30 12:02  且行且思  阅读(635)  评论(0编辑  收藏  举报

第一步:配置Spring

如果你跟我一样采用的Maven,那么只需要将下面的依赖,加入到pom.xml文件就可以了:
<!--spring websocket库-->
 <dependency>
          <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.context.version}</version>
 </dependency>

 

第二步:配置WebSocket

使用Configurer类和 Annotation来进行WebSocket配置。
首先要创建一个类,继承WebSocketMessageBrokerConfigurer,并且在类上加上annotation:@Configuration和@EnableWebSocketMessageBroker。这样,Spring就会将这个类当做配置类,并且打开WebSocket。

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //添加这个Endpoint,这样在网页中就可以通过websocket连接上服务了
        registry.addEndpoint("/ws").withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        System.out.println("服务器启动成功");
        //这里设置的simple broker是指可以订阅的地址,也就是服务器可以发送的地址
        /**        
         * user 用于用户聊天
         */
        config.enableSimpleBroker("/topic","/user");
        config.setApplicationDestinationPrefixes("/app");
    }
}

 第一个方法,是registerStompEndpoints,大意就是注册消息连接点(我自己的理解),所以我们进行了连接点的注册:

registry.addEndpoint("/ws").withSockJS();

我们加了一个叫coordination的连接点,在网页上我们就可以通过这个链接来和服务器的WebSocket连接了。但是后面还有一句withSockJs,这是什么呢?SockJs是一个WebSocket的通信js库,Spring对这个js库进行了后台的自动支持,也就是说,我们如果使用SockJs,那么我们就不需要对后台进行更多的配置,只需要加上这一句就可以了。

第二个方法,configureMessageBroker,大意是设置消息代理,也就是页面上用js来订阅的地址,也是我们服务器往WebSocket端接收js端发送消息的地址。

config.enableSimpleBroker("/user");
config.setApplicationDestinationPrefixes("/app");

首先,定义了一个连接点叫user,从名字可以看的出,最后我会做一个聊天的例子。然后,设置了一个应用程序访问地址的前缀,目的估计是为了和其他的普通请求区分开吧。也就是说,网页上要发送消息到服务器上的地址是/app/user。

 

后端实现类:

 

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;



@Api(tags = "WebSocket控制器") @ApiSupport(author = "abc", order = 10) @RestController public class WebSocketController { @Resource private SimpMessagingTemplate template; @Resource private AuthService authService; @Resource private CommonCacheOperator commonCacheOperator; public static final String WS_CACHE_ALL_KEY = "ws:"; /** * 发送信息 * * @author abc * @date 2024年10月10日10:36:00 **/ @ApiOperationSupport(order = 1) @ApiOperation("发送信息") @GetMapping("/app/send") public CommonResult<String> send(@ApiParam(value="用户Id", required = true) @RequestParam String userId, @ApiParam(value="发送消息message:(0='取消确认,1='扫码成功,2=小程序确认登录')", required = true) @RequestParam String message) { String qrCodeLink =CommonCryptogramUtil.doSm4CbcDecrypt(userId); Object obj=commonCacheOperator.get(WS_CACHE_ALL_KEY+userId); if(ObjectUtil.isEmpty(obj)) { return CommonResult.error("二维码已失效"); } if(message.trim().equals("0")) { template.convertAndSendToUser(userId, "/token", CommonResult.data(false)); } else if(message.trim().equals("1")) { template.convertAndSendToUser(userId, "/token", CommonResult.data(true)); } else if(message.trim().equals("2")) { String token= authService.doLoginById(StpLoginUserUtil.getLoginUser().getId(), AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.B.getValue()); template.convertAndSendToUser(userId, "/token", CommonResult.data(token)); commonCacheOperator.remove(WS_CACHE_ALL_KEY+userId); } return CommonResult.ok(); } /** * 订阅信息 * * @author abc * @date 2024年10月10日10:36:12 **/ @ApiOperationSupport(order = 2) @ApiOperation("订阅信息") @GetMapping("/user/{userId}/token") public CommonResult<String> user(@PathVariable String userId) { return CommonResult.ok(); } /** * 获取二维码 * * @author abc * @date 2024年10月10日10:36:12 **/ @ApiOperationSupport(order = 3) @ApiOperation("获取二维码") @CommonNoRepeat @GetMapping("/app/getQRCode") public CommonResult<Map<String,String>> getQRCode(@ApiParam(value="时间戳", required = true) @RequestParam String tiem) { Map<String,String> map=new HashMap<String, String>(); String randomString = RandomUtil.randomString(11); String qrCodeLink =CommonCryptogramUtil.doSm4CbcEncrypt(randomString); map.put("str", qrCodeLink); // 二维码宽度 int qrCodeWidth = 190; // 二维码高度 int qrCodeHeight = 190; try { commonCacheOperator.put(WS_CACHE_ALL_KEY+qrCodeLink, qrCodeLink, 5 * 60); BufferedImage qrCodeImage = QRCodeUtil.getQrCodeImage(qrCodeLink, qrCodeWidth, qrCodeHeight); String base64QrCode = QRCodeUtil.convertImageToBase64(qrCodeImage); map.put("img", base64QrCode); } catch (WriterException e) { e.printStackTrace(); } return CommonResult.data(map); } @MessageMapping("/send") // 映射客户端发送的消息 public void greeting(String message) throws Exception { System.out.println(message); //template.convertAndSendToUser("1", "/token", message); commonCacheOperator.put(WS_CACHE_ALL_KEY+message, message, 5 * 60); } }

template.convertAndSendToUser(username, destination, message)  API。
它接受一个字符串用户名(客户端即前端创建随机码UUID),这意味着如果我们以某种方式为每个连接设置唯一的用户名,我们应该能够发送消息给订阅主题的特定用户。

 

第三步:配置Web端

首先我们要使用两个js库,一个是之前说过的SockJs,一个是stomp,这是一种通信协议,暂时不介绍它,只需要知道是一种更方便更安全的发送消息的库就行了。

需要连接服务端的WebSocket:

var socket = new SockJS('/ws');
var stompClient = Stomp.over(socket);
stompClient.connect('', '', function (frame) {});

没错,就只需要两句话。有了这三句话,我们就已经可以连接上了服务器。

 

使用SockJs还有一个好处,那就是对浏览器进行兼容,如果是IE11以下等对WebSocket支持不好的浏览器,SockJs会自动的将WebSocket降级到轮询(这个不知道的可以去百度一下),之前也说了,Spring对SockJs也进行了支持,也就是说,如果之前加了withSockJs那句代码,那么服务器也会自动的降级为轮询。(怎么样,是不是很兴奋,Spring这个特性太让人舒服了)

Vue3 源码:

<template>
  <div class="loginmain">
    <div class="loginbox relative">
      <div
        class="absolute top-13 right-2 w-15 h-15 bg-cyan-100 QRCode"
        v-if="isShowQRCode"
        @click="handleSwitch"
      ></div>
      <div
        class="absolute top-13 right-2 w-15 h-15 bg-cyan-100 password"
        v-else
        @click="handleSwitch"
      ></div>
      <span class="logo"><img src="@/assets/images/logo.png" style="height: 35px" /></span>
      <h2 style="margin-bottom: 0"><b>平台</b> </h2>
      <LoginForm v-if="isShowQRCode" />
      <div class="QRCodeCon" v-else>
        <div>
          <div
            class="w-full h-65.8 bg-light-50 flex justify-center items-center"
            v-if="showStatus == 1"
          >
            <!-- <img src="/static/images/组5196.png" class="w-50 h-50" /> -->
            <QrCode
              :value="qrCodeUrl"
              class="enter-x flex justify-center xl:justify-start"
              :width="240"
            />
            <div
              class="w-full h-50% z-100 bg-light-50/50 absolute flex justify-center items-center"
              v-if="timeoutStatus"
            >
              <RedoOutlined
                style="color: #000; font-size: 50px; font-weight: 1000; cursor: pointer"
                @click="handleRefresh"
              />
            </div>
          </div>
          <div
            class="w-full h-65.8 bg-light-50 flex flex-col items-center justify-center"
            v-if="showStatus == 2"
          >
            <div class="mb-2">
              <CheckCircleOutlined :style="{ fontSize: '50px', color: '#1890ff' }" />
            </div>
            <div class="text-[22px] mb-1">扫描成功!</div>
            <div class="text-[#9d9da7] text-[16px]">请在手机上根据提示确认登录!</div>
          </div>
          <div
            class="w-full h-65.8 bg-light-50 flex flex-col items-center justify-center"
            v-if="showStatus == 3"
          >
            <div class="mb-2">
              <CheckCircleOutlined :style="{ fontSize: '50px', color: '#9eba20' }" />
            </div>
            <div class="text-[22px] mb-1">登录成功!</div>
          </div>
          <div
            class="w-full h-65.8 bg-light-50 flex flex-col items-center justify-center"
            v-if="showStatus == 4"
          >
            <div class="mb-2">
              <CloseCircleOutlined :style="{ fontSize: '50px', color: '#f80000' }" />
            </div>
            <div class="text-[22px] mb-1">手机已拒绝登录!</div>
          </div>
        </div>
      </div>
    </div>
    <div style="clear: both"></div>
    <div class="bottom" style="position: fixed; bottom: 0; width: 100%; height: 80px">
      <div class="main-bottom-wrapper">
        <a
          style="color: rgb(160 160 160); text-decoration: none"
          href="https://beian.miit.gov.cn"
          target="_blank"
        >
          XXXXXXXXXXXXXXXXXXXXX</a
        >
        <br />
        <img src="/static/images/gtimg.png" class="home-icon" />
        <a
          style="color: rgb(160 160 160); text-decoration: none"
          href="https://beian.mps.gov.cn/#/query/webSearch?code=0000"
          rel="noreferrer"
          target="_blank"
          >XXXXXXXXXXXXXXXXX</a
        >
      </div>
    </div>
  </div>
  <!-- <div class="bottom" style="height: 60px; text-align: center">
    <div class="container">
      <a
        href="https://beian.miit.gov.cn/"
        target="_blank"
        style="text-decoration: none; color: #49e"
        >xxxxxx</a
      >
    </div>
  </div> -->
</template>
<script lang="ts" setup>
  import { ref, onMounted, onUnmounted, computed } from 'vue';
  import { AppLogo, AppLocalePicker, AppDarkModeToggle } from '@/components/Application';
  import LoginForm from './LoginForm.vue';
  import ForgetPasswordForm from './ForgetPasswordForm.vue';
  import RegisterForm from './RegisterForm.vue';
  import MobileForm from './MobileForm.vue';
  import QrCodeForm from './QrCodeForm.vue';
  import { useGlobSetting } from '@/hooks/setting';
  import { useI18n } from '@/hooks/web/useI18n';
  import { useDesign } from '@/hooks/web/useDesign';
  import { useLocaleStore } from '@/store/modules/locale';
  import { CheckCircleOutlined, CloseCircleOutlined, RedoOutlined } from '@ant-design/icons-vue';
  import moment from 'moment';
  import { QrCode } from '@/components/Qrcode';

  import SockJS from 'sockjs-client/dist/sockjs.min.js';
  import Stomp from 'stompjs';

  import { useUserStore } from '@/store/modules/user';
  import { router } from '@/router';

  const userStore = useUserStore();

  defineProps({
    sessionTimeout: {
      type: Boolean,
    },
  });

  const globSetting = useGlobSetting();
  const { prefixCls } = useDesign('login');
  const { t } = useI18n();
  const localeStore = useLocaleStore();
  const showLocale = localeStore.getShowPicker;
  const title = computed(() => globSetting?.title ?? '');

  const isShowQRCode = ref(true);
  const handleSwitch = () => {
    isShowQRCode.value = !isShowQRCode.value;
    if (!isShowQRCode.value) {
      handleShowCode();
      countdown(countdownNum.value);
    }
    showStatus.value = 1;
  };
  const qrCodeUrl = ref('');
  const handleShowCode = () => {
    const generateRandomString = () => {
      const characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
      let randomString = '';
      for (let i = 0; i < 10; i++) {
        const randomIndex = Math.floor(Math.random() * characters.length);
        randomString += characters.charAt(randomIndex);
      }
      return randomString;
    };
    qrCodeUrl.value = generateRandomString();
    webSocketJS();
  };
  const stompClient = ref(null);
  const showStatus = ref(1);
  const webSocketJS = () => {
    let socket = new SockJS('/api/ws');
    stompClient.value = Stomp.over(socket);
    stompClient.value.heartbeat.outgoing = 10000;
    stompClient.value.heartbeat.incoming = 0;
    //去掉debug打印
    // stompClient.value.debug = null;
    //开始连接
    stompClient.value.connect(
      {},
      (frame) => {
        console.log('Connected:' + frame);
        console.info('[WebSocket] 连接请求发送成功!');
        //进行订阅服务
        //***连接
        stompClient.value.subscribe(`/user/${qrCodeUrl.value}/token`, (message) => {
          const { body } = message;
          if (body) {
            const _body = JSON.parse(body);
            const { data } = _body;
            if (data.toString() == 'true') {
              showStatus.value = 2;
            }
            if (data.toString().length > 10) {
              showStatus.value = 3;
              userStore.setToken(data);
              router.push('/index/index');
            }
          }
        });
        stompClient.value.send('/app/send', {}, qrCodeUrl.value, (res) => {
          console.log(res);
        });
      },
      () => {
        // 断开连接
        console.log('连接请求发送失败');
        //一连接请求发送失败就关闭
        // this.buttonStatus = true;
      },
    );
  };
  const timeoutStatus = ref(false);
  const intervalId = ref(null);
  const countdown = (durationInSeconds) => {
    let remainingTime = durationInSeconds;
    intervalId.value = setInterval(() => {
      const minutes = Math.floor(remainingTime / 60);
      const seconds = remainingTime % 60;

      console.log(`${minutes}:${seconds < 10 ? '0' : ''}${seconds}`);

      if (remainingTime === 0) {
        clearInterval(intervalId.value);
        timeoutStatus.value = true;
      } else {
        remainingTime--;
      }
    }, 1000);
  };
  const handleRefresh = () => {
    timeoutStatus.value = false;
    countdown(countdownNum.value);
    handleShowCode();
    clearInterval(intervalId.value);
  };
  const countdownNum = ref(60);
  onUnmounted(() => {
    stompClient.value.disconnect();
    clearInterval(intervalId.value);
  });
</script>
<style lang="less">
  @import 'login';
</style>
<style lang="less" scoped>
  .main-bottom-wrapper {
    bottom: 0;
    width: 370px;
    height: 40px;
    margin: 0 auto;
    margin-top: 15px;
    padding-bottom: 15px;
    text-align: center;
  }

  .home-icon {
    display: inline-block;
    width: 20px;
    height: 20px;
    vertical-align: text-bottom;
  }

  .QRCode {
    background: url('/static/images/组5196.png') no-repeat center center;
    background-size: 100% 100%;
    cursor: pointer;
  }

  .password {
    background: url('/static/images/组5197.png') no-repeat center center;
    background-size: 100% 100%;
    cursor: pointer;
  }

  .loginbox {
    border-radius: 0;
  }
</style>