При изчисленията виртуалните мрежови изчисления (VNC) са графична система за споделяне на работния плот, която използва Remote Framebuffer (RFB) протокол за дистанционно управление на друг компютър. Той предава събития от клавиатурата и мишката от един компютър на друг и предава графични актуализации на екрана обратно в другата посока по мрежа.
RFB е прост протокол за отдалечен достъп до графични потребителски интерфейси. Тъй като работи на ниво буфер на кадри, той е приложим за всички системи за прозорци и приложения, включително Microsoft Windows, Mac OS X и X Window System.
В тази статия ще покажа как да внедрим RFB сървърния протокол и ще демонстрирам с малко приложение Java Swing как да предавам основния прозорец през TCP връзка на VNC зрители. Идеята е да се демонстрират основни характеристики на протокола и възможна реализация в Java.
Читателят трябва да има основни познания по езика за програмиране на Java и да е запознат с основните понятия за TCP / IP мрежи, модел клиент-сървър и др. В идеалния случай, четецът е Java разработчик и има известен опит с добре познати внедрения на VNC като RealVNC, UltraVNC, TightVNC и др.
Спецификацията на RFB протокола е доста добре дефинирани . Според Wikipedia протоколът RFB има няколко версии. В тази статия фокусът ни ще бъде върху често срещаните съобщения, които трябва да се разбират правилно от повечето внедрения на VNC, независимо от версията на протокола.
След като VNC преглед (клиент) установи TCP връзка към VNC сървър (RFB услуга), първата фаза включва обмен на версия на протокола:
RFB Service ----------- 'RFB 003.003
' -------> VNC viewer RFB Service <---------- 'RFB 003.008
' -------- VNC viewer
Това е прост поток от байтове, в който може да се декодира ASCII символи , като „RFB 003.008 n“.
След като това е направено, следващата стъпка е удостоверяване. VNC сървърът изпраща масив от байтове, за да посочи какъв тип удостоверяване поддържа. Например:
RFB Service ----------- 0x01 0x02 -----------> VNC viewer RFB Service <----------- 0x02 ----------- VNC viewer
Тук VNC сървърът изпрати само 1 възможен тип удостоверяване (0x02). Първият байт 0x01 означава броя на наличните типове удостоверяване. VNC viewer трябва да отговори със стойност 0x02, тъй като това е единственият възможен тип, поддържан от сървъра в този пример.
След това сървърът ще изпрати предизвикателство за удостоверяване (в зависимост от кой алгоритъм има няколко) и клиентът трябва да отговори с правилно съобщение за отговор на предизвикателството и да изчака сървъра да потвърди отговора. След като клиентът бъде удостоверен, той може да продължи с процеса на установяване на сесия.
Най-простият начин тук е да не изберете никакво удостоверяване. RFB протоколът така или иначе е несигурен, независимо от механизма за удостоверяване. Ако сигурността е важна, правилният начин е да се тунелират RFB сесиите чрез VPN или SSH връзки.
В този момент VNC viewer изпраща споделено съобщение на работния плот, което казва дали клиентът ще сподели и ще позволи на други VNC зрители да се свържат към същия работен плот. Внедряването на услугата RFB зависи да разгледа това съобщение и евентуално да попречи на множество зрители на VNC да споделят един и същ екран. Това съобщение е с дължина само 1 байт и валидна стойност е 0x00 или 0x01.
И накрая, RFB сървърът изпраща съобщение за начало на сървъра, което съдържа размер на екрана, битове на пиксел, дълбочина, голям флаг на endian и истински цветни флагове, максимални стойности за червен, зелен и син цвят, битови позиции в пиксел за червен, зелен и син цвят и низа / заглавие на работния плот. Първите два байта представляват ширината на екрана в пиксели, следващите два байта са височината на екрана. След байтове с височина на екрана в съобщението трябва да присъстват битове на пиксел байт. Стойността обикновено е 8, 16 или 32. При повечето съвременни системи с пълен цветен диапазон, битове на пиксел байт имат стойност 32 (0x20). Той казва на клиента, че може да поиска пълен цвят за всеки пиксел от сървъра. Big endian байтът е ненулев само ако пикселите са в голям endian ред. Ако истинският цветен байт е различен от нула (true), следващите шест байта указват как да извлечете интензитета на червения, зеления и синия цвят от стойността на пиксела. Следващите шест байта са максимално допустимите стойности за червен, зелен и син компонент на пиксела. Това е важно в 8-битовия цветен режим, където са налични само няколко бита за всеки цветен компонент. Червените, зелените и сините отмествания определят битовите позиции за всеки цвят. Последните три байта се допълват и трябва да бъдат игнорирани от клиента. След пикселен формат има байт, който определя дължината на низ за заглавие на работния плот. Заглавието на работния плот е кодиран ASCII низ в байтов масив с произволна дължина.
След първоначално съобщение на сървъра, RFB услугата трябва да чете клиентски съобщения от сокета и да ги декодира. Има 6 вида съобщения:
Документацията на протокола е доста точна и обяснява всяко съобщение. За всяко съобщение се обяснява всеки байт. Например съобщение за иницииране на сървър:
Брой байтове | Тип | Описание |
---|---|---|
2 | U16 | framebuffer-width |
2 | U16 | framebuffer-height |
16. | PIXEL_FORMAT | сървър-пиксел-формат |
4 | U32 | дължина на името |
дължина на името | U8 масив | низ-име |
Тук PIXEL_FORMAT е:
Брой байтове | Тип | Описание |
---|---|---|
един | U8 | бита на пиксел |
един | U8 | дълбочина |
един | U8 | big-endian-flag |
един | U8 | true-color-flag |
2 | U16 | червен-макс |
2 | U16 | зелено-макс |
2 | U16 | синьо-макс |
един | U8 | червена смяна |
един | U8 | зелена смяна |
един | U8 | синя смяна |
3 | подплата |
U16 означава неподписано 16-битово цяло число (два байта), U32 е неподписано 32-битово цяло число, U8 масивът е масив от байтове и т.н.
Типично Java сървърно приложение се състои от една нишка, която слуша клиентски връзки, и няколко нишки, обработващи клиентски връзки.
/* * Use TCP port 5902 (display :2) as an example to listen. */ int port = 5902; ServerSocket serverSocket; serverSocket = new ServerSocket(port); /* * Limit sessions to 100. This is lazy way, if * somebody really open 100 sessions, server socket * will stop listening and no new VNC viewers will be * able to connect. */ while (rfbClientList.size() <100) { /* * Wait and accept new client. */ Socket client = serverSocket.accept(); /* * Create new object for each client. */ RFBService rfbService = new RFBService(client); /* * Add it to list. */ rfbClientList.add(rfbService); /* * Handle new client session in separate thread. */ (new Thread(rfbService, 'RFBService' + rfbClientList.size())).start(); }
Тук е избран TCP порт 5902 (дисплей: 2) и цикълът while чака клиент да се свърже. Метод ServerSocket.accept () блокира и кара нишката да чака нова клиентска връзка. След като клиентът се свърже, се създава нова нишка RFBService, която обработва съобщения за RFB протокол, получени от клиента.
Class RFBService изпълнява интерфейс Runnable. Пълно е с методи за четене на байтове от сокет. Метод тичам () е важно, което се изпълнява веднага, когато нишката се стартира в края на цикъла:
@Override public void run() { try { /* * RFB server has to send protocol version string first. * And wait for VNC viewer to replay with * protocol version string. */ sendProtocolVersion(); String protocolVer = readProtocolVersion(); if (!protocolVer.startsWith('RFB')) { throw new IOException(); }
Тук метод sendProtocolVersion () изпраща RFB низ към клиента (VNC viewer) и след това чете низ от версия на протокола от клиента. Клиентът трябва да отговори с нещо като „RFB 003.008 n“. Метод readProtocolVersion () разбира се блокира, като всеки метод, чието име започва с прочетената дума.
private String readProtocolVersion() throws IOException { byte[] buffer = readU8Array(12); return new String(buffer); }
Методът readProtocolVersion () е прост: той чете 12 байта от сокета и връща стойност на низ. Функция readU8Array (int) чете определен брой байтове, в този случай 12 байта. Ако няма достатъчно байтове за четене в сокета, той изчаква:
private byte[] readU8Array(int len) throws IOException { byte[] buffer = new byte[len]; int offset = 0, left = buffer.length; while (offset Подобен на readU8Array (int) , методи readU16int () и readU32int () съществуват, които четат байтове от сокет и връщат цяло число.
След като изпрати версията на протокола и прочете отговора, услугата RFB трябва да изпрати съобщение за защита:
/* * RFB server sends security type bytes that may request * a user to type password. * In this implementation, this is set to simples * possible option: no authentication at all. */ sendSecurityType();
При това изпълнение е избран най-простият начин: не се изисква парола от страна на клиента на VNC.
private void sendSecurityType() throws IOException { out.write(SECURITY_TYPE); out.flush(); }
където SECURITY_TYPE е байтов масив:
private final byte[] SECURITY_TYPE = {0x00, 0x00, 0x00, 0x01};
Този масив от байтове от RFB протокол версия 3.3 означава, че VNC viewer не трябва да изпраща никаква парола.
След това това, което RFB услугата трябва да получи от клиента, е споделен флаг на работния плот. Това е един байт в сокета.
/* * RFB server reads shared desktop flag. It's a single * byte that tells RFB server * should it support multiple VNC viewers connected at * same time or not. */ byte sharedDesktop = readSharedDesktop();
След като флагът за споделен работен плот бъде прочетен от сокета, ние го игнорираме в нашата реализация.
RFB услугата трябва да изпрати първоначално съобщение на сървъра:
/* * RFB server sends ServerInit message that includes * screen resolution, * number of colors, depth, screen title, etc. */ screenWidth = JFrameMainWindow.jFrameMainWindow.getWidth(); screenHeight = JFrameMainWindow.jFrameMainWindow.getHeight(); String windowTitle = JFrameMainWindow.jFrameMainWindow.getTitle(); sendServerInit(screenWidth, screenHeight, windowTitle);
Клас JFrameMainWindow е JFrame, който е тук с демонстрационна цел като източник на графики. Съобщението за инициализация на сървъра има задължителна ширина и височина на екрана в пиксели и заглавие на работния плот. В този пример това е заглавието на JFrame, получено по метода getTitle ().
След съобщение за иницииране на сървъра, нишката на услугата RFB цикли, като чете от сокет шест вида съобщения:
/* * Main loop where clients messages are read from socket. */ while (true) { /* * Mark first byte and read it. */ in.mark(1); int messageType = in.read(); if (messageType == -1) { break; } /* * Go one byte back. */ in.reset(); /* * Depending on message type, read complete message on socket. */ if (messageType == 0) { /* * Set Pixel Format */ readSetPixelFormat(); } else if (messageType == 2) { /* * Set Encodings */ readSetEncoding(); } else if (messageType == 3) { /* * Frame Buffer Update Request */ readFrameBufferUpdateRequest(); } else if (messageType == 4) { /* * Key Event */ readKeyEvent(); } else if (messageType == 5) { /* * Pointer Event */ readPointerEvent(); } else if (messageType == 6) { /* * Client Cut Text */ readClientCutText(); } else { err('Unknown message type. Received message type = ' + messageType); } }
Всеки метод readSetPixelFormat () , readSetEncoding () , readFrameBufferUpdateRequest () , ... readClientCutText () блокира и задейства някакво действие.
Например, readClientCutText () метод чете текст, който е кодиран в съобщение, когато потребителят изрязва текст от страна на клиента и след това VNC viewer изпраща текст чрез RFB протокол към сървъра. След това текстът се поставя от страна на сървъра в клипборда.
Клиентски съобщения
Всички шест съобщения трябва да се поддържат от услугата RFB, поне на ниво байт: когато клиентът изпраща съобщение, трябва да се прочете пълната дължина на байта. Това е така, защото RFB протоколът е ориентиран към байтове и няма граница между две съобщения.
Най-голямото съобщение за импортиране е заявка за актуализация на буфер на рамка. Клиентът може да поиска пълна актуализация или допълнителна актуализация на екрана.
private void readFrameBufferUpdateRequest() throws IOException { int messageType = in.read(); int incremental = in.read(); if (messageType == 0x03) { int x_pos = readU16int(); int y_pos = readU16int(); int width = readU16int(); int height = readU16int(); screenWidth = width; screenHeight = height; if (incremental == 0x00) { incrementalFrameBufferUpdate = false; int x = JFrameMainWindow.jFrameMainWindow.getX(); int y = JFrameMainWindow.jFrameMainWindow.getY(); RobotScreen.robo.getScreenshot(x, y, width, height); sendFrameBufferUpdate(x_pos, y_pos, width, height, 0, RobotScreen.robo.getColorImageBuffer()); } else if (incremental == 0x01) { incrementalFrameBufferUpdate = true; } else { throw new IOException(); } } else { throw new IOException(); } }
Първият байт на съобщението за искане на буфер на кадър е тип съобщение. Стойността винаги е 0x03. Следващият байт е инкрементален флаг, който казва на сървъра да изпрати пълен кадър или просто разлика. В случай на пълна заявка за актуализация, RFB услугата ще направи екранна снимка на главния прозорец с помощта на класа RobotScreen и ще я изпрати на клиента.
Ако това е допълнителна заявка, флаг incrementalFrameBufferUpdate ще бъде зададено на истина. Този флаг ще се използва от компонентите на Swing, за да се провери дали трябва да изпращат части от екрана, които са се променили. Обикновено JMenu, JMenuItem, JTextArea и др. Трябва да правят постепенно актуализиране на екрана, когато потребителят премества показалеца на мишката, щраква, изпраща натискане на клавиш и т.н.
Метод sendFrameBufferUpdate (int, int, int, int, int []) изтрива буфера на изображението в сокет.
public void sendFrameBufferUpdate(int x, int y, int width, int height, int encodingType, int[] screen) throws IOException { if (x + width > screenWidth || y + height > screenHeight) { err ('Invalid frame update size:'); err (' x = ' + x + ', y = ' + y); err (' width = ' + width + ', height = ' + height); return; } byte messageType = 0x00; byte padding = 0x00; out.write(messageType); out.write(padding); int numberOfRectangles = 1; writeU16int(numberOfRectangles); writeU16int(x); writeU16int(y); writeU16int(width); writeU16int(height); writeS32int(encodingType); for (int rgbValue : screen) { int red = (rgbValue & 0x000000FF); int green = (rgbValue & 0x0000FF00) >> 8; int blue = (rgbValue & 0x00FF0000) >> 16; if (bits_per_pixel == 8) { out.write((byte) colorMap.get8bitPixelValue(red, green, blue)); } else { out.write(red); out.write(green); out.write(blue); out.write(0); } } out.flush(); }
Методът проверява дали координатите (x, y) не излизат от екрана заедно с ширината x височината на буфера на изображението. Стойността на типа съобщение за актуализация на буфер на рамка е 0x00. Стойността на уплътняването обикновено е 0x00 и трябва да се игнорира от VNC viewer. Броят на правоъгълниците е двубайтова стойност и определя колко правоъгълници следват в съобщението.
Всеки правоъгълник има горна лява координата, ширина и височина, тип кодиране и пикселни данни. Има някои ефективни формати за кодиране, които могат да се използват, като zrle, hextile и tight. За да запазим нещата прости и лесни за разбиране, ще използваме сурово кодиране при нашето изпълнение.
Суровото кодиране означава, че цветът на пикселите се предава като RGB компонент. Ако клиентът е задал пикселно кодиране като 32-битово, тогава 4 байта се предават за всеки пиксел. Ако клиентът използва 8-битов цветен режим, тогава всеки пиксел се предава като 1 байт. Кодът е показан в for-loop. Имайте предвид, че за 8-битов режим се използва цветна карта за намиране на най-доброто съвпадение за всеки пиксел от екрана / буфера на изображението. За 32-битов пикселен режим буферът за изображения съдържа масив от цели числа, всяка стойност има мултиплексирани RGB компоненти.
Демонстрационно приложение Swing
Демонстрационното приложение Swing съдържа слушател на действия, който задейства sendFrameBufferUpdate (int, int, int, int, int []) метод. Обикновено елементите на приложението, като Swing компонентите, трябва да имат слушатели и да изпращат промяна на екрана на клиента. Например, когато потребителят напише нещо в JTextArea, то трябва да бъде предадено на VNC viewer.
public void actionPerformed(ActionEvent arg0) { /* * Get dimensions and location of main JFrame window. */ int offsetX = JFrameMainWindow.jFrameMainWindow.getX(); int offsetY = JFrameMainWindow.jFrameMainWindow.getY(); int width = JFrameMainWindow.jFrameMainWindow.getWidth(); int height = JFrameMainWindow.jFrameMainWindow.getHeight(); /* * Do not update screen if main window dimension has changed. * Upon main window resize, another action listener will * take action. */ int screenWidth = RFBDemo.rfbClientList.get(0).screenWidth; int screenHeight = RFBDemo.rfbClientList.get(0).screenHeight; if (width != screenWidth || height != screenHeight) { return; } /* * Capture new screenshot into image buffer. */ RobotScreen.robo.getScreenshot(offsetX, offsetY, width, height); int[] delta = RobotScreen.robo.getDeltaImageBuffer(); if (delta == null) { offsetX = 0; offsetY = 0; Iterator it = RFBDemo.rfbClientList.iterator(); while (it.hasNext()) { RFBService rfbClient = it.next(); if (rfbClient.incrementalFrameBufferUpdate) { try { /* * Send complete window. */ rfbClient.sendFrameBufferUpdate( offsetX, offsetY, width, height, 0, RobotScreen.robo.getColorImageBuffer()); } catch (SocketException ex) { it.remove(); } catch (IOException ex) { ex.printStackTrace(); it.remove(); } rfbClient.incrementalFrameBufferUpdate = false; } } } else { offsetX = RobotScreen.robo.getDeltaX(); offsetY = RobotScreen.robo.getDeltaY(); width = RobotScreen.robo.getDeltaWidth(); height = RobotScreen.robo.getDeltaHeight(); Iterator it = RFBDemo.rfbClientList.iterator(); while (it.hasNext()) { RFBService rfbClient = it.next(); if (rfbClient.incrementalFrameBufferUpdate) { try { /* * Send only delta rectangle. */ rfbClient.sendFrameBufferUpdate( offsetX, offsetY, width, height, 0, delta); } catch (SocketException ex) { it.remove(); } catch (IOException ex) { ex.printStackTrace(); it.remove(); } rfbClient.incrementalFrameBufferUpdate = false; } } } }
Кодът на този екшън слушател е доста прост: прави екранна снимка на главния прозорец JFrameMain с помощта на клас RobotScreen, след което се определя дали е необходима частична актуализация на екрана. Променлива diffUpdateOfScreen се използва като флаг за частична актуализация. И накрая пълният буфер на изображението или само различни редове се предават на клиента. Този код също така разглежда повече свързани клиенти, поради което се използва итератор и се поддържа списък с клиенти RFBDemo.rfbClientList член.
Слушателят за действие за актуализиране на Framebuffer може да се използва в таймера, който може да бъде стартиран от всяка промяна на JComponent:
/* * Define timer for frame buffer update with 400 ms delay and * no repeat. */ timerUpdateFrameBuffer = new Timer(400, new ActionListenerFrameBufferUpdate()); timerUpdateFrameBuffer.setRepeats(false);
Този код е в конструктора на класа JFrameMainWindow. Таймерът се стартира в метода doIncrementalFrameBufferUpdate ():
public void doIncrementalFrameBufferUpdate() { if (RFBDemo.rfbClientList.size() == 0) { return; } if (!timerUpdateFrameBuffer.isRunning()) { timerUpdateFrameBuffer.start(); } }
Други слушатели на действия обикновено извикват метода doIncrementalFrameBufferUpdate ():
public class DocumentListenerChange implements DocumentListener { @Override public void changedUpdate(DocumentEvent e) { JFrameMainWindow jFrameMainWindow = JFrameMainWindow.jFrameMainWindow; jFrameMainWindow.doIncrementalFrameBufferUpdate(); } // ... }
Този начин трябва да бъде лесен и лесен за следване. Необходима е само препратка към екземпляр JFrameMainWindow и едно извикване на doIncrementalFrameBufferUpdate () метод. Методът ще провери дали има свързани клиенти и дали има таймер timerUpdateFrameBuffer ще бъде стартиран. След като таймерът е стартиран, слушателят на действията всъщност ще направи екранна снимка и sendFrameBufferUpdate () се изпълнява.

Фигурата по-горе показва връзката на слушателя с процедурата за актуализиране на буфера на кадрите. Повечето слушатели се задействат, когато потребителят извърши действие: щракне, избере текст, напише нещо в текстовата област и т.н. След това функция член doIncrementalFramebufferUpdate () се изпълнява, което стартира таймера timerUpdateFrameBuffer . Таймерът в крайна сметка ще се обади sendFrameBufferUpdate () метод в клас RFBService и това ще доведе до актуализация на екрана от страна на клиента (VNC viewer).
Заснемане на екран, възпроизвеждане на клавиши и преместване на показалеца на мишката на екрана
Java има вграден клас Robot, който позволява на разработчика да напише приложение, което ще вземе екранни снимки, изпраща ключове, манипулира показалеца на мишката, произвежда кликвания и т.н.
За да вземете област от екрана, където се показва прозорецът JFrame, се използва RobotScreen. Основният метод е getScreenshot (int, int, int, int) който улавя област на екрана. RGB стойностите за всеки пиксел се съхраняват в масив int []:
public void getScreenshot(int x, int y, int width, int height) { Rectangle screenRect = new Rectangle(x, y, width, height); BufferedImage colorImage = robot.createScreenCapture(screenRect); previousImageBuffer = colorImageBuffer; colorImageBuffer = ((DataBufferInt) colorImage.getRaster().getDataBuffer()).getData(); if (previousImageBuffer == null || previousImageBuffer.length != colorImageBuffer.length) { previousImageBuffer = colorImageBuffer; } this.width = width; this.height = height; }
Методът съхранява пикселите в масива colorImageBuffer. За да получите пикселни данни, getColorImageBuffer () метод може да се използва.
Методът също така запазва предишния буфер на изображението. Възможно е да получите само пиксели, които са били променени. За да получите само разлика в областта на изображението, използвайте метода getDeltaImageBuffer () .
Изпращането на натискания на клавиши в системата е лесно с клас Robot. Въпреки това, някои специални ключови кодове, получени от зрителите на VNC, трябва първо да бъдат преведени правилно. Класът RobotKeyboard има метод sendKey (int, int) който обработва специални клавиши и буквено-цифрови клавиши:
public void sendKey(int keyCode, int state) { switch (keyCode) { case 0xff08: doType(VK_BACK_SPACE, state); break; case 0xff09: doType(VK_TAB, state); break; case 0xff0d: case 0xff8d: doType(VK_ENTER, state); break; case 0xff1b: doType(VK_ESCAPE, state); break; … case 0xffe1: case 0xffe2: doType(VK_SHIFT, state); break; case 0xffe3: case 0xffe4: doType(VK_CONTROL, state); break; case 0xffe9: case 0xffea: doType(VK_ALT, state); break; default: /* * Translation of a..z keys. */ if (keyCode >= 97 && keyCode <= 122) { /* * Turn lower-case a..z key codes into upper-case A..Z key codes. */ keyCode = keyCode - 32; } doType(keyCode, state); } }
Състоянието на аргумента определя дали бутонът е натиснат или освободен. След правилен превод на ключовия код в VT константа, метод doType (int, int) предава стойността на ключа на робот и ефектът е същият като локалния потребител е натиснал клавиша на клавиатурата:
private void doType(int keyCode, int state) { if (state == 0) { robot.keyRelease(keyCode); } else { robot.keyPress(keyCode); } }
Подобно на RobotKeyboard е класът RobotMouse, който обработва събития на показалеца и кара показалеца на мишката да се движи и щраква.
public void mouseMove(int x, int y) { robot.mouseMove(x, y); }
И трите класа RobotScreen, RobotMouse и RobotKeyboard разпределят нов екземпляр на Robot в конструктор:
this.robot = new Robot();
Имаме само по един екземпляр от всеки, тъй като няма нужда на ниво приложение да има повече от един екземпляр на клас RobotScreen, RobotMouse или RobotKeyboard.
public static void main(String[] args) { ... /* * Initialize static Robot objects for screen, keyboard and mouse. */ RobotScreen.robo = new RobotScreen(); RobotKeyboard.robo = new RobotKeyboard(); RobotMouse.robo = new RobotMouse(); ... }
В това демо приложение тези копия са създадени в главен () функция.
Резултатът е базирано на Swing приложение в Java, което действа като доставчик на RFB услуги и позволява на стандартните VNC зрители да се свържат с него:

Заключение
RFB протоколът е широко използван и приет. Клиентски внедрения под формата на VNC зрители съществуват за почти всички платформи и устройства. Основната цел е отдалечено показване на настолни компютри, но може да има и други приложения. Например можете да създадете изящни графични инструменти и да имате достъп до тях от разстояние, за да подобрите съществуващите си отдалечени работни потоци .
Тази статия обхваща основите на RFB протокола, формата на съобщението, как да изпратите част от екрана и как да се справите с клавиатурата и мишката. Пълният изходен код с демо приложението Swing е на разположение на GitHub .