目 录CONTENT

文章目录

使用长轮询解决某些场景的实时消息推送需求

成培培
2024-12-10 / 0 评论 / 0 点赞 / 14 阅读 / 0 字

需求来源

最近做一个需求实现在移动端通过按钮,远程控制大屏幕上展示的资源进行实时切换,可以展示一个大屏页面,可以展示一段视频,也可以展示一张图片。

解决思路

大屏幕上打开一个游览器,访问指定动态资源展示页面,接收需要展示的资源就行了,如果是页面就使用iframe嵌套指定页面,如果是视频就用video播放视频即可,唯一可能麻烦一点的就是如何实现展示资源的主动推送到前端,一般会想到这样几种方案:

短轮询

首先最简单的就是前端进行短轮询,每隔一段时间请求获取一次当前大屏幕需要展示的资源,如果跟当前展示的不一致就更新展示内容即可。但是这样就会有一个操作延迟的问题,比如规定了前端每隔10秒获取一次最新的数据,那么每次操作切换时,可能会有最多10秒的一个切换延迟,体验并不是很好,如果将时间间隔缩短,比如1秒请求一次,那样可能又会给服务器造成不必要的压力,因为切换操作毕竟不会经常发生,如果这样的动态资源展示大屏幕比较多,就更不建议这样做短轮询了。

websocket

使用websocket的话当然也可以,如果基于STOMP协议去集成代码也不会特别麻烦,具体可以参考这篇博文:https://www.chengpei.top/archives/springboot-websocket-stomp

但是为了这么一个小需求去给项目集成websocket,我觉得有点杀鸡用牛刀了,毕竟维护的老项目非必要还是不要引入太多乱七八糟的依赖,最小改动解决问题最好。

长轮询

这里我最终采用的是长轮询去实现,既可以满足操作的实时性,实现起来也非常简单。使用到了spring-web依赖中的一个DeferredResult类,它封装了Servlet3.0提供了基于servlet的异步处理api,可以方便的实现http请求的挂起及异步的返回响应。

实现代码

前端代码

前端比较简单就一个单页面,根据查询到的不同资源资源类型,使用不同的dev展示资源即可

<template>
  <div class='screenPanel'>
    <div class="iframeBox" v-if="screenConfig.displayType == 'page'">
      <iframe :src="screenConfig.displayUrl" frameborder="no"></iframe>
    </div>
    <div class="videoBox" v-else-if="screenConfig.displayType == 'mp4'">
      <video :src="screenConfig.displayUrl" controls loop muted autoplay></video>
    </div>
    <div class="emptyBox" v-else>
      <img src="@/assets/img/empty.png" alt="" srcset="">
      <p>暂未配置资源,请配置完成后刷新页面</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import {onMounted, reactive} from "vue";
import {httpClient} from "@/api/http";

let screenConfig = reactive({
  displayType: "",
  displayUrl: "",
});

let immediate = 1;
const updateScreen = async () => {
  let res = await httpClient.get('/dynamicScreenConfig/getScreenConfig/screen?immediate=' + immediate);
  if (res.code == 200) {
    screenConfig.displayType = res.data.displayType;
    screenConfig.displayUrl = res.data.displayUrl;
    immediate = 0;
    await updateScreen();
  } else {
    immediate = 1;
    screenConfig.displayType = "";
  }
}

onMounted(() => {
  updateScreen();
})

</script>

这是使用的vue,贴出了部分代码,主要是页面加载完成后递归调用updateScreen()方法,其中的接口是一个长轮询接口,当参数immediate为1时接口会查询数据库立即返回,如果参数immediate传0,则接口会挂起最多30秒,等待服务器主动返回

后端代码

@Service
public class DynamicScreenConfigServiceImpl implements IDynamicScreenConfigService {

	private final Map<String, DeferredResult<R<DynamicScreenConfig>>> deferredResultCache = new ConcurrentHashMap<>();

	@Override
	public boolean enable(String screenCode, String configId) {
		newDynamicScreenConfig = xxxx; // 这里是查询数据库启用更新指定的展示资源,获取最新的展示资源
		if (newDynamicScreenConfig != null) {
			DeferredResult<R<DynamicScreenConfig>> deferredResult = deferredResultCache.get(screenCode);
			if (deferredResult != null && !deferredResult.isSetOrExpired()) {
				deferredResult.setResult(R.data(newDynamicScreenConfig));
			}
		}
		return true;
	}

	@Override
	public DeferredResult<R<DynamicScreenConfig>> getScreenConfig(String screenCode, Integer immediate) {
		DeferredResult<R<DynamicScreenConfig>> deferredResult = deferredResultCache.get(screenCode);
		if (deferredResult == null || deferredResult.isSetOrExpired()) {
            // 如果当前屏幕对应的DeferredResult为空则创建一个放入内存
			deferredResult = new DeferredResult<>(30000L);
			deferredResultCache.put(screenCode, deferredResult);
			DeferredResult<R<DynamicScreenConfig>> finalDeferredResult = deferredResult;
            // 超时则查询数据库中最新的
			deferredResult.onTimeout(() -> finalDeferredResult.setResult(R.data(getDbScreenConfig(screenCode))));
		}
		if (immediate != null && immediate == 1) {
			// 立刻返回
			deferredResult.setResult(R.data(getDbScreenConfig(screenCode)));
		}
		return deferredResult;
	}

	private DynamicScreenConfig getDbScreenConfig(String screenCode) {
		// 查询数据库中指定大屏需要展示的最新资源配置
        screenConfig = xxxxx;
		return screenConfig;
	}
}

以上是部分代码,分别对应两个接口,一个更新指定屏幕展示资源的接口,一个获取指定屏幕最新展示资源的接口,第二个接口挂起时,可以通过第一个接口的调用立即返回,实现了类似消息实时推送的效果。

这里有个问题就是,如果服务时多节点部署的,那么从当前服务的内存里找DeferredResult可能会找不到,这时可能需要使用消息总线之类的能力将消息播散到集群的每一个节点,各自都在内存中找DeferredResult,找到则set返回

0

评论区