В данной статье я планирую показать, как написать простейший VNC на языке C# и Python.Данный проект будет являться лишь обучающим материалом и одним из способов реализации подобного приложения, чтобы вы могли на его основе написать свой софт. В моем коде будет только необходимый функционал для подобных приложений.
Что такое VNC?
Проще говоря, VNC – это удаленное управление компьютером, в нашем случае будет реализовано управление курсором на экране клиента а также трансляция картинки с экрана клиента. Картинка с клиента будет отправлена на сервер, а координаты курсора будут собираться с сервера и отправляться на клиентскую часть для того чтобы перенести на компьютере клиента мышь в ту же область, что и на сервере.Как будет реализовано VNC?
У нас будет 2 части проекта.
Первой частью будет клиентская сторона, написана эта часть будет на C#. Она будет отвечать за создание скриншотов, форматирование их в формат base64 а также отправку этих скриншотов на сервер. Также клиентский код будет эмулировать движение курсора по координатам, которые мы будем получать от сервера.
Вторая часть проекта будет серверной стороной. Она будет отвечать за получение скриншотов с клиента в формате base64 а затем отображения этих скриншотов на HTML странице. Также серверная часть будет получать координаты вашего курсора и отправлять их на клиентскую часть для эмуляции курсора мыши.Теперь, когда мы разобрались с тем, как будет устроен наш проект, можно приступать к реализации и написанию нашей программы.
Написание клиентской части:
Клиентская часть будет реализована на C#. Для начала нам потребуется создать консольное приложение C# Net Framework. IDE выбранным для написания клиентской части будет Visual Studio.
В созданном проекте будет по стандарту файл с названием Program.cs, в нем мы и будем писать на данный момент весь код клиентской части.Как и в предыдущих своих темах, я для начала покажу полный код, а затем буду объяснять строки этого кода:
C#:
using Newtonsoft.Json;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Encoder = System.Drawing.Imaging.Encoder;
class Stream
{
static async Task Main(string[] args)
{
string serverUrl = “http://127.0.0.1:5000”;
while (true)
{
await CaptureAndSendScreenshot(serverUrl);
await Task.Delay(5000);
}
}
static public async Task CaptureAndSendScreenshot(string serverUrl)
{
Bitmap screenshot = CaptureScreen();
byte[] screenshotBytes = GetBytesFromImage(screenshot);
string screenshotString = Convert.ToBase64String(screenshotBytes);
bool isSent = await SendBase64ToServer(screenshotString, serverUrl);
if (isSent)
{
Console.WriteLine(“Screenshot sent successfully!”);
}
else
{
Console.WriteLine(“Failed to send screenshot!”);
}
}
static byte[] GetBytesFromImage(Bitmap image)
{
using (MemoryStream ms = new MemoryStream())
{
image.Save(ms, ImageFormat.Jpeg);
return ms.ToArray();
}
}
static Bitmap CaptureScreen()
{
Rectangle screenBounds = Screen.PrimaryScreen.Bounds;
Bitmap screenshot = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb);
using (Graphics graphics = Graphics.FromImage(screenshot))
{
graphics.CopyFromScreen(screenBounds.X, screenBounds.Y, 0, 0, screenBounds.Size, CopyPixelOperation.SourceCopy);
}
return screenshot;
}
static async Task<bool> SendBase64ToServer(string base64Data, string serverUrl)
{
string apiUrl = $”{serverUrl}/upload_screenshot”;
var payload = new { screenshot = base64Data };
string jsonPayload = JsonConvert.SerializeObject(payload);
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, “application/json”);
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(apiUrl, content);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
Теперь перейдем к объяснению данного кода:
C#:
static async Task Main(string[] args)
{
string serverUrl = “http://127.0.0.1:5000”;
while (true)
{
await CaptureAndSendScreenshot(serverUrl);
await Task.Delay(5000);
В переменной serverUrl указывается ссылка на наш будущий сервер. Затем, в бесконечном цикле вызывается функция CaptureAndSendScreenshot, которая делает скриншоты и отправляет их на сервер в формате base64. Delay означает задержку в определенное количество секунд, она нужна для того, чтобы скриншоты отправлялись с определенной периодичностью. Я установил задержку для того, чтобы мой слабый ПК не помер при запуске софта, вы же можете удалить эту строку, чтобы трансляция была более плавной.
C#:
static public async Task CaptureAndSendScreenshot(string serverUrl)
{
Bitmap screenshot = CaptureScreen();
byte[] screenshotBytes = GetBytesFromImage(screenshot);
string screenshotString = Convert.ToBase64String(screenshotBytes);
bool isSent = await SendBase64ToServer(screenshotString, serverUrl);
if (isSent)
{
Console.WriteLine(“Screenshot sent successfully!”);
}
else
{
Console.WriteLine(“Failed to send screenshot!”);
Эта функция захватывает скриншот экрана и преобразует его в формат base64, вызывая GetBytesFromImage, а затем отправляет его на сервер, вызывая функцию SendBase64ToServer. Если скриншот успешно получен и отправлен на сервер, то в консоль выводится сообщение “Screenshot sent successfully!”. Если что-то пошло не так, то выводится сообщение “Failed to send screenshot!”.
C#:
static byte[] GetBytesFromImage(Bitmap image)
{
using (MemoryStream ms = new MemoryStream())
{
image.Save(ms, ImageFormat.Jpeg);
return ms.ToArray();
Эта функция принимает скриншот и затем конвертирует jpeg в массив байтов.
C#:
static Bitmap CaptureScreen()
{
Rectangle screenBounds = Screen.PrimaryScreen.Bounds;
Bitmap screenshot = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb);
using (Graphics graphics = Graphics.FromImage(screenshot))
{
graphics.CopyFromScreen(screenBounds.X, screenBounds.Y, 0, 0, screenBounds.Size, CopyPixelOperation.SourceCopy);
}
return screenshot;
Эта функция захватывает скриншот экрана. “Screen.PrimaryScreen.Bounds” используется для получения размера экрана. Также в данной функции создается объект Bitmap, в который копируется изображение экрана, используя “Graphics”. Именно объект Bitmap и используется в функции “GetBytesFromImage”.
C#:
static async Task<bool> SendBase64ToServer(string base64Data, string serverUrl)
{
string apiUrl = $”{serverUrl}/upload_screenshot”;
var payload = new { screenshot = base64Data };
string jsonPayload = JsonConvert.SerializeObject(payload);
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, “application/json”);
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(apiUrl, content);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
Эта функция принимает скриншот в формате строки base64 и отправляет на сервер в формате base64 через POST запрос с использованием HTTP.
На данный момент, клиентская часть готова. В дальнейшем мы её ещё дополним для эмуляции курсора мыши, а пока что нам нужно написать серверную часть, которая будет принимать скриншоты и отображать их.Серверная часть будет написана на Python с использованием Flask.
Спойлер: Что такое Flask?
Приступим к написанию сервера:
Для начала нам нужно создать проект, для этого я выбрал IDE PyCharm. Версия Python, которую я планирую использовать – Python 3.11.Установку IDE и создание проекта я уже показывал в предыдущей теме по созданию скам-обменника, так что показывать я не буду это.После создания проекта создаем файл main.py, если он ещё не создан, и первым делом заходим в терминал в PyCharm и скачиваем библиотеку Flask командой “pip install flask”.Теперь, как обычно, покажу код, а затем напишу разъяснения к этому коду.
Python:
from flask import Flask, render_template, request from flask_socketio import SocketIO, emit app = Flask(__name__) socketio = SocketIO(app) @app.route(‘/upload_screenshot’, methods=[‘POST’]) def upload_screenshot(): screenshot_data = request.json[‘screenshot’] emit(‘screenshot_received’, {‘screenshot’: screenshot_data}, broadcast=True, namespace=’/’) return “Screenshot received successfully!” @app.route(‘/translations’) def translations(): return render_template(‘translations.html’) if __name__ == ‘__main__’: socketio.run(app, debug=True, allow_unsafe_werkzeug=True)
Теперь приступим к разъяснению кода:
Python:
@app.route(‘/upload_screenshot’, methods=[‘POST’]) def upload_screenshot(): screenshot_data = request.json[‘screenshot’] emit(‘screenshot_received’, {‘screenshot’: screenshot_data}, broadcast=True, namespace=’/’) return “Screenshot received successfully!”
Данная функция принимает скриншоты в формате JSON, в котором хранится base64 строка, которая и является скриншотом.Данные извлекаются из JSON с помощью “request.json[‘screenshot’]”, и затем отправляются извлеченные данные через сокет событием “screenshot_received” в HTML файл, который мы пока что не создали, но позже мы его обязательно создадим.
Python:
if __name__ == ‘__main__’: socketio.run(app, debug=True, allow_unsafe_werkzeug=True)
Данный код запускает наше Flask приложение с определенными параметрами. Такими как debug и allow_unsafe_werkzeug.allow_unsafe_werkzeug Означает, что сервер может быть запущен в режиме, который не предназначен для использования в релизном приложении, так как некоторые функции могут быть не безопасны для использования в готовом приложении.Теперь приступим к веб-части для отображения скриншотов. Для начала создаем папку templates. Это папка по умолчанию, в которой Flask ищет HTML файлы. Лучше её не менять, так как эта папка является привычным для всех кодеров местом, где хранятся HTML файлы.
Теперь внутри этой папки создаем HTML файл с названием translations и в нем пишем вот такой вот код:Python:
Python:
<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8″> <meta name=”viewport” content=”width=device-width, initial-scale=1.0″> <title>Translations</title> <script src=”https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.1/socket.io.js”></script> </head> <body> <div id=”image-container”></div> <script> const socket = io(); socket.on(‘connect’, function() { console.log(‘Connected to server’); }); socket.on(“screenshot_received”, function(data) { const imageContainer = document.getElementById(‘image-container’); const img = new Image(); img.src = ‘data:image/jpeg;base64,’ + data.screenshot; imageContainer.innerHTML = ”; imageContainer.appendChild(img); }); </script> </body> </html>
Теперь приступим к разъяснению кода:
Python:
<div id=”image-container”></div>
Эта строка является объектом в который будет передаваться скриншот.
Python:
const socket = io(); socket.on(‘connect’, function() { console.log(‘Connected to server’); });
Этот код отвечает за подключение к сокету и написан он на JS. При успешном подключении в консоль выводится “Connected to server”, но выводится не в ту консоль, которая в PyCharm, а в консоль, которая находится в браузере при нажатии на клавишу F12.
Python:
socket.on(“screenshot_received”, function(data) { const imageContainer = document.getElementById(‘image-container’); const img = new Image(); img.src = ‘data:image/jpeg;base64,’ + data.screenshot; imageContainer.innerHTML = ”; imageContainer.appendChild(img); });
Этот код также написан на JS. Он нужен для того, чтобы вызывать событие screenshot_received, чтобы принимать изображение через сокет и выводить его на странице HTML.
Python:
const imageContainer = document.getElementById(‘image-container’);
В этой строке указывается объект, в который и будет транслироваться скриншот. В нашем HTML-шаблоне это div объект с id “image-container”, о нем я писал ранее.
Python:
imageContainer.innerHTML = ”;
Эта строка очищает контейнер с id “image-container” перед тем, как загрузить туда новый скриншот. Это нужно, чтобы скриншоты заменяли друг друга, а не скапливались на одной странице.
С написанием клиента и сервера для трансляции изображения мы закончили. Теперь можно все это запустить и проверить.Первым делом запускаем сервер на Python. Если в коде нет ошибок, то при удачном запуске мы должны увидеть что-то подобное в консоли PyCharm:
Теперь берем этот IP адрес и вставляем его в клиентскую часть в строку “string serverUrl”, а затем запускаем клиентскую часть. Если все удачно запустится, в консоли клиента мы должны будем увидеть такой текст: “Screenshot sent successfully!”
После того как мы убедились, что нет ошибок и все запустилось, можно перейти по ссылке на страницу, где отображается скриншот. В моем случае это “http://127.0.0.1:5000/translations”. На данной странице вы сможете уже увидеть трансляцию в действии.Теперь, когда мы убедились в работоспособности нашего кода, приступим к эмуляции движения курсора. Для этого нам нужно зайти в проект клиента и дописать код.
Спойлер: Что будет использоваться для эмуляции?
Для получения координат с сервера будем использовать подключение через сокеты. Теперь представлю сам код:
C#:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Text;
using System.Runtime.InteropServices;
class Program
{
[DllImport(“user32.dll”)]
static extern bool SetCursorPos(int x, int y);
[DllImport(“user32.dll”)]
static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, int dwExtraInfo);
const int MOUSEEVENTF_LEFTDOWN = 0x02;
const int MOUSEEVENTF_LEFTUP = 0x04;
static async Task Main(string[] args)
{
string serverUrl = “http://127.0.0.1:5000”;
string tcpServerIp = “127.0.0.1”;
int tcpServerPort = 12345;
// Запускаем метод, который будет принимать координаты через TCP сокет
_ = Task.Run(() => ReceiveCoordinatesFromTCPServer(tcpServerIp, tcpServerPort));
// После запуска метода принятия координат, продолжаем отправку скриншотов
while (true)
{
await CaptureAndSendScreenshot(serverUrl);
await Task.Delay(5000);
}
}
static async Task ReceiveCoordinatesFromTCPServer(string ip, int port)
{
try
{
while (true)
{
using (TcpClient client = new TcpClient())
{
await client.ConnectAsync(ip, port);
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[1024];
StringBuilder coordinates = new StringBuilder();
while (true)
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
break;
string coords = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine(“Received coordinates: ” + coords);
// Получаем координаты X и Y
string[] coordArray = coords.Split(‘,’);
int x = int.Parse(coordArray[0]);
int y = int.Parse(coordArray[1]);
// Эмулируем движение мыши
SetCursorPos(x, y);
// Эмулируем клик левой кнопки мыши
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, x, y, 0, 0);
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($”Error receiving coordinates: {ex.Message}”);
}
}
static async Task CaptureAndSendScreenshot(string serverUrl)
{
try
{
Bitmap screenshot = CaptureScreen();
byte[] screenshotBytes = GetBytesFromImage(screenshot);
string screenshotString = Convert.ToBase64String(screenshotBytes);
await SendBase64ToServer(screenshotString, serverUrl);
Console.WriteLine(“Screenshot sent successfully!”);
}
catch (Exception ex)
{
Console.WriteLine($”Failed to send screenshot: {ex.Message}”);
}
}
static byte[] GetBytesFromImage(Bitmap image)
{
using (MemoryStream ms = new MemoryStream())
{
image.Save(ms, ImageFormat.Jpeg);
return ms.ToArray();
}
}
static Bitmap CaptureScreen()
{
Rectangle screenBounds = Screen.PrimaryScreen.Bounds;
Bitmap screenshot = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb);
using (Graphics graphics = Graphics.FromImage(screenshot))
{
graphics.CopyFromScreen(screenBounds.X, screenBounds.Y, 0, 0, screenBounds.Size, CopyPixelOperation.SourceCopy);
}
return screenshot;
}
static async Task SendBase64ToServer(string base64Data, string serverUrl)
{
try
{
string apiUrl = $”{serverUrl}/upload_screenshot”;
var payload = new { screenshot = base64Data };
string jsonPayload = Newtonsoft.Json.JsonConvert.SerializeObject(payload);
StringContent content = new StringContent(jsonPayload, Encoding.UTF8, “application/json”);
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.PostAsync(apiUrl, content);
if (!response.IsSuccessStatusCode)
{
throw new Exception($”Failed to send screenshot to server. Status code: {response.StatusCode}”);
}
}
}
catch (Exception ex)
{
throw new Exception($”Error sending screenshot to server: {ex.Message}”);
}
}
}
Теперь предоставлю объяснения к коду:
[DllImport(“user32.dll”)] Предназначен для того что бы использовать win32
const int MOUSEEVENTF_LEFTDOWN = 0x02;
const int MOUSEEVENTF_LEFTUP = 0x04;
Данные контанты нужно что бы назначить названия событиям для эмуляции движения и эмуляции нажатия левой кнопки мыши
string tcpServerIp = “127.0.0.1”;
int tcpServerPort = 12345;
Эти строки нужны что бы подключаться к сереверу через TCP сокет
static async Task ReceiveCoordinatesFromTCPServer(string ip, int port)
{
try
{
while (true)
{
using (TcpClient client = new TcpClient())
{
await client.ConnectAsync(ip, port);
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[1024];
StringBuilder coordinates = new StringBuilder();
while (true)
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
break;
string coords = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine(“Received coordinates: ” + coords);
string[] coordArray = coords.Split(‘,’);
int x = int.Parse(coordArray[0]);
int y = int.Parse(coordArray[1]);
SetCursorPos(x, y);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, x, y, 0, 0);
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($”Error receiving coordinates: {ex.Message}”);
Эта функция отвечает за получение координат x и y, которые будут поступать от сервера. Также эта функция отвечает за эмуляцию мыши по координатам, которые получаем.
С кодом клиента надеюсь все +- понятно, теперь приступим к коду сервера.
Python:
import socket
from flask import Flask, request, jsonify, render_template # Импортируем render_template
from flask_socketio import SocketIO, emit
app = Flask(__name__)
socketio = SocketIO(app)
click_coordinates = {“x”: None, “y”: None}
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((‘127.0.0.1’, 12345))
server_socket.listen(1)
@app.route(‘/upload_screenshot’, methods=[‘POST’])
def upload_screenshot():
screenshot_data = request.json[‘screenshot’]
emit(‘screenshot_received’, {‘screenshot’: screenshot_data}, broadcast=True, namespace=’/’)
return “Screenshot received successfully!”
@app.route(‘/translations’)
def translations():
return render_template(‘translations.html’)
@socketio.on(‘click_coordinates’, namespace=’/’)
def handle_click_coordinates(data):
global click_coordinates
x = data[‘x’]
y = data[‘y’]
print(‘Click Coordinates – X:’, x, ‘Y:’, y)
click_coordinates = {“x”: x, “y”: y}
print(‘Click coordinates sent to client.’)
client_socket, addr = server_socket.accept()
client_socket.sendall(f”{x},{y}”.encode())
client_socket.close()
if __name__ == ‘__main__’:
socketio.run(app, debug=True, allow_unsafe_werkzeug=True, use_reloader=False)
Теперь приступим к объяснению кода сервера:
Python:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((‘127.0.0.1’, 12345))
server_socket.listen(1)
Этот код будет отвечать за создание TCP сервера
Python:
x = data[‘x’]
y = data[‘y’]
Этот код отвечает за хранение координат, которые будут получаться с HTML страницы, в которой будет JavaScript для получения этих самых координат. Об этом я расскажу чуть позже.
Python:
client_socket, addr = server_socket.accept() client_socket.sendall(f”{x},{y}”.encode()) client_socket.close()
Этот код отвечает за отправку координат на клиент через TCP сокет
Теперь рассмотрим обновление нашей HTML страницы, на которой будет определение координат относительно объекта, в котором хранится изображение, полученное с клиента.
HTML:
<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8″> <meta name=”viewport” content=”width=device-width, initial-scale=1.0″> <title>Translations</title> <script src=”https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.3.1/socket.io.js”></script> </head> <body> <div id=”image-container” style=”background-color: #ccc;”>Click here</div> <script> const socket = io(); socket.on(‘connect’, function() { console.log(‘Connected to server’); }); // Получаем фото через WebSocket и выводим его на страницу socket.on(“screenshot_received”, function(data) { const imageContainer = document.getElementById(‘image-container’); const img = new Image(); img.src = ‘data:image/jpeg;base64,’ + data.screenshot; imageContainer.innerHTML = ”; // Очищаем контейнер перед выводом нового фото imageContainer.appendChild(img); }); // Обработчик события клика document.getElementById(‘image-container’).addEventListener(‘click’, function(event) { const imageContainer = document.getElementById(‘image-container’); const rect = imageContainer.getBoundingClientRect(); const offsetX = event.clientX – rect.left; const offsetY = event.clientY – rect.top; console.log(‘Relative Coordinates – X:’, offsetX, ‘Y:’, offsetY); socket.emit(‘click_coordinates’, {‘x’: offsetX, ‘y’: offsetY}); }); </script> </body> </html>
Теперь объяснение кода:
HTML:
const offsetX = event.clientX – rect.left
Получение координат относительно X
HTML:
const offsetY = event.clientY – rect.top;
В этой строке получаем координаты относительно Y
HTML:
socket.emit(‘click_coordinates’, {‘x’: offsetX, ‘y’: offsetY});
Данная строка отвечает за отправку координат на сервер. Эти координаты как раз и будут храниться в python файле в этом коде:
Python:
x = data[‘x’] y = data[‘y’]
На этом код закончен. Теперь можно запустить сервер на Python, а затем клиент. Если все запустилось корректно, то можем перейти на страницу HTML, в моем случае это
http://127.0.0.1:5000/translations
На странице вы должны будете увидеть что-то подобное (изображение с экрана клиента):
Спойлер: Для чего нужен VNC?
Спойлер: Что можно добавить в данный проект?
Заключение:
В данной статье я показал не самый сложный вариант реализации VNC по моему мнению. Я старался сделать код как можно проще и объяснить его упрощенно, чтобы каждый понял. Есть определенные моменты, которые можно было реализовать иначе. Например, сделать отправку изображений через сокеты, а не HTTP, как у меня. Дело в том, что изначально я планировал сделать все через HTTP и статью писал по ходу написания программы. Однако, когда дело дошло до эмуляции, я понял, что без костылей через HTTP не обойтись. Поэтому было принято решение делать эмуляцию через сокеты, а отправку изображений оставить через HTTP. В общем, я надеюсь, что статья будет кому-то полезна, и вы сможете подчерпнуть для себя что-то новое из нее. Если у вас есть замечания по коду или рекомендации, буду рад их услышать и применить в следующих своих проектах.
Leave a Reply