Java에서 원격 프레임 버퍼 서버 구현

게시 됨: 2022-03-11

컴퓨팅에서 VNC(가상 네트워크 컴퓨팅)는 RFB(원격 프레임 버퍼) 프로토콜을 사용하여 다른 컴퓨터를 원격으로 제어하는 ​​그래픽 데스크탑 공유 시스템입니다. 한 컴퓨터에서 다른 컴퓨터로 키보드 및 마우스 이벤트를 전송하고 네트워크를 통해 그래픽 화면 업데이트를 다시 다른 방향으로 중계합니다.

RFB는 그래픽 사용자 인터페이스에 대한 원격 액세스를 위한 간단한 프로토콜입니다. 프레임 버퍼 수준에서 작동하기 때문에 Microsoft Windows, Mac OS X 및 X Window System을 포함한 모든 윈도우 시스템 및 응용 프로그램에 적용할 수 있습니다.

Java에서 원격 프레임 버퍼 서버 측 프로토콜 기반 Swing 애플리케이션 구축

Java에서 원격 프레임 버퍼 서버 측 프로토콜 기반 Swing 애플리케이션 구축
트위터

이 기사에서는 RFB 서버 측 프로토콜을 구현하는 방법과 작은 Java Swing 응용 프로그램을 사용하여 TCP 연결을 통해 VNC 뷰어에 기본 창을 전송하는 방법을 보여줍니다. 이 아이디어는 프로토콜의 기본 기능과 Java에서 가능한 구현을 시연하는 것입니다.

독자는 Java 프로그래밍 언어에 대한 기본 지식이 있어야 하며 TCP/IP 네트워킹, 클라이언트-서버 모델 등의 기본 개념에 익숙해야 합니다. 이상적으로 독자는 Java 개발자이고 RealVNC와 같은 잘 알려진 VNC 구현에 대한 약간의 경험이 있어야 합니다. , UltraVNC, TightVNC 등

원격 프레임 버퍼 프로토콜 사양

RFB 프로토콜 사양은 꽤 잘 정의되어 있습니다. Wikipedia에 따르면 RFB 프로토콜에는 여러 버전이 있습니다. 이 기사에서는 프로토콜 버전에 관계없이 대부분의 VNC 구현에서 적절하게 이해해야 하는 공통 메시지에 초점을 맞출 것입니다.

VNC 뷰어(클라이언트)가 VNC 서버(RFB 서비스)에 대한 TCP 연결을 설정한 후 첫 번째 단계에는 프로토콜 버전 교환이 포함됩니다.

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

"RFB 003.008\n"과 같은 ASCII 문자로 디코딩할 수 있는 간단한 바이트 스트림입니다.

완료되면 다음 단계는 인증입니다. VNC 서버는 지원하는 인증 유형을 나타내기 위해 바이트 배열을 보냅니다. 예를 들어:

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

여기서 VNC 서버는 1개의 가능한 인증 유형(0x02)만 보냈습니다. 첫 번째 바이트 0x01은 사용 가능한 인증 유형의 수를 나타냅니다. VNC 뷰어는 값 0x02로 응답해야 합니다. 이 예에서 서버가 지원하는 유일한 유형이기 때문입니다.

다음으로 서버는 인증 챌린지를 보내고(알고리즘에 따라 여러 가지가 있음) 클라이언트는 적절한 챌린지 응답 메시지로 응답하고 서버가 응답을 확인할 때까지 기다려야 합니다. 클라이언트가 인증되면 세션 설정 프로세스를 계속할 수 있습니다.

여기서 가장 간단한 방법은 인증을 전혀 선택하지 않는 것입니다. RFB 프로토콜은 인증 메커니즘에 관계없이 어쨌든 안전하지 않습니다. 보안이 중요한 경우 적절한 방법은 VPN 또는 SSH 연결을 통해 RFB 세션을 터널링하는 것입니다.

이 시점에서 VNC 뷰어는 클라이언트가 공유하고 다른 VNC 뷰어가 동일한 데스크톱에 연결할 수 있는지 여부를 알려주는 공유 데스크톱 메시지를 보냅니다. 해당 메시지를 고려하고 여러 VNC 뷰어가 동일한 화면을 공유하는 것을 방지하는 것은 RFB 서비스 구현에 달려 있습니다. 이 메시지는 길이가 1바이트에 불과하며 유효한 값은 0x00 또는 0x01입니다.

마지막으로 RFB 서버는 화면 크기, 픽셀당 비트 수, 깊이, 빅 엔디안 플래그 및 트루 컬러 플래그, 빨강, 녹색 및 파랑 색상의 최대값, 빨강, 녹색 및 파랑 색상에 대한 픽셀의 비트 위치를 포함하는 서버 초기화 메시지를 보냅니다. , 및 데스크탑 문자열/제목. 처음 2바이트는 화면 너비를 픽셀 단위로 나타내고 다음 2바이트는 화면 높이를 나타냅니다. 화면 높이 바이트 다음에 픽셀 바이트당 비트가 메시지에 있어야 합니다. 값은 일반적으로 8, 16 또는 32입니다. 전체 색상 범위를 사용하는 대부분의 최신 시스템에서 픽셀 바이트당 비트 수는 32(0x20)입니다. 서버에서 각 픽셀에 대해 풀 컬러를 요청할 수 있음을 클라이언트에 알립니다. 빅 엔디안 바이트는 픽셀이 빅 엔디안 순서인 경우에만 0이 아닙니다. 트루 컬러 바이트가 0이 아닌 경우(true) 다음 6바이트는 픽셀 값에서 빨강, 녹색 및 파랑 색상 강도를 추출하는 방법을 지정합니다. 다음 6바이트는 픽셀의 빨강, 녹색 및 파랑 구성 요소에 대해 허용되는 최대 값입니다. 이것은 각 색상 구성 요소에 대해 몇 개의 비트만 사용할 수 있는 8비트 색상 모드에서 중요합니다. 빨강, 녹색 및 파랑 이동은 각 색상의 비트 위치를 결정합니다. 마지막 3바이트는 패딩이며 클라이언트에서 무시해야 합니다. 픽셀 형식 뒤에는 바탕 화면 제목에 대한 문자열의 길이를 정의하는 바이트가 있습니다. 데스크탑 제목은 임의 길이의 바이트 배열로 된 ASCII로 인코딩된 문자열입니다.

원격 프레임 버퍼 서버-클라이언트 프로토콜: 버전 교환, 인증 및 서버 초기화 메시지

원격 프레임 버퍼 서버-클라이언트 프로토콜: 버전 교환, 인증 및 서버 초기화 메시지
트위터

서버 초기화 메시지 이후 RFB 서비스는 소켓에서 클라이언트 메시지를 읽고 디코딩해야 합니다. 6가지 유형의 메시지가 있습니다.

  • SetPixelFormat
  • 인코딩 설정
  • 프레임 버퍼 업데이트 요청
  • 키 이벤트
  • 포인터 이벤트
  • ClientCutText

프로토콜 문서는 매우 정확하며 각 메시지를 설명합니다. 각 메시지에 대해 모든 바이트가 설명됩니다. 예를 들어, 서버 초기화 메시지:

바이트 수 유형 설명
2 U16 프레임 버퍼 너비
2 U16 프레임 버퍼 높이
16 PIXEL_FORMAT 서버 픽셀 형식
4 U32 이름 길이
이름 길이 U8 어레이 이름 문자열

여기에서 PIXEL_FORMAT은 다음과 같습니다.

바이트 수 유형 설명
1 U8 픽셀당 비트 수
1 U8 깊이
1 U8 빅엔디안 플래그
1 U8 트루 컬러 플래그
2 U16 레드맥스
2 U16 그린맥스
2 U16 블루맥스
1 U8 적색편이
1 U8 녹색 교대
1 U8 청색 이동

U16은 부호 없는 16비트 정수(2바이트), 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() 가 차단되고 스레드가 새 클라이언트 연결을 기다리게 합니다. 클라이언트가 연결되면 클라이언트에서 수신한 RFB 프로토콜 메시지를 처리하는 새 스레드 RFBService가 생성됩니다.

RFBService 클래스는 Runnable 인터페이스를 구현합니다. 소켓에서 바이트를 읽는 방법으로 가득 차 있습니다. run() 메서드는 루프의 끝에서 스레드가 시작될 때 즉시 실행되는 중요합니다.

 @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 뷰어)로 보낸 다음 클라이언트에서 프로토콜 버전 문자열을 읽습니다. 클라이언트는 "RFB 003.008\n"과 같이 응답해야 합니다. 메서드 readProtocolVersion() 은 이름이 read라는 단어로 시작하는 모든 메서드와 마찬가지로 차단됩니다.

 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 < buffer.length) { int numOfBytesRead = 0; numOfBytesRead = in.read(buffer, offset, left); offset = offset + numOfBytesRead; left = left - numOfBytesRead; } return buffer; }

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 뷰어가 암호를 보낼 필요가 없음을 의미합니다.

다음으로 RFB 서비스가 클라이언트에서 가져와야 하는 것은 공유 데스크탑 플래그입니다. 소켓의 1바이트입니다.

 /* * 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입니다. 서버 초기화 메시지에는 필수 화면 너비와 높이(픽셀 단위), 바탕 화면 제목이 있습니다. 이 예에서는 getTitle() 메서드로 얻은 JFrame의 제목입니다.

서버 초기화 메시지 후 RFB 서비스 스레드는 소켓에서 다음 6가지 유형의 메시지를 읽어 루프합니다.

 /* * 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 뷰어가 RFB 프로토콜을 통해 서버로 텍스트를 보냅니다. 그런 다음 텍스트는 클립보드의 서버 측에 배치됩니다.

클라이언트 메시지

6개의 모든 메시지는 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 플래그가 true로 설정됩니다. 이 플래그는 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 뷰어에서 무시해야 합니다. 사각형의 개수는 2바이트 값으로 메시지에서 몇 개의 사각형이 뒤에 오는지를 정의합니다.

각 사각형에는 왼쪽 상단 좌표, 너비 및 높이, 인코딩 유형 및 픽셀 데이터가 있습니다. zrle, hextile 및 tight와 같이 사용할 수 있는 몇 가지 효율적인 인코딩 형식이 있습니다. 그러나 간단하고 이해하기 쉽게 구현하기 위해 원시 인코딩을 사용합니다.

Raw 인코딩은 픽셀 색상이 RGB 구성 요소로 전송됨을 의미합니다. 클라이언트가 픽셀 인코딩을 32비트로 설정한 경우 각 픽셀에 대해 4바이트가 전송됩니다. 클라이언트가 8비트 컬러 모드를 사용하는 경우 각 픽셀은 1바이트로 전송됩니다. 코드는 for 루프에 표시됩니다. 8비트 모드의 경우 컬러 맵은 스크린샷/이미지 버퍼에서 각 픽셀에 가장 잘 맞는 것을 찾는 데 사용됩니다. 32비트 픽셀 모드의 경우 이미지 버퍼에는 정수 배열이 포함되며 각 값에는 RGB 구성 요소가 다중화됩니다.

스윙 데모 애플리케이션

스윙 데모 응용 프로그램에는 sendFrameBufferUpdate(int, int, int, int, int[]) 메서드를 트리거하는 작업 수신기가 포함되어 있습니다. 일반적으로 Swing 구성 요소와 같은 응용 프로그램 요소에는 리스너가 있어야 하고 화면 변경 사항을 클라이언트에 보내야 합니다. 예를 들어 사용자가 JTextArea에 무언가를 입력하면 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; } } } }

이 액션 리스너의 코드는 매우 간단합니다. RobotScreen 클래스를 사용하여 기본 창 JFrameMain의 스크린샷을 찍은 다음 화면의 부분 업데이트가 필요한지 여부를 결정합니다. 변수 diffUpdateOfScreen 은 부분 업데이트의 플래그로 사용됩니다. 그리고 마지막으로 완전한 이미지 버퍼 또는 다른 행만 클라이언트로 전송됩니다. 이 코드는 또한 연결된 더 많은 클라이언트를 고려하므로 iterator가 사용되고 클라이언트 목록이 RFBDemo.rfbClientList<RFBService> 멤버에서 유지됩니다.

프레임 버퍼 업데이트 작업 수신기는 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() 가 실행됩니다.

위 그림은 프레임 버퍼 업데이트 절차에 대한 리스너 관계를 보여줍니다. 대부분의 리스너는 사용자가 클릭, 텍스트 선택, 텍스트 영역에 무언가 입력 등의 작업을 수행할 때 트리거됩니다. 그런 다음 타이머 timerUpdateFrameBuffer 를 시작하는 멤버 함수 doIncrementalFramebufferUpdate() 가 실행됩니다. 타이머는 결국 RFBService 클래스의 sendFrameBufferUpdate() 메서드를 호출하고 클라이언트 측(VNC 뷰어)에서 화면 업데이트를 발생시킵니다.

화면 캡처, 키 입력 재생 및 화면에서 마우스 포인터 이동

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

이 데모 애플리케이션에서 이러한 인스턴스는 main() 함수에서 생성됩니다.

결과는 RFB 서비스 제공자 역할을 하고 표준 VNC 뷰어가 연결할 수 있도록 하는 Java의 Swing 기반 애플리케이션입니다.

결론

RFB 프로토콜은 널리 사용되고 허용됩니다. VNC 뷰어 형태의 클라이언트 구현은 거의 모든 플랫폼 및 장치에 대해 존재합니다. 주요 목적은 데스크톱을 원격으로 표시하는 것이지만 다른 응용 프로그램도 있을 수 있습니다. 예를 들어 멋진 그래픽 도구를 만들고 원격으로 액세스하여 기존 원격 워크플로를 향상시킬 수 있습니다.

이 기사에서는 RFB 프로토콜의 기본, 메시지 형식, 화면의 일부를 보내는 방법, 키보드와 마우스를 다루는 방법에 대해 설명합니다. Swing 데모 애플리케이션이 포함된 전체 소스 코드는 GitHub에서 사용할 수 있습니다.