Guacamole实现远程桌面+实时语音(VNC)
文章摘要:本文介绍了基于Guacamole+TigerVNC远程桌面方案叠加WebRTC语音的技术实现方案。核心包括:1) 使用Node.js搭建信令服务器管理WebSocket连接;2) 配置Guacamole启用WebRTC支持,集成STUN/TURN服务;3) 部署Coturn服务器进行NAT穿透;4) 通过Electron套壳浏览器实现WebRTC语音功能。该方案解决了传统RDP协议的黑屏
前言
继续方案的探索与选型,原计划选择RDP协议的远程桌面是想利用它原生支持语音的。业务“远程帮办”拆解就是远程桌面 + 实时双向语音。因为发现RDP协议的远程桌面,远程现在的被远程端(win pro)系统,会有抢占问题,导致被远程端黑屏,体验不好,所以最后还是选择VNC协议,上一篇已经分享了,这继续分享双向语音方案组合。。
一、技术选型
guacamole + TigerVNC远程桌面方案,继续叠加coturn + webRTC + Node实现双向实时语音。
方案核心
1.node跑信令服务js
// signaling-server.js
const WebSocket = require('ws服务地址(guacamole内置提供的websocket)');
// 启动一个WebSocket服务器,监听8080端口
const wss = new WebSocket.Server({ port: 端口 });
console.log('Signaling server started on ws://localhost:8080');
// 使用一个Map来存储连接的客户端,键为用户ID,值为WebSocket连接对象
const clients = new Map();
wss.on('connection', (ws) => {
// 为新连接生成一个唯一ID
const clientId = generateUniqueId();
clients.set(clientId, ws);
console.log(`Client ${clientId} connected`);
// 连接成功后,将ID发送给客户端
ws.send(JSON.stringify({ type: 'your-id', id: clientId }));
// 监听来自客户端的消息
ws.on('message', (message) => {
let data;
try {
data = JSON.parse(message);
} catch (e) {
console.error('Invalid JSON received:', message);
return;
}
console.log(`Received message from ${clientId}:`, data.type);
const targetClient = clients.get(data.targetId);
if (targetClient && targetClient.readyState === WebSocket.OPEN) {
// 为消息添加发送者ID,然后转发
data.senderId = clientId;
targetClient.send(JSON.stringify(data));
}
});
// 监听连接关闭事件
ws.on('close', () => {
clients.delete(clientId);
console.log(`Client ${clientId} disconnected`);
});
ws.on('error', (error) => {
console.error(`Error with client ${clientId}:`, error);
clients.delete(clientId);
});
});
// 生成一个简单的唯一ID函数
function generateUniqueId() {
return Math.random().toString(36).substr(2, 9);
}
放在服务器上,用node.js跑。
2.guacamole增加配置(guacamole.properties)
#如果使用webRTC
enable-webrtc: true
enable-websocket: true
websocket-enabled: true
webrtc-enabled: true
webrtc-stun-server: stun:coturn部署ip:3478
webrtc-turn-server: turn:coturn部署ip:3478
webrtc-turn-username: guacamole提供账号
webrtc-turn-password: guacamole提供账号密码
#音频输入配置
enable-audio: true
enable-audio-input: true
#音频编码器配置
#音频质量设置
audio-bitrate: 128000
audio-mime-types: audio/L16, audio/opus
3.coturn配置(turnserver.conf )
# --- 网络配置 ---
# 监听所有网络接口。注意:在生产环境中,应该只监听必要的接口
listening-ip=0.0.0.0
# 标准 TURN 端口
listening-port=3478
# TLS/DTLS 端口(取消注释以启用)
#tls-listening-port=5349
#dtls-listening-port=5349
# --- 中继配置 ---
# 中继端口范围,根据您的网络环境和预期负载调整
min-port=49152
max-port=50000
# 内部中继IP地址
relay-ip=内网ip
# 外部IP地址(NAT后的公网IP,如果有)
external-ip=外网ip
# --- 认证配置 ---
# 设置域名,用于长期凭证机制
realm=域名
# 启用长期凭证机制
#lt-cred-mech
# --- 用户凭证 ---
# 直接在配置文件中定义用户。注意:在生产环境中应使用更安全的方法
user=guacamole提供账号:guacamole提供账号密码
#user=user2:password2
# --- TLS/DTLS 配置 ---
# TLS 证书和私钥路径(取消注释以启用)
#cert=/etc/turnserver/fullchain.pem
#pkey=/etc/turnserver/privkey.pem
# 推荐的密码套件,提供强加密(取消注释以启用)
#cipher-list="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
# --- 安全设置 ---
# 启用指纹,防止中间人攻击
fingerprint
# 启用过期 nonce 检测,防止重放攻击(取消注释以启用)
#stale-nonce=3600
# 设置 DTLS 会话密钥的生命周期(单位:秒)(取消注释以启用)
#dtls-key-lifetime=3600
# --- 性能优化 ---
# 最大允许的总带宽(字节/秒),0 表示无限制
max-bps=0
# 所有会话的总配额(字节/秒),格式:数字:数字,0 表示无限制
total-quota=0:0
# 单个用户的配额(字节/秒),0 表示无限制
user-quota=0
# --- 日志设置 ---
# 启用详细日志,便于调试。在生产环境中可以降低日志级别
verbose
# --- 高级配置 ---
# 允许环回地址,用于测试。生产环境中应禁用
#no-loopback-peers
# 允许使用 TURN 服务的 IP 范围,增强安全性(取消注释并根据需要调整)
#allowed-peer-ip=10.0.0.0-10.255.255.255
#allowed-peer-ip=172.16.0.0-172.31.255.255
#allowed-peer-ip=192.168.0.0-192.168.255.255
# 启用 CLI 访问和状态报告(取消注释并设置密码以启用)
#cli-password=<strong-admin-password>
#status-port=5986
# --- 注意事项 ---
# 1. 在生产环境中,确保所有密码和密钥都是强密码,并定期更新
# 2. 根据您的具体需求和网络环境调整配置
# 3. 定期检查日志文件,监控服务器性能和可能的安全问题
# 4. 确保 TLS 证书有效且定期更新
# 5. 考虑使用防火墙进一步限制对 TURN 服务器的访问
# 6. 在生产环境中,考虑使用外部认证系统而不是直接在配置文件中存储用户凭证
# 7. 根据实际负载调整性能相关的参数
# 8. 定期更新 TURN 服务器软件以获取最新的安全补丁
集成使用
这里计划是用Electorn套浏览器,走浏览器支持的webRTC实现麦克风、语音。
guacamole本身就是走的浏览器,所以只需要参考guacamole开源的web自己写页面,实现远程桌面。
然后,页面增加走coturn的webRTC语音实现。
main.js
// public/main.js
const localVideo = document.getElementById('local-video');
const remoteVideo = document.getElementById('remote-video');
const myIdSpan = document.getElementById('my-id');
const peerIdInput = document.getElementById('peer-id-input');
const callBtn = document.getElementById('call-btn');
const hangupBtn = document.getElementById('hangup-btn');
const copyIdBtn = document.getElementById('copy-id-btn');
const callStatus = document.getElementById('call-status');
// 连接到信令服务器
const socket = new WebSocket('ws服务地址(guacamole内置提供的websocket)');
let myId;
let localStream;
let peerConnection;
let targetId;
// STUN服务器配置,用于NAT穿透。这里我们使用Google的公共STUN服务器。
const configuration = {
iceServers: [
//{ urls: 'stun:stun.l.google.com:19302' } 谷歌提供的
{ urls: 'stun:coturn部署ip:coturn的stun端口' }
]
};
// 1. WebSocket 信令处理
socket.onopen = () => {
console.log('Connected to signaling server');
};
socket.onmessage = (message) => {
const data = JSON.parse(message.data);
console.log('Received message:', data.type);
switch (data.type) {
case 'your-id':
myId = data.id;
myIdSpan.textContent = myId;
copyIdBtn.disabled = false;
break;
case 'offer':
handleOffer(data.offer, data.senderId);
break;
case 'answer':
handleAnswer(data.answer);
break;
case 'candidate':
handleCandidate(data.candidate);
break;
case 'hangup':
handleHangup();
break;
default:
break;
}
};
socket.onerror = (error) => {
console.error('WebSocket Error:', error);
};
// 2. 媒体流处理和UI事件
async function start() {
try {
// 获取本地音视频流
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.srcObject = localStream;
} catch (error) {
console.error('Error accessing media devices.', error);
alert('无法访问摄像头和麦克风,请检查权限。');
}
}
callBtn.onclick = () => {
targetId = peerIdInput.value;
if (!targetId) {
return alert('请输入对方的ID');
}
createPeerConnection();
// 创建并发送Offer
peerConnection.createOffer()
.then(offer => peerConnection.setLocalDescription(offer))
.then(() => {
sendMessage({ type: 'offer', offer: peerConnection.localDescription });
updateCallStatus(`正在呼叫 ${targetId}...`);
})
.catch(e => console.error(e));
};
hangupBtn.onclick = () => {
sendMessage({ type: 'hangup' });
handleHangup();
};
copyIdBtn.onclick = () => {
navigator.clipboard.writeText(myId).then(() => {
alert('ID已复制到剪贴板!');
}).catch(err => {
console.error('Could not copy text: ', err);
});
};
// 3. WebRTC核心功能函数
function createPeerConnection() {
peerConnection = new RTCPeerConnection(configuration);
// 将本地媒体流的轨道添加到连接中
localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream));
// 监听ICE Candidate事件,并发送给对方
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
sendMessage({ type: 'candidate', candidate: event.candidate });
}
};
// 监听远程媒体流
peerConnection.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0];
};
// 更新UI
hangupBtn.disabled = false;
callBtn.disabled = true;
}
function sendMessage(message) {
message.targetId = targetId;
socket.send(JSON.stringify(message));
}
// 被呼叫方:处理收到的Offer
function handleOffer(offer, senderId) {
targetId = senderId;
createPeerConnection();
peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
.then(() => peerConnection.createAnswer())
.then(answer => peerConnection.setLocalDescription(answer))
.then(() => {
sendMessage({ type: 'answer', answer: peerConnection.localDescription });
updateCallStatus(`与 ${targetId} 通话中`);
})
.catch(e => console.error(e));
}
// 呼叫方:处理收到的Answer
function handleAnswer(answer) {
peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
updateCallStatus(`与 ${targetId} 通话中`);
}
// 双方:处理收到的ICE Candidate
function handleCandidate(candidate) {
peerConnection.addIceCandidate(new RTCIceCandidate(candidate))
.catch(e => console.error(e));
}
// 双方:处理挂断
function handleHangup() {
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
remoteVideo.srcObject = null;
updateCallStatus('状态:空闲');
hangupBtn.disabled = true;
callBtn.disabled = false;
targetId = null;
}
function updateCallStatus(status) {
callStatus.textContent = `状态:${status}`;
}
// 页面加载后立即启动
start();
测试html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC Voice Communication</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.container { display: flex; flex-direction: column; gap: 20px; }
.videos { display: flex; gap: 20px; }
video { border: 1px solid black; width: 320px; height: 240px; background-color: #333; }
.controls { display: flex; flex-direction: column; gap: 10px; max-width: 400px; }
input, button { padding: 8px; font-size: 16px; }
#call-status { font-weight: bold; color: #555; }
</style>
</head>
<body>
<h1>WebRTC 双向语音/视频通信</h1>
<div class="container">
<div class="videos">
<div>
<h3>本地视频</h3>
<video id="local-video" autoplay muted></video>
</div>
<div>
<h3>远程视频</h3>
<video id="remote-video" autoplay></video>
</div>
</div>
<div class="controls">
<div>
<strong>你的ID: </strong>
<span id="my-id">正在连接...</span>
<button id="copy-id-btn" disabled>复制ID</button>
</div>
<input type="text" id="peer-id-input" placeholder="输入对方的ID">
<button id="call-btn">呼叫</button>
<button id="hangup-btn" disabled>挂断</button>
<p id="call-status">状态:空闲</p>
</div>
</div>
<script src="main.js"></script>
</body>
</html>
效果
效果就用这个测试html先试验语音吧,Electorn组合浏览器就不细讲了,让大家自己探索吧。其实呢,我也是不擅长前端,也志不在前端。
至此,guacamole + TigerVNC + coturn + node.js + webRTC实现VNC远程桌面+双向实时语音方案就走通了,剩下就是前端集成、优化了。
总结
就写到这,希望能帮到大家,还是那个认知,思路很重要,技术的实现里,思路可以出方案。其他时候,思路变现也是有路径的,uping
更多推荐

所有评论(0)