百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

连接远程云应用时鼠标卡顿,性能优化问题

ztj100 2025-09-12 06:13 4 浏览 0 评论

guacamole远程桌面服务器应用。企业中遇到一个性能问题。就是连接远程桌面应用之后,鼠标会出现重影,卡顿问题。

<template>
  <a-drawer
    ref="cloudAppRef"
    v-model:open="open"
    :width="drawerWidth"
    :mask-style="{ opacity: 0 }"
    :body-style="{ padding: 0, overflow: 'hidden' }"
    class="cloud-applications-container"
    title="云应用"
    placement="right"
    @close="onClose"
  >
    <template #extra>
      <ps-svg-icon v-if="isFullScreen" name="FullscreenExitOutlined" :size="16" class="extra-ico" @click="onFullscreen(false)" />
      <ps-svg-icon v-else name="FullscreenOutlined" :size="16" class="extra-ico" @click="onFullscreen(true)" />
    </template>
    <div class="guacamole-wrapper">
      <div ref="displayContainer" class="guacamole-display" tabindex="0" />
      <div ref="mouseCapture" class="mouse-capture" @mousemove.stop @mousedown.stop @mouseup.stop />
    </div>
  </a-drawer>
</template>

<script setup lang="ts">
import PsSvgIcon from '@ps/svg-icon'
import Guacamole from 'guacamole-common-js'
import { computed, getCurrentInstance, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { queryAppTokenApi } from './api'
import { cloudApplicationsEmits, cloudApplicationsProps } from './cloud-applications'
import { setupI18n } from './locale'

defineOptions({
  name: 'PsCloudApplications',
})

const props = defineProps(cloudApplicationsProps)
const emits = defineEmits(cloudApplicationsEmits)

// core
const App: any = getCurrentInstance()?.appContext.app
setupI18n(App)

const drawerWidth = ref('66%')
const cloudAppRef = ref()
const displayContainer = ref()
const mouseCapture = ref()
const isFullScreen = ref(false)
const guacamole = reactive({
  client: null as any,
  tunnel: null as any,
  display: null as any,
  mouse: null as any,
  keyboard: null as any,
  maxRetryCount: 5,
  curRetryCount: 0,
  mouseState: null as any,
  localPointerHidden: false
})

const open = computed({
  get() {
    return props.appVisabled
  },
  set(value) {
    emits('update:appVisabled', value)
  },
})

const token = ref('')
const guacId = ref(1)

// 调整DPI为更合适的值,减少指针渲染负担
const getDpiValue = () => {
  const screenDpi = window.devicePixelRatio || 1;
  return Math.min(Math.max(96, 96 * screenDpi), 300); // 限制在96-300之间
}

// 新增:安全关闭连接的方法
const safeDisconnect = () => {
  // 1. 先断开客户端连接(会自动关闭隧道)
  if (guacamole.client) {
    try {
      guacamole.client.disconnect();
    } catch (e) {
      console.warn('客户端断开连接时出错:', e);
    }
    guacamole.client = null;
  }
  
  // 2. 仅在确认有close方法时才调用tunnel.close()
  if (guacamole.tunnel && typeof guacamole.tunnel.close === 'function') {
    try {
      guacamole.tunnel.close();
    } catch (e) {
      console.warn('隧道关闭时出错:', e);
    }
    guacamole.tunnel = null;
  } else {
    // 对于没有close方法的版本,直接置空引用
    guacamole.tunnel = null;
  }
  
  // 3. 清理输入设备
  if (guacamole.mouse) {
    try {
      guacamole.mouse.unlisten();
    } catch (e) {
      console.warn('鼠标事件清理出错:', e);
    }
    guacamole.mouse = null;
  }
  
  if (guacamole.keyboard) {
    try {
      guacamole.keyboard.unlisten();
    } catch (e) {
      console.warn('键盘事件清理出错:', e);
    }
    guacamole.keyboard = null;
  }
}

const initGuacamole = async () => {
  // 先安全关闭可能存在的旧连接
  safeDisconnect();

  const width = displayContainer.value.clientWidth || 1920
  const height = displayContainer.value.clientHeight || 1080
  const dpi = getDpiValue();
  
  // 精简URL参数,只保留必要的
  guacamole.tunnel = new Guacamole.WebSocketTunnel(
    `wss://guacamole.poissonsoft.com/guacamole/websocket-tunnel?` +
    `token=${token.value}&` +
    `GUAC_DATA_SOURCE=mysql&` +
    `GUAC_ID=${guacId.value}&` +
    `GUAC_TYPE=c&` +
    `GUAC_WIDTH=${width}&` +
    `GUAC_HEIGHT=${height}&` +
    `GUAC_DPI=${dpi}&` +
    `GUAC_TIMEZONE=Asia%2FShanghai`
  )
  
  guacamole.client = new Guacamole.Client(guacamole.tunnel)
  guacamole.client.connect()

  // 在连接成功后初始化输入设备
  guacamole.client.onstatechange = (state: any) => {
    if (state === 3) { // READY状态
      // 显示远程桌面
      guacamole.display = guacamole.client.getDisplay()
      guacamole.display.getElement().style.zIndex = 'auto'
      
      // 设置CSS加速渲染
      const element = guacamole.display.getElement();
      element.style.willChange = 'transform';
      element.style.transform = 'translateZ(0)';
      element.style.backfaceVisibility = 'hidden';
      
      // 清空容器再添加,避免重复
      displayContainer.value.innerHTML = '';
      displayContainer.value.appendChild(element);
      displayContainer.value.addEventListener('click', setFocus);

      // 检查是否加载完毕
      checkDisplayReady(changeSize);

      // 绑定鼠标事件
      initMouseHandling(element);

      // 绑定键盘事件
      initKeyboardHandling();
      
      // 隐藏本地鼠标指针
      hideLocalPointer(true);
    } else if (state === 0) { // 断开连接状态
      hideLocalPointer(false);
    }
  }

  // 错误处理与重连机制
  guacamole.tunnel.onerror = (error: any) => {
    console.error('Tunnel error:', error);
    hideLocalPointer(false);
    
    if ([512, 514, 515, 769, 776].includes(error.code)) {
      if (guacamole.curRetryCount < guacamole.maxRetryCount) {
        setTimeout(initGuacamole, 2000);
        guacamole.curRetryCount++;
      }
    }
  }
}

// 改进鼠标事件处理,减少延迟
const initMouseHandling = (element: HTMLElement) => {
  // 清除之前的鼠标实例
  if (guacamole.mouse) {
    guacamole.mouse.unlisten();
  }
  
  // 使用绝对定位的鼠标捕获层解决指针偏移问题
  guacamole.mouse = new Guacamole.Mouse(mouseCapture.value);
  
  // 使用requestAnimationFrame减少事件频率
  const debounceMouseEvents = (callback: Function) => {
    let isProcessing = false;
    return (mouseState: any) => {
      if (!isProcessing) {
        requestAnimationFrame(() => {
          callback(mouseState);
          isProcessing = false;
        });
        isProcessing = true;
      }
    };
  };
  
  const handleMouse = debounceMouseEvents((mouseState: any) => {
    guacamole.mouseState = mouseState;
    guacamole.client.sendMouseState(mouseState, true);
  });
  
  guacamole.mouse.onmousedown = handleMouse;
  guacamole.mouse.onmouseup = handleMouse;
  guacamole.mouse.onmousemove = handleMouse;
}

// 改进键盘事件处理
const initKeyboardHandling = () => {
  if (guacamole.keyboard) {
    guacamole.keyboard.unlisten();
  }
  
  guacamole.keyboard = new Guacamole.Keyboard(displayContainer.value);
  
  guacamole.keyboard.onkeydown = (keysym: any) => {
    guacamole.client.sendKeyEvent(1, keysym);
  };
  
  guacamole.keyboard.onkeyup = (keysym: any) => {
    guacamole.client.sendKeyEvent(0, keysym);
  };
}

// 控制本地鼠标指针显示/隐藏
const hideLocalPointer = (hide: boolean) => {
  if (displayContainer.value) {
    displayContainer.value.style.cursor = hide ? 'none' : 'auto';
    mouseCapture.value.style.cursor = hide ? 'none' : 'auto';
    guacamole.localPointerHidden = hide;
  }
}

const checkDisplayReady = (callback: any) => {
  const timer = setInterval(() => {
    if (guacamole.client?.getDisplay().getWidth() > 0) {
      clearInterval(timer);
      callback();
    }
  }, 100);
}

const changeSize = () => {
  if (!guacamole.client) return;
  
  const display = guacamole.client.getDisplay();
  const container = displayContainer.value;
  
  if (!display || !container) return;

  const width = container.clientWidth;
  const height = container.clientHeight;
  const displayWidth = display.getWidth();
  const displayHeight = display.getHeight();

  if (displayWidth > 0 && displayHeight > 0) {
    const scale = Math.min(
      width / displayWidth, 
      height / displayHeight
    ).toFixed(4);
    
    display.scale(scale);
    
    if (isFullScreen.value) {
      guacamole.client.sendSize(width, height);
    }
  }
}

const onFullscreen = (isFull: boolean) => {
  isFullScreen.value = isFull;
  drawerWidth.value = isFull ? '100%' : '66%';
  requestAnimationFrame(() => {
    setTimeout(changeSize, 300);
  });
}

const setFocus = () => {
  displayContainer.value?.focus();
}

const onClose = () => {
  // 使用安全关闭方法
  safeDisconnect();
  hideLocalPointer(false);
  open.value = false;
}

// 监听容器尺寸变化,自动调整
const handleResize = () => {
  if (guacamole.client?.getDisplay()) {
    changeSize();
  }
};

onMounted(async () => {
  const res = await queryAppTokenApi('cloudAppToken');
  if (res?.data?.length) {
    token.value = res.data[0].nameEn;
    guacId.value = res.data[0].name || 1;
  }
  
  window.addEventListener('resize', handleResize);
  setTimeout(initGuacamole, 100);
})

onUnmounted(() => {
  displayContainer.value?.removeEventListener('click', setFocus);
  window.removeEventListener('resize', handleResize);
  
  // 组件卸载时安全关闭连接
  safeDisconnect();
});

watch(open, (newVal) => {
  if (newVal && guacamole.client?.getDisplay()) {
    setTimeout(changeSize, 300);
  }
});
</script>

<style scoped lang="scss">
@use 'cloud-applications';

.guacamole-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
}

.guacamole-display {
  width: 100%;
  height: 100%;
  min-height: 400px;
  outline: none;
  transform: translateZ(0);
  will-change: transform;
}

.mouse-capture {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
  pointer-events: auto;
}

.extra-ico {
  margin-left: 8px;
  cursor: pointer;
}
</style>

主要的优化点思路有:
1、通过CSS属性 transform: translateZ(0)、will-change等启用浏览器硬件加速,提升渲染性能。will-change属性会提前预告浏览器优化方向,预创建GP U层,分配显存,避免变化时卡顿;
2、启用RQ(requestAnimationFrame)来优化动画执行;

相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: