Recentemente, deparei-me com um conceito fascinante: compartilhar a área de trabalho remota usando apenas tecnologias web. Embora o artigo original fornecido fosse limitado em termos de código-fonte, decidi desenvolver uma solução funcional. Durante um período de restrições locais, investi tempo para criar uma implementação básica, ainda que instável, que estou compartilhando neste artigo.
- Princípio Funcional
O componente servidor, que corresponde ao computador controlaod, estabelece uma escuta em uma porta WebSocket utilizando a biblioteca Fleck. Ao receber mensagens dos clientes, o sistema interperta eventos de teclado e mouse, simulando essas entradas localmente com a ajuda do componente KeyMouseHook. Adicionalmente, um temporizador captura a tela a cada 500ms e transmite a imagem para todos os clientes conectados.
- Código do Servidor
using Fleck;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Loamen.KeyMouseHook;
using Loamen.KeyMouseHook.Native;
namespace ServidorDesktopRemoto
{
public partial class FormPrincipal : Form
{
private List<IWebSocketConnection> conexoesAtivas;
private WebSocketServer servidorWebSocket;
private InputSimulador simulador = new InputSimulador();
[DllImport("user32.dll")]
public static extern bool PrintWindow(IntPtr janela, IntPtr hdcBlt, UInt32 nFlags);
public FormPrincipal()
{
InitializeComponent();
}
private void AdicionarLogConsole(string mensagem)
{
if (richTextBoxLog.Text.Length > 50000)
{
richTextBoxLog.Clear();
}
richTextBoxLog.AppendText(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff"));
richTextBoxLog.AppendText(" " + mensagem);
richTextBoxLog.AppendText("\r\n");
richTextBoxLog.ScrollToCaret();
if (checkBoxHabilitarLog.Checked)
{
string caminhoLog = ConfigurationManager.AppSettings["caminho_log"].ToString() + "/" + DateTime.Now.ToString("yyyy-MM-dd");
if (!Directory.Exists(caminhoLog))
{
Directory.CreateDirectory(caminhoLog);
}
File.AppendAllText(caminhoLog + "/servidor_remoto.log",
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + " " + mensagem + "\r\n", Encoding.UTF8);
}
}
private void btnIniciarServidor_Click(object sender, EventArgs e)
{
conexoesAtivas = new List<IWebSocketConnection>();
servidorWebSocket = new WebSocketServer("ws://" + txtEnderecoIP.Text + ":" + txtPorta.Text);
servidorWebSocket.RestartAfterListenError = true;
servidorWebSocket.Start(socket =>
{
socket.OnOpen = () =>
{
Invoke((new Action(() =>
{
AdicionarLogConsole("Conexão aberta!");
})));
conexoesAtivas.Add(socket);
};
socket.OnClose = () =>
{
Invoke((new Action(() =>
{
AdicionarLogConsole("Conexão fechada!");
})));
conexoesAtivas.Remove(socket);
};
socket.OnMessage = mensagem =>
{
Invoke((new Action(() =>
{
AdicionarLogConsole(mensagem);
if (checkBoxModoControle.Checked)
{
var comando = JsonConvert.DeserializeObject<ComandoCliente>(mensagem);
ProcessarComando(comando);
}
})));
};
});
AdicionarLogConsole("Servidor iniciado com sucesso");
}
private void FormPrincipal_Shown(object sender, EventArgs e)
{
txtEnderecoIP.Text = ConfigurationManager.AppSettings["ip"].ToString();
txtPorta.Text = ConfigurationManager.AppSettings["porta"].ToString();
}
private Image CapturarTela(IntPtr handle)
{
Control controle = Control.FromHandle(handle);
Bitmap imagem = new Bitmap(controle.Width, controle.Height);
Graphics gp = Graphics.FromImage(imagem);
IntPtr dc = gp.GetHdc();
PrintWindow(handle, dc, 0);
gp.ReleaseHdc();
gp.Dispose();
return imagem;
}
private void btnCapturarTela_Click(object sender, EventArgs e)
{
try
{
Bitmap bitmapTela = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
Graphics g = Graphics.FromImage(bitmapTela);
g.CopyFromScreen(new Point(0, 0), new Point(0, 0), Screen.PrimaryScreen.Bounds.Size);
imagemTemporaria = SalvarComoJpg(bitmapTela, 10);
AdicionarLogConsole("Captura de tela realizada com sucesso");
}
catch (Exception ex)
{
AdicionarLogConsole("Erro ao capturar tela: " + ex.Message);
}
}
public Image SalvarComoJpg(Image imagem, long qualidade)
{
ImageCodecInfo codecInfo = null;
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.MimeType == "image/jpeg")
{
codecInfo = codec;
break;
}
}
EncoderParameters parametros = new EncoderParameters(2);
parametros.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, qualidade);
parametros.Param[1] = new EncoderParameter(System.Drawing.Imaging.Encoder.Compression, (long)EncoderValue.CompressionLZW);
return imagem;
}
public byte[] ImagemParaBytes(Image imagem)
{
MemoryStream ms = new MemoryStream();
imagem.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
return ms.ToArray();
}
private void btnEnviarTela_Click(object sender, EventArgs e)
{
byte[] dadosImagem = ImagemParaBytes(imagemTemporaria);
conexoesAtivas.ToList().ForEach(conexao => conexao.Send(dadosImagem));
}
private void checkBoxCapturaAutomatica_CheckedChanged(object sender, EventArgs e)
{
timerCaptura.Enabled = checkBoxCapturaAutomatica.Checked;
}
private void timerCaptura_Tick(object sender, EventArgs e)
{
btnCapturarTela_Click(sender, e);
btnEnviarTela_Click(sender, e);
}
private void ProcessarComando(ComandoCliente comando)
{
switch (comando.acao)
{
case "mousemove":
var ponto = new Point(comando.x, comando.y).ToAbsolutePoint();
simulador.Mouse.MoverPara(ponto.X, ponto.Y);
break;
case "mousedown":
simulador.Mouse.CliqueEsquerdoPressionado();
break;
case "mouseup":
simulador.Mouse.CliqueEsquerdoSolto();
break;
case "click":
simulador.Mouse.CliqueEsquerdo();
break;
case "dbclick":
simulador.Mouse.DuploCliqueEsquerdo();
break;
case "rightclick":
simulador.Mouse.CliqueDireito();
break;
case "keyup":
List<VirtualKeyCode> teclasModificadoras = new List<VirtualKeyCode>();
if (comando.shift) teclasModificadoras.Add(VirtualKeyCode.SHIFT);
if (comando.ctrl) teclasModificadoras.Add(VirtualKeyCode.CONTROL);
if (comando.alt) teclasModificadoras.Add(VirtualKeyCode.MENU);
simulador.Teclado.TeclaModificada(teclasModificadoras, (VirtualKeyCode)comando.tecla);
break;
}
}
}
}
- Interface do Servidor
A interface do servidor contém elementos padrão do Windows Forms, com nomes intuitivos que facilitam a identificação dos componentes. Não há necessidade de explicações detalhadas sobre a interface, pois os controles são autoexplicativos.
- Arquivos Adicionais do Servidor
É necessário incluir uma classe para representar os comandos do cliente, além do arquivo de configuração da aplicação.
- Cliente Implementado com jQuery
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Cliente Desktop Remoto</title>
<script src="jquery-3.6.0.min.js"></script>
<style>
body {
padding: 0px;
margin: 0px;
overflow: hidden;
}
#canvasDesktop {
display: block;
cursor: crosshair;
}
</style>
</head>
<body>
<canvas id="canvasDesktop"></canvas>
</body>
<script>
var wsImpl = window.WebSocket || window.MozWebSocket;
var conexaoWebSocket = new wsImpl("ws://127.0.0.1:10086/");
var canvas = document.getElementById("canvasDesktop");
var ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var imagemDesktop = new Image();
conexaoWebSocket.onmessage = function (evento) {
imagemDesktop.src = URL.createObjectURL(evento.data);
imagemDesktop.onload = function () {
ctx.drawImage(imagemDesktop, 0, 0, canvas.width, canvas.height);
};
};
var shiftPressionado = false;
var ctrlPressionado = false;
var altPressionado = false;
$(window).keydown(function (evento) {
AtualizarTeclasModificadoras(evento, true);
if (TeclaNaoEModificadora(evento)) {
EnviarComandoTeclado("keydown", evento.keyCode);
}
});
$(window).keyup(function (evento) {
AtualizarTeclasModificadoras(evento, false);
if (TeclaNaoEModificadora(evento)) {
EnviarComandoTeclado("keyup", evento.keyCode);
}
});
function AtualizarTeclasModificadoras(evento, pressionada) {
ctrlPressionado = evento.ctrlKey;
shiftPressionado = evento.shiftKey;
altPressionado = evento.altKey;
}
function TeclaNaoEModificadora(evento) {
return evento.key !== "Shift" && evento.key !== "Ctrl" && evento.key !== "Alt";
}
function EnviarComandoTeclado(acao, codigoTecla) {
var comando = {
acao: acao,
tecla: codigoTecla,
shift: shiftPressionado,
ctrl: ctrlPressionado,
alt: altPressionado
};
conexaoWebSocket.send(JSON.stringify(comando));
}
$(canvas).mousemove(function (e) {
var x = e.offsetX;
var y = e.offsetY;
conexaoWebSocket.send(JSON.stringify({acao: "mousemove", x: x, y: y}));
});
$(canvas).contextmenu(function (e) {
e.preventDefault();
var x = e.offsetX;
var y = e.offsetY;
conexaoWebSocket.send(JSON.stringify({acao: "rightclick", x: x, y: y}));
});
$(canvas).mousedown(function (e) {
var acao = e.button === 0 ? "mousedown" : "rightmousedown";
conexaoWebSocket.send(JSON.stringify({acao: acao, x: e.offsetX, y: e.offsetY}));
});
$(canvas).mouseup(function (e) {
var acao = e.button === 0 ? "mouseup" : "rightmouseup";
conexaoWebSocket.send(JSON.stringify({acao: acao, x: e.offsetX, y: e.offsetY}));
});
$(canvas).click(function (e) {
conexaoWebSocket.send(JSON.stringify({acao: "click", x: e.offsetX, y: e.offsetY}));
});
$(canvas).dblclick(function (e) {
conexaoWebSocket.send(JSON.stringify({acao: "dbclick", x: e.offsetX, y: e.offsetY}));
});
document.oncontextmenu = function () {
return false;
};
$(window).resize(function() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
</script>
</html>