Implementacja zdalnego serwera bufora ramki w Javie

Opublikowany: 2022-03-11

W informatyce Virtual Network Computing (VNC) to graficzny system udostępniania pulpitu, który wykorzystuje protokół Remote Framebuffer (RFB) do zdalnego sterowania innym komputerem. Przesyła zdarzenia klawiatury i myszy z jednego komputera do drugiego i przekazuje aktualizacje graficznego ekranu z powrotem w innym kierunku przez sieć.

RFB to prosty protokół zdalnego dostępu do graficznych interfejsów użytkownika. Ponieważ działa na poziomie bufora ramki, ma zastosowanie do wszystkich systemów i aplikacji okienkowych, w tym Microsoft Windows, Mac OS X i X Window System.

Budowanie opartej na protokole serwera aplikacji Swing Remote Framebuffer w języku Java

Budowanie opartej na protokole serwera aplikacji Swing Remote Framebuffer w języku Java
Ćwierkać

W tym artykule pokażę, jak zaimplementować protokół RFB po stronie serwera i zademonstruję za pomocą małej aplikacji Java Swing, jak przesłać główne okno przez połączenie TCP do przeglądarek VNC. Ideą jest zademonstrowanie podstawowych cech protokołu i możliwości implementacji w Javie.

Czytelnik powinien mieć podstawową wiedzę na temat języka programowania Java i powinien znać podstawowe pojęcia dotyczące sieci TCP/IP, modelu klient-serwer itp. Idealnie byłoby, gdyby czytelnik był programistą Java i miał pewne doświadczenie z dobrze znanymi implementacjami VNC, takimi jak RealVNC , UltraVNC, TightVNC itp.

Specyfikacja protokołu zdalnego bufora ramki

Specyfikacja protokołu RFB jest dość dobrze zdefiniowana. Według Wikipedii protokół RFB ma kilka wersji. W tym artykule skupimy się na typowych komunikatach, które powinny być poprawnie zrozumiane przez większość implementacji VNC, niezależnie od wersji protokołu.

Po nawiązaniu przez przeglądarkę VNC (klient) połączenia TCP z serwerem VNC (usługa RFB), pierwsza faza obejmuje wymianę wersji protokołu:

 RFB Service ----------- "RFB 003.003\n" -------> VNC viewer RFB Service <---------- "RFB 003.008\n" -------- VNC viewer

Jest to prosty strumień bajtów, który można zdekodować na znaki ASCII, takie jak „RFB 003.008\n”.

Gdy to zrobisz, następnym krokiem jest uwierzytelnienie. Serwer VNC wysyła tablicę bajtów, aby wskazać typ uwierzytelniania, który obsługuje. Na przykład:

 RFB Service ----------- 0x01 0x02 -----------> VNC viewer RFB Service <----------- 0x02 ----------- VNC viewer

Tutaj serwer VNC wysłał tylko 1 możliwy typ uwierzytelniania (0x02). Pierwszy bajt 0x01 oznacza liczbę dostępnych typów uwierzytelniania. Przeglądarka VNC musi odpowiedzieć wartością 0x02, ponieważ jest to jedyny możliwy typ obsługiwany przez serwer w tym przykładzie.

Następnie serwer wyśle ​​wyzwanie uwierzytelnienia (w zależności od algorytmu jest ich kilka), a klient musi odpowiedzieć odpowiednią wiadomością typu challenge-response i poczekać na potwierdzenie odpowiedzi przez serwer. Po uwierzytelnieniu klient może kontynuować proces nawiązywania sesji.

Najprostszym sposobem jest tutaj całkowity brak uwierzytelniania. Protokół RFB i tak jest niezabezpieczony, niezależnie od mechanizmu uwierzytelniania. Jeśli bezpieczeństwo jest ważne, właściwym sposobem byłoby tunelowanie sesji RFB przez połączenia VPN lub SSH.

W tym momencie przeglądarka VNC wysyła wiadomość współdzielonego pulpitu, która informuje, czy klient będzie udostępniał i zezwalał innym przeglądarkom VNC na łączenie się z tym samym pulpitem. Od implementacji usługi RFB zależy uwzględnienie tego komunikatu i prawdopodobnie uniemożliwienie wielu przeglądającym VNC współużytkowania tego samego ekranu. Ta wiadomość ma długość tylko 1 bajta, a poprawną wartością jest 0x00 lub 0x01.

Wreszcie serwer RFB wysyła wiadomość init serwera, która zawiera wymiary ekranu, bity na piksel, głębię, flagi big endian i flagi true color, maksymalne wartości dla kolorów czerwonego, zielonego i niebieskiego, pozycje bitów w pikselach dla kolorów czerwonego, zielonego i niebieskiego i napis/tytuł na pulpicie. Pierwsze dwa bajty reprezentują szerokość ekranu w pikselach, kolejne dwa bajty to wysokość ekranu. Po bajtach wysokości ekranu w wiadomości powinny znajdować się bity na piksel. Wartość wynosi zwykle 8, 16 lub 32. W większości nowoczesnych systemów z pełnym zakresem kolorów, bity na bajt piksela mają wartość 32 (0x20). Mówi klientowi, że może zażądać pełnego koloru dla każdego piksela z serwera. Bajt big endian jest niezerowy tylko wtedy, gdy piksele są w kolejności big endian. Jeśli bajt true color jest niezerowy (true), następne sześć bajtów określa sposób wyodrębnienia intensywności koloru czerwonego, zielonego i niebieskiego z wartości piksela. Kolejne sześć bajtów to maksymalne dozwolone wartości dla czerwonej, zielonej i niebieskiej składowej piksela. Jest to ważne w 8-bitowym trybie kolorów, w którym dla każdego składnika koloru dostępnych jest tylko kilka bitów. Przesunięcia w kolorze czerwonym, zielonym i niebieskim określają pozycje bitów dla każdego koloru. Ostatnie trzy bajty są dopełniane i powinny zostać zignorowane przez klienta. Po formacie pikselowym znajduje się bajt określający długość ciągu znaków dla tytułu pulpitu. Tytuł pulpitu to ciąg znaków zakodowany w ASCII w tablicy bajtów o dowolnej długości.

Protokół serwer-klient zdalnego bufora ramki: wymiana wersji, uwierzytelnianie i wiadomość inicjująca serwera

Protokół serwer-klient zdalnego bufora ramki: wymiana wersji, uwierzytelnianie i wiadomość inicjująca serwera
Ćwierkać

Po komunikacie inicjującym serwer, usługa RFB powinna odczytać komunikaty klienta z gniazda i zdekodować je. Istnieje 6 rodzajów wiadomości:

  • Ustaw format pikseli
  • Ustaw kodowanie
  • Żądanie aktualizacji bufora ramki
  • Ważne wydarżenie
  • WskaźnikZdarzenie
  • KlientCutText

Dokumentacja protokołu jest dość dokładna i wyjaśnia każdą wiadomość. Dla każdej wiadomości wyjaśniony jest każdy bajt. Na przykład wiadomość inicjująca serwera:

Liczba bajtów Rodzaj Opis
2 U16 szerokość bufora ramki
2 U16 wysokość bufora ramki
16 PIXEL_FORMAT format-piksela-serwera
4 U32 nazwa-długość
nazwa-długość Tablica U8 nazwa-string

Tutaj PIXEL_FORMAT to:

Liczba bajtów Rodzaj Opis
1 U8 bity na piksel
1 U8 głębokość
1 U8 big-endian-flaga
1 U8 flaga-prawdziwego koloru
2 U16 red-max
2 U16 zielony-max
2 U16 niebieski-max
1 U8 przesunięcie ku czerwieni
1 U8 zielone przesunięcie
1 U8 niebieskie przesunięcie
3 wyściółka

U16 oznacza 16-bitową liczbę całkowitą bez znaku (dwa bajty), U32 jest 32-bitową liczbą całkowitą bez znaku, tablica U8 to tablica bajtów itp.

Implementacja protokołu w Javie

Typowa aplikacja serwera Java składa się z jednego wątku nasłuchującego połączeń klientów i kilku wątków obsługujących połączenia klientów.

 /* * 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(); }

Tutaj został wybrany port TCP 5902 (wyświetlanie :2), a pętla while czeka na połączenie klienta. Metoda ServerSocket.accept() blokuje i powoduje, że wątek czeka na nowe połączenie klienta. Gdy klient się połączy, tworzony jest nowy wątek RFBService, który obsługuje komunikaty protokołu RFB otrzymane od klienta.

Klasa RFBService implementuje interfejs Runnable. Jest pełen metod odczytu bajtów z gniazda. Ważna jest metoda run() , która jest wykonywana natychmiast po uruchomieniu wątku na końcu pętli:

 @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(); }

Tutaj metoda sendProtocolVersion() wysyła ciąg RFB do klienta (przeglądarki VNC), a następnie odczytuje ciąg wersji protokołu od klienta. Klient powinien odpowiedzieć czymś w rodzaju „RFB 003.008\n”. Metoda readProtocolVersion() jest oczywiście blokująca, jak każda metoda, której nazwa zaczyna się od słowa read.

 private String readProtocolVersion() throws IOException { byte[] buffer = readU8Array(12); return new String(buffer); }

Metoda readProtocolVersion() jest prosta: odczytuje 12 bajtów z gniazda i zwraca wartość ciągu. Funkcja readU8Array(int) odczytuje określoną liczbę bajtów, w tym przypadku 12 bajtów. Jeśli nie ma wystarczającej liczby bajtów do odczytania w gnieździe, czeka:

 private byte[] readU8Array(int len) throws IOException { byte[] buffer = new byte[len]; int offset = 0, left = buffer.length; while (offset < buffer.length) { int numOfBytesRead = 0; numOfBytesRead = in.read(buffer, offset, left); offset = offset + numOfBytesRead; left = left - numOfBytesRead; } return buffer; }

Podobnie jak readU8Array(int) , istnieją metody readU16int() i readU32int() , które odczytują bajty z gniazda i zwracają wartość całkowitą.

Po wysłaniu wersji protokołu i odczytaniu odpowiedzi serwis RFB powinien wysłać komunikat bezpieczeństwa:

 /* * 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();

W tej implementacji wybierany jest najprostszy sposób: nie wymagaj żadnego hasła od strony klienta VNC.

 private void sendSecurityType() throws IOException { out.write(SECURITY_TYPE); out.flush(); }

gdzie SECURITY_TYPE to tablica bajtów:

 private final byte[] SECURITY_TYPE = {0x00, 0x00, 0x00, 0x01};

Ta tablica bajtów protokołu RFB w wersji 3.3 oznacza, że ​​przeglądarka VNC nie musi wysyłać żadnego hasła.

Następnie to, co usługa RFB powinna otrzymać od klienta, to flaga współdzielonego pulpitu. To jeden bajt na gnieździe.

 /* * 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();

Gdy flaga współdzielonego pulpitu zostanie odczytana z gniazda, ignorujemy ją w naszej implementacji.

Usługa RFB musi wysłać wiadomość inicjującą serwer:

 /* * 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);

Klasa JFrameMainWindow to JFrame, która jest tutaj w celach demonstracyjnych jako źródło grafiki. Wiadomość init serwera ma obowiązkową szerokość i wysokość ekranu w pikselach oraz tytuł pulpitu. W tym przykładzie jest to tytuł JFrame uzyskany metodą getTitle().

Po komunikacie inicjującym serwer, usługa RFB zapętla pętle wątków, odczytując z gniazda sześć typów komunikatów:

 /* * 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); } }

Każda metoda readSetPixelFormat() , readSetEncoding() , readFrameBufferUpdateRequest() , … readClientCutText() blokuje i wyzwala jakąś akcję.

Na przykład metoda readClientCutText() odczytuje tekst, który jest zakodowany w wiadomości, gdy użytkownik wycina tekst po stronie klienta, a następnie przeglądarka VNC wysyła tekst za pośrednictwem protokołu RFB do serwera. Tekst jest następnie umieszczany po stronie serwera w schowku.

Wiadomości klienta

Wszystkie sześć wiadomości musi być obsługiwanych przez usługę RFB, przynajmniej na poziomie bajtów: gdy klient wysyła wiadomość, musi zostać odczytana pełna długość bajtów. Dzieje się tak, ponieważ protokół RFB jest zorientowany bajtowo i nie ma granicy między dwoma komunikatami.

Najbardziej importowanym komunikatem jest żądanie aktualizacji bufora ramki. Klient może zażądać pełnej lub przyrostowej aktualizacji ekranu.

 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(); } }

Pierwszy bajt komunikatu żądania bufora ramki to typ komunikatu. Wartość to zawsze 0x03. Następny bajt to flaga incremental, która mówi serwerowi, aby wysłał pełną klatkę lub tylko różnicę. W przypadku żądania pełnej aktualizacji, usługa RFB wykona zrzut ekranu głównego okna za pomocą klasy RobotScreen i wyśle ​​go do klienta.

Jeśli jest to żądanie przyrostowe, flaga incrementalFrameBufferUpdate zostanie ustawiona na wartość true. Ta flaga będzie używana przez komponenty Swinga do sprawdzenia, czy muszą wysłać zmienione części ekranu. Zwykle JMenu, JMenuItem, JTextArea itp. muszą wykonywać przyrostową aktualizację ekranu, gdy użytkownik porusza wskaźnikiem myszy, klika, wysyła naciśnięcie klawisza itp.

Metoda sendFrameBufferUpdate(int, int, int, int, int[]) opróżnia bufor obrazu do gniazda.

 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(); }

Metoda sprawdza, czy współrzędne (x, y) nie znikają z ekranu razem z szerokością x wysokością bufora obrazu. Wartość typu wiadomości dla aktualizacji bufora ramki to 0x00. Wartość dopełnienia wynosi zwykle 0x00 i powinna być ignorowana przez przeglądarkę VNC. Liczba prostokątów jest wartością dwubajtową i określa, ile prostokątów występuje w wiadomości.

Każdy prostokąt ma górną lewą współrzędną, szerokość i wysokość, typ kodowania i dane pikseli. Istnieje kilka wydajnych formatów kodowania, których można użyć, takich jak zrle, hextile i tight. Jednak, aby wszystko było proste i łatwe do zrozumienia, w naszej implementacji użyjemy surowego kodowania.

Kodowanie surowe oznacza, że ​​kolor pikseli jest przesyłany jako składnik RGB. Jeśli klient ustawił kodowanie pikseli na 32-bitowe, to dla każdego piksela przesyłane są 4 bajty. Jeśli klient korzysta z 8-bitowego trybu kolorów, każdy piksel jest przesyłany jako 1 bajt. Kod jest wyświetlany w pętli for. Zwróć uwagę, że w trybie 8-bitowym do znalezienia najlepszego dopasowania dla każdego piksela ze zrzutu ekranu/bufora obrazu używana jest mapa kolorów. W trybie pikseli 32-bitowych bufor obrazu zawiera tablicę liczb całkowitych, a każda wartość ma zmultipleksowane składowe RGB.

Aplikacja Demo Swing

Aplikacja demo Swing zawiera detektor akcji, który wyzwala metodę sendFrameBufferUpdate(int, int, int, int, int[]) . Zazwyczaj elementy aplikacji, takie jak komponenty Swing, powinny mieć detektory i wysyłać zmiany ekranu do klienta. Na przykład, gdy użytkownik wpisuje coś w JTextArea, powinno to zostać przesłane do przeglądarki VNC.

 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<RFBService> 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<RFBService> 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; } } } }

Kod tego detektora akcji jest dość prosty: wykonuje zrzut ekranu głównego okna JFrameMain przy użyciu klasy RobotScreen, a następnie określa, czy potrzebna jest częściowa aktualizacja ekranu. Zmienna diffUpdateOfScreen jest używana jako flaga dla częściowej aktualizacji. I na koniec do klienta przesyłany jest cały bufor obrazu lub tylko różne wiersze. Ten kod uwzględnia również większą liczbę podłączonych klientów, dlatego używany jest iterator, a lista klientów jest utrzymywana w elemencie RFBDemo.rfbClientList<RFBService> .

Odbiornik akcji aktualizacji bufora ramki może być użyty w Timerze, który może zostać uruchomiony przez dowolną zmianę JComponent:

 /* * Define timer for frame buffer update with 400 ms delay and * no repeat. */ timerUpdateFrameBuffer = new Timer(400, new ActionListenerFrameBufferUpdate()); timerUpdateFrameBuffer.setRepeats(false);

Ten kod znajduje się w konstruktorze klasy JFrameMainWindow. Timer jest uruchamiany w metodzie doIncrementalFrameBufferUpdate():

 public void doIncrementalFrameBufferUpdate() { if (RFBDemo.rfbClientList.size() == 0) { return; } if (!timerUpdateFrameBuffer.isRunning()) { timerUpdateFrameBuffer.start(); } }

Inne detektory akcji zwykle wywołują metodę doIncrementalFrameBufferUpdate() :

 public class DocumentListenerChange implements DocumentListener { @Override public void changedUpdate(DocumentEvent e) { JFrameMainWindow jFrameMainWindow = JFrameMainWindow.jFrameMainWindow; jFrameMainWindow.doIncrementalFrameBufferUpdate(); } // ... }

Ta droga powinna być prosta i łatwa do naśladowania. Potrzebne jest tylko odwołanie do instancji JFrameMainWindow i pojedyncze wywołanie metody doIncrementalFrameBufferUpdate() . Metoda sprawdzi, czy są połączeni klienci, a jeśli są, uruchomi się timer timerUpdateFrameBuffer . Po uruchomieniu licznika, program nasłuchujący akcji wykona zrzut ekranu i zostanie wykonane sendFrameBufferUpdate() .

Powyższy rysunek przedstawia stosunek słuchacza do procedury aktualizacji bufora ramki. Większość detektorów jest wyzwalana, gdy użytkownik wykonuje akcję: klika, zaznacza tekst, wpisuje coś w obszarze tekstowym itp. Następnie wykonywana jest funkcja członkowska doIncrementalFramebufferUpdate() , która uruchamia timer timerUpdateFrameBuffer . Timer w końcu wywoła metodę sendFrameBufferUpdate() w klasie RFBService i spowoduje aktualizację ekranu po stronie klienta (przeglądarka VNC).

Przechwytywanie ekranu, odtwarzanie naciśnięć klawiszy i przesuwanie wskaźnika myszy na ekranie

Java ma wbudowaną klasę Robot, która umożliwia programiście napisanie aplikacji, która będzie pobierać zrzuty ekranu, wysyłać klucze, manipulować wskaźnikiem myszy, wytwarzać kliknięcia itp.

Aby uchwycić obszar ekranu, na którym wyświetlane jest okno JFrame, używany jest RobotScreen. Główną metodą jest getScreenshot(int, int, int, int), która przechwytuje obszar ekranu. Wartości RGB dla każdego piksela są przechowywane w tablicy 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; }

Metoda przechowuje piksele w tablicy colorImageBuffer. Aby uzyskać dane pikseli, można użyć metody getColorImageBuffer() .

Metoda zapisuje również poprzedni bufor obrazu. Możliwe jest uzyskanie tylko zmienionych pikseli. Aby uzyskać tylko różnicę obszaru obrazu, użyj metody getDeltaImageBuffer() .

Wysyłanie naciśnięć klawiszy do systemu jest łatwe dzięki klasie Robot. Jednak niektóre specjalne kody kluczy otrzymane od przeglądarek VNC muszą być najpierw poprawnie przetłumaczone. Klasa RobotKeyboard posiada metodę sendKey(int, int) , która obsługuje klawisze specjalne i alfanumeryczne:

 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); } }

Stan argumentu określa, czy klawisz jest naciśnięty, czy zwolniony. Po poprawnym przetłumaczeniu kodu klucza na stałą VT, metoda doType(int, int) przekazuje wartość klucza do Robota i efekt jest taki sam, jak lokalny użytkownik nacisnął klawisz na klawiaturze:

 private void doType(int keyCode, int state) { if (state == 0) { robot.keyRelease(keyCode); } else { robot.keyPress(keyCode); } }

Podobnie do RobotKeyboard jest klasa RobotMouse, która obsługuje zdarzenia wskaźnika i powoduje, że wskaźnik myszy porusza się i klika.

 public void mouseMove(int x, int y) { robot.mouseMove(x, y); }

Wszystkie trzy klasy RobotScreen, RobotMouse i RobotKeyboard przydzielają nową instancję Robot w konstruktorze:

 this.robot = new Robot();

Mamy tylko jedną instancję każdej z nich, ponieważ na poziomie aplikacji nie ma potrzeby posiadania więcej niż jednej instancji klasy RobotScreen, RobotMouse lub 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(); ... }

W tej aplikacji demonstracyjnej te instancje są tworzone w funkcji main() .

Rezultatem jest aplikacja oparta na Swing w Javie, która działa jako dostawca usług RFB i umożliwia łączenie się z nią standardowym przeglądarkom VNC:

Wniosek

Protokół RFB jest szeroko stosowany i akceptowany. Implementacje klienckie w postaci przeglądarek VNC istnieją na prawie wszystkich platformach i urządzeniach. Głównym celem jest zdalne wyświetlanie pulpitów, ale mogą też istnieć inne aplikacje. Na przykład możesz stworzyć fajne narzędzia graficzne i uzyskać do nich zdalny dostęp, aby ulepszyć istniejące zdalne przepływy pracy.

W tym artykule omówiono podstawy protokołu RFB, formatu wiadomości, sposobu wysyłania części ekranu oraz radzenia sobie z klawiaturą i myszą. Pełny kod źródłowy z aplikacją demonstracyjną Swing jest dostępny na GitHub.