利用 WebMKS 和 Java 实现前端访问虚拟机网页

实战干货:手把手教你用 WebMKS 和 Java 搞定 ESXi 虚拟机网页控制台

最近在折腾虚拟机网页控制台,发现用 VMware 原生的 WebMKS SDK 配合 ESXi 的 API 效果最好。这套方案不用装插件,直接在浏览器里就能操作。把中间踩过的坑和实现逻辑整理出来,希望能帮到大家。


1. 准备工作:依赖得放本地

WebMKS 这个 SDK 比较老派,它离不开 jQuery 。为了稳妥,别用 CDN,直接把这几个文件下载下来放到你项目的 public/static/wmks 目录下。

怎么引入?

index.html 里按这个顺序排好,jQuery 必须在最前面,不然 SDK 报错。

HTML

xml 复制代码
<head>
  <link rel="stylesheet" href="/static/wmks/css/wmks-all.css">
  <script src="/static/wmks/jquery.min.js"></script>
  <script src="/static/wmks/jquery-ui.min.js"></script>
  <script src="/static/wmks/wmks.min.js"></script>
</head>

2. 后端:Java 怎么拿 Ticket

控制台连接前得先要个"门票"(Ticket)。后端用 Java 调用官方的 vijava 库,去 ESXi 那里申请一个。

核心逻辑:

  1. 连上 ESXi。
  2. 靠 UUID 找到那台虚拟机。
  3. 申请一个类型叫 webmks 的票据。

Java

java 复制代码
public TicketInfo getVmTicket(String host, String user, String pwd, String vmUuid) throws Exception {
    // 建立连接
    ServiceInstance si = new ServiceInstance(new URL("https://" + host + "/sdk"), user, pwd, true);
    try {
        InventoryNavigator nav = new InventoryNavigator(si.getRootFolder());
        VirtualMachine vm = (VirtualMachine) nav.searchManagedEntity("VirtualMachine", vmUuid);
        
        // 关键:拿 webmks 票据
        VirtualMachineTicket vmt = vm.acquireTicket("webmks");
        
        // 把票据、主机IP、端口发给前端
        return new TicketInfo(vmt.getTicket(), vmt.getHost(), 443);
    } finally {
        si.getServerConnection().logout(); // 记得退出登录,不然会话堆积
    }
}

3. 前端:Vue 3 里的核心控制

初始化连接

在 Vue 里的思路是:拿到票据后,立刻初始化 SDK 并连上 WebSocket。

TypeScript

typescript 复制代码
const startWMKS = (ticket: string) => {
  const WMKS_LIB = window.WMKS;
  
  // 初始化配置
  wmksInstance.value = WMKS_LIB.createWMKS("wmks-container", {
    enableHints: true,
    changeResolutionOnResizing: true, // 窗口变了,分辨率也跟着变
    rescaleOnParentResize: true,      // 自动缩放
    position: 'absolute'
  });

  // 连上 ESXi 的 443 端口
  wmksInstance.value.connect(`wss://${activeVM.value.host}:443/ticket/${ticket}`);
};

全屏怎么变清晰?

全屏时如果直接拉伸,画面会糊。我的办法是:先断开,变全屏,再重连。虽然多等了半秒,但画面会根据全屏后的尺寸重新协商,清晰度瞬间提升。


4. Windows 虚拟机的专项关照

搞 Linux 基本没啥事,但 Windows 挺挑剔,这几点没做到肯定出问题:

  1. 必装 VMware Tools:不装的话,显卡驱动不行,画面传输慢甚至直接黑屏。
  2. 显存给够 :Windows 10 这种系统,显存起码给到 128MB
  3. CAD 按钮 :Windows 登录得按 Ctrl+Alt+Del。网页里按没用,必须给用户做个按钮调用 wmksInstance.sendCAD()

5. 样式:别留白边

为了让黑色背景铺满整个弹窗,CSS 得这么写:

SCSS

css 复制代码
/* 弹窗样式 */
:deep(.vnc-session-dialog) {
  display: flex;
  flex-direction: column;
  background: #000 !important;

  /* 全屏模式强行占满 */
  &.is-fullscreen { 
    height: 100vh !important;
    .el-dialog__body { flex: 1; display: flex; flex-direction: column; }
  }

  /* 核心:让 body 自动撑开 */
  .el-dialog__body {
    flex: 1;
    padding: 0 !important;
    overflow: hidden;
  }
}

总结一下

做这玩意儿逻辑其实不难,关键在于细节:

  • 顺序:jQuery 必须先引。
  • 时机:全屏重连能解决画面糊的问题。
  • 补丁:给 Windows 留个 CAD 按钮,装好 VMware Tools。

只要这几点对齐了,剩下的就是调 UI 让它看着更顺手的事儿了。 最后放上图片和源码

js 复制代码
<template>
  <div class="vnc-app-wrapper">
    <div class="page-header">
      <div class="brand">
        <el-icon :size="28" color="#409eff"><Monitor /></el-icon>
        <h1>VMware 虚拟化资源管理</h1>
      </div>
    </div>

    <div class="vm-grid">
      <div v-for="(vm, index) in vmList" :key="index" class="vm-mini-card">
        <div class="vm-status-bar" :class="vm.status"></div>
        <div class="vm-card-body">
          <div class="vm-icon">
            <Platform v-if="vm.os === 'windows'" />
            <Cpu v-else />
          </div>
          <div class="vm-details">
            <h3 class="vm-name">{{ vm.name }}</h3>
            <p class="vm-ip">{{ vm.host }}</p>
          </div>
          <div class="vm-actions">
            <el-button type="primary" plain size="small" @click="openConsole(vm)">
              连接控制台
            </el-button>
          </div>
        </div>
        <div class="vm-footer">
          <span>UUID: {{ vm.vmUuid.substring(0, 14) }}...</span>
        </div>
      </div>
    </div>

    <el-dialog 
      v-model="vncDialogVisible" 
      :fullscreen="isFullscreen"
      :width="isFullscreen ? '100%' : '1100px'" 
      top="5vh"
      :draggable="true"
      :show-close="false"
      class="vnc-session-dialog"
      :before-close="handleCloseVNC"
    >
      <template #header>
        <div class="session-header drag-area">
          <div class="session-info">
            <div class="status-indicator" :class="connectionClass"></div>
            <span class="session-name">
              {{ activeVM?.name }} | {{ activeVM?.host }}
            </span>
          </div>
          <div class="session-controls">
            <el-button size="small" link @click="sendCAD">CAD</el-button>
            <div class="divider"></div>
            <el-button size="small" link @click="toggleFullscreen">
              <el-icon :size="16">
                <FullScreen v-if="!isFullscreen" />
                <CopyDocument v-else />
              </el-icon>
            </el-button>
            <el-button size="small" link type="danger" @click="handleCloseVNC">
              <el-icon :size="16"><Close /></el-icon>
            </el-button>
          </div>
        </div>
      </template>

      <div 
        class="session-stage" 
        v-loading="reloading" 
        element-loading-background="rgba(0, 0, 0, 0.9)"
        element-loading-text="正在重构画面..."
      >
        <div id="wmks-container"></div>
      </div>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, computed, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import { Monitor, FullScreen, Close, CopyDocument, Platform, Cpu } from '@element-plus/icons-vue'
import { getVmConsoleTicket } from '@/api/taskManagement/vmConsole'


const vmList = ref([
  { 
    name: 'Windows-测试机', 
    host: '', 
    username: '', 
    password: '', 
    vmUuid: '',
    os: 'windows',
    status: 'online'
  },
  { 
    name: 'Linux-生产机', 
    host: '', 
    username: '', 
    password: '', 
    vmUuid: '',
    os: 'linux',
    status: 'online'
  }
])

const activeVM = ref<any>(null)
const vncDialogVisible = ref(false)
const isFullscreen = ref(false)
const reloading = ref(false)
const vncStatus = ref('disconnected')
const wmksInstance = ref<any>(null)

const connectionClass = computed(() => ({
  'is-connecting': vncStatus.value === 'connecting',
  'is-connected': vncStatus.value === 'connected'
}))


const openConsole = (vm: any) => {
  activeVM.value = vm;
  handleConnect(false);
}

const toggleFullscreen = async () => {
  reloading.value = true;
  if (wmksInstance.value) {
    wmksInstance.value.disconnect();
    wmksInstance.value.destroy();
    wmksInstance.value = null;
  }
  isFullscreen.value = !isFullscreen.value;
  await nextTick();
  setTimeout(async () => {
    try { await handleConnect(true); } finally { reloading.value = false; }
  }, 500);
}

const handleConnect = async (isRetry = false) => {
  const win = window as any;
  if (!win.WMKS) return ElMessage.error("SDK 未加载");

  try {
    vncStatus.value = 'connecting';
    if (!isRetry) vncDialogVisible.value = true;

    const res = await getVmConsoleTicket({
      host: activeVM.value.host,
      username: activeVM.value.username,
      password: activeVM.value.password,
      vmUuid: activeVM.value.vmUuid
    });
    
    const ticket = res.data?.ticket || res.ticket || res;
    await nextTick();
    startWMKS(ticket);
  } catch (err) {
    ElMessage.error("Ticket 申请失败,请检查机房连接");
    vncStatus.value = 'disconnected';
  }
}

const startWMKS = (ticket: string) => {
  const WMKS_LIB = (window as any).WMKS;
  wmksInstance.value = WMKS_LIB.createWMKS("wmks-container", {
    enableHints: true,
    useVNCHandshake: false,
    changeResolutionOnResizing: true,
    rescaleOnParentResize: true,
    position: 'absolute'
  });
  
  wmksInstance.value.register(WMKS_LIB.CONST.Events.CONNECTION_STATE_CHANGE, (evt: any, data: any) => {
    if (data.state === WMKS_LIB.CONST.ConnectionState.CONNECTED) {
      vncStatus.value = 'connected';
    }
  });

  wmksInstance.value.connect(`wss://${activeVM.value.host}:443/ticket/${ticket}`);
}

const sendCAD = () => wmksInstance.value?.sendCAD();
const handleCloseVNC = () => {
  if (wmksInstance.value) {
    wmksInstance.value.disconnect();
    wmksInstance.value.destroy();
    wmksInstance.value = null;
  }
  vncDialogVisible.value = false;
  isFullscreen.value = false;
  vncStatus.value = 'disconnected';
}

onBeforeUnmount(handleCloseVNC);
</script>

<style scoped lang="scss">
.vnc-app-wrapper {
  padding: 40px;
  background-color: #f8fafc;
  min-height: 100vh;
}

.page-header {
  max-width: 1200px;
  margin: 0 auto 40px;
  .brand {
    display: flex; align-items: center; gap: 12px;
    h1 { font-size: 22px; color: #334155; margin: 0; }
  }
}

.vm-grid {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
}

.vm-mini-card {
  background: #fff; border-radius: 12px; overflow: hidden;
  border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
  transition: transform 0.2s;
  &:hover { transform: translateY(-4px); }

  .vm-status-bar { 
    height: 4px; background: #cbd5e1;
    &.online { background: #10b981; }
  }
  
  .vm-card-body {
    padding: 24px; display: flex; flex-direction: column; align-items: center;
    .vm-icon {
      font-size: 32px; color: #3b82f6; margin-bottom: 16px;
      padding: 12px; background: #eff6ff; border-radius: 12px;
    }
    .vm-name { font-size: 17px; font-weight: 600; color: #1e293b; margin: 0 0 4px; }
    .vm-ip { font-size: 13px; color: #64748b; margin-bottom: 24px; }
    .vm-actions { width: 100%; .el-button { width: 100%; } }
  }

  .vm-footer {
    padding: 12px 20px; background: #f8fafc; border-top: 1px solid #f1f5f9;
    font-size: 11px; color: #94a3b8; font-family: ui-monospace, SFMono-Regular;
  }
}

:deep(.vnc-session-dialog) {
  display: flex; flex-direction: column;
  background: #000 !important; border-radius: 12px; overflow: hidden;
  
  .el-dialog__header {
    padding: 0 !important;
    margin: 0 !important;
    height: 50px;
    cursor: move; 
  }

  .el-dialog__body {
    flex: 1; display: flex; flex-direction: column;
    padding: 0 !important;
  }
  
  &.is-fullscreen { border-radius: 0; }
}

.session-header {
  height: 50px; background: #1e293b; padding: 0 20px;
  display: flex; justify-content: space-between; align-items: center;
  
  .session-info {
    display: flex; align-items: center; gap: 10px;
    pointer-events: none; 
    .status-indicator { 
      width: 8px; height: 8px; border-radius: 50%; background: #64748b;
      &.is-connected { background: #10b981; box-shadow: 0 0 8px #10b981; }
    }
    .session-name { color: #f1f5f9; font-size: 14px; font-weight: 500; }
  }
  
  .session-controls {
    pointer-events: auto;
    display: flex; align-items: center; gap: 10px;
    .divider { width: 1px; height: 16px; background: #334155; }
    .el-button { color: #94a3b8; &:hover { color: #fff; } }
  }
}

.session-stage {
  flex: 1; width: 100%; min-height: 600px;
  background: #000; position: relative;
  #wmks-container { width: 100% !important; height: 100% !important; position: absolute; }
}

:deep(.el-dialog__headerbtn) { display: none; }
</style>

这是index.html下面需要引入的内容

js 复制代码
    <link rel="stylesheet" href="/WebMKS_SDK_2.2.0/css/wmks-all.css">

    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    
    <script>
        window.jQuery = window.$ = window.jQuery;
        console.log('JQuery Check:', window.jQuery.fn.jquery);
    </script>

    <script src="/jquery-ui.min.js"></script>

    <script src="/WebMKS_SDK_2.2.0/wmks.min.js"></script>

    <script>
        if (window.jQuery && (window.jQuery.ui || window.jQuery.widget)) {
            console.log('Result: jQuery UI is fully loaded and attached to jQuery.');
        } else {
            console.error('Result: jQuery UI failed to attach to window.jQuery!');
        }
    </script>
相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax