解决 RDP 连接后按键循环发送导致内存卡死问题 链接到标题

为了解决 RDP 连接后按键循环发送导致内存卡死的问题,主要需要确保在重新连接时正确清理旧的 Guacamole 客户端及其事件监听器。

1. 使用 useRef 跟踪客户端实例 链接到标题

clientkeyboard 等对象使用 useRef 保存,以便在清理时获取最新实例。

2. 在重新连接前清理旧资源 链接到标题

在创建新客户端前,断开旧连接并移除事件监听器。

3. 确保事件处理函数使用最新实例 链接到标题

在键盘事件处理函数中,使用 ref.current 来引用当前客户端。主要集中在正确管理 Guacamole 客户端生命周期和事件监听器清理。

代码示例 链接到标题

import React, { useEffect, useRef, useState } from "react";
// ... 其他导入保持不变

const GuacdConsumer = () => {
    // 使用 useRef 替代 useState 保存易变对象
    const clientRef = useRef(null);
    const keyboardRef = useRef(null);
    const mouseRef = useRef(null);
    const touchRef = useRef(null);
    const tunnelRef = useRef(null);

    const renderDisplay = (sessionId, protocol, width, height, value) => {
        // 清理旧连接
        const cleanup = () => {
            if (clientRef.current) {
                clientRef.current.disconnect();
                clientRef.current = null;
            }
            if (tunnelRef.current) {
                tunnelRef.current.disconnect();
                tunnelRef.current = null;
            }
            if (keyboardRef.current) {
                keyboardRef.current.onkeydown = null;
                keyboardRef.current.onkeyup = null;
                keyboardRef.current = null;
            }
            if (mouseRef.current) {
                mouseRef.current.onmousedown = null;
                mouseRef.current.onmouseup = null;
                mouseRef.current.onmousemove = null;
                mouseRef.current = null;
            }
            if (touchRef.current) {
                touchRef.current.onmousedown = null;
                touchRef.current.onmousemove = null;
                touchRef.current.onmouseup = null;
                touchRef.current = null;
            }
            const displayEle = document.getElementById("display");
            if (displayEle) displayEle.innerHTML = "";
        };

        cleanup(); // 先清理旧连接

        let tunnel = new Guacamole.WebSocketTunnel(
            `${wsServer}/sessions/${sessionId}/tunnel`
        );
        tunnelRef.current = tunnel;

        let client = new Guacamole.Client(tunnel);
        clientRef.current = client;

        client.onclipboard = handleClipboardReceived;
        client.onstatechange = (state) => {
            if (state === 3) {
                message.destroy("login");
                setModalOpen(false);
            }
            onClientStateChange(state, sessionId, tunnel);
        };

        client.onerror = onError;
        tunnel.onerror = onError;

        const displayEle = document.getElementById("display");
        const element = client.getDisplay().getElement();
        displayEle.appendChild(element);

        client.connect(paramStr);

        const display = client.getDisplay();
        display.onresize = function (width, height) {
            display.scale(
                Math.min(
                    window.innerHeight / display.getHeight(),
                    window.innerWidth / display.getHeight()
                )
            );
        };

        const sink = new Guacamole.InputSink();
        displayEle.appendChild(sink.getElement());
        sink.focus();

        const keyboard = new Guacamole.Keyboard(sink.getElement());
        keyboardRef.current = keyboard;

        const handleKeyDown = (keysym) => {
            if (!clientRef.current) return;
            clientRef.current.sendKeyEvent(1, keysym);
        };

        const handleKeyUp = (keysym) => {
            if (clientRef.current) {
                clientRef.current.sendKeyEvent(0, keysym);
            }
        };

        keyboard.onkeydown = handleKeyDown;
        keyboard.onkeyup = handleKeyUp;

        const sinkFocus = debounce(() => {
            sink.focus();
        });

        const mouse = new Guacamole.Mouse(element);
        mouseRef.current = mouse;

        mouse.onmousedown = mouse.onmouseup = function (mouseState) {
            sinkFocus();
            clientRef.current?.sendMouseState(mouseState);
        };

        mouse.onmousemove = function (mouseState) {
            sinkFocus();
            clientRef.current?.getDisplay().showCursor(false);
            mouseState.x = mouseState.x / display.getScale();
            mouseState.y = mouseState.y / display.getScale();
            clientRef.current?.sendMouseState(mouseState);
        };

        const touch = new Guacamole.Mouse.Touchpad(element);
        touchRef.current = touch;

        touch.onmousedown =
        touch.onmousemove =
        touch.onmouseup =
        function (state) {
            clientRef.current?.sendMouseState(state);
        };
    };

    useEffect(() => {
        return () => {
            // 组件卸载时彻底清理
            if (clientRef.current) {
                clientRef.current.disconnect();
            }
            if (tunnelRef.current) {
                tunnelRef.current.disconnect();
            }
            if (keyboardRef.current) {
                keyboardRef.current.onkeydown = null;
                keyboardRef.current.onkeyup = null;
            }
            if (mouseRef.current) {
                mouseRef.current.onmousedown = null;
                mouseRef.current.onmouseup = null;
                mouseRef.current.onmousemove = null;
            }
            if (touchRef.current) {
                touchRef.current.onmousedown = null;
                touchRef.current.onmousemove = null;
                touchRef.current.onmouseup = null;
            }
        };
    }, []);

    const sendCombinationKey = (keys) => {
        const client = clientRef.current;
        if (!client) return;
        
        requestAnimationFrame(() => {
            keys.forEach(keysym => client.sendKeyEvent(1, keysym));
            keys.forEach(keysym => client.sendKeyEvent(0, keysym));
        });
    };
};

## 主要改动说明

### 1. 使用 `useRef` 管理易变对象
-`client``tunnel` 等 Guacamole 对象改为 `useRef` 存储确保在闭包中始终访问最新实例

### 2. 连接前清理机制
- 在创建新连接前增加 `cleanup` 函数彻底清理旧连接相关资源

### 3. 严格的事件监听清理
- 在组件卸载和重新连接时明确移除所有事件监听器

### 4. 安全的事件处理
- 所有事件处理函数都通过 `clientRef.current` 访问客户端并增加空值检查

### 5. 动画帧优化
- 在发送组合键时使用 `requestAnimationFrame` 避免快速连续操作

## 这些修改可以确保
- 每次重新建立连接时旧连接会被正确清理
- 不会存在多个客户端实例同时
- 键盘鼠标事件监听器被正确清理防止重复触发问题