การใช้ Remote Framebuffer Server ใน Java
เผยแพร่แล้ว: 2022-03-11ในการคำนวณ Virtual Network Computing (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 เป็นต้น
ข้อมูลจำเพาะโปรโตคอล Framebuffer ระยะไกล
ข้อกำหนดโปรโตคอล RFB ค่อนข้างกำหนดไว้อย่างดี ตามวิกิพีเดีย โปรโตคอล RFB มีหลายเวอร์ชัน สำหรับบทความนี้ เราจะเน้นไปที่ข้อความทั่วไปที่การใช้งาน VNC ส่วนใหญ่ควรทำความเข้าใจอย่างเหมาะสม โดยไม่คำนึงถึงเวอร์ชันของโปรโตคอล
หลังจากที่ตัวแสดง VNC (ไคลเอนต์) สร้างการเชื่อมต่อ TCP กับเซิร์ฟเวอร์ VNC (บริการ RFB) เฟสแรกจะเกี่ยวข้องกับการแลกเปลี่ยนเวอร์ชันโปรโตคอล:
RFB Service ----------- "RFB 003.003\n" -------> VNC viewer RFB Service <---------- "RFB 003.008\n" -------- VNC viewer
เป็นสตรีมไบต์อย่างง่ายซึ่งสามารถถอดรหัสเป็นอักขระ ASCII ได้ เช่น “RFB 003.008\n”
เมื่อเสร็จแล้วขั้นตอนต่อไปคือการรับรองความถูกต้อง เซิร์ฟเวอร์ VNC ส่งอาร์เรย์ของไบต์เพื่อระบุประเภทของการรับรองความถูกต้องที่รองรับ ตัวอย่างเช่น:
RFB Service ----------- 0x01 0x02 -----------> VNC viewer RFB Service <----------- 0x02 ----------- VNC viewer
ที่นี่เซิร์ฟเวอร์ VNC ส่งประเภทการพิสูจน์ตัวตนที่เป็นไปได้เพียง 1 ประเภท (0x02) ไบต์แรก 0x01 หมายถึงจำนวนประเภทการพิสูจน์ตัวตนที่มีอยู่ โปรแกรมแสดง VNC ต้องตอบกลับด้วยค่า 0x02 เนื่องจากเป็นประเภทเดียวที่เป็นไปได้ที่เซิร์ฟเวอร์รองรับในตัวอย่างนี้
ถัดไป เซิร์ฟเวอร์จะส่งคำท้าในการรับรองความถูกต้อง (ขึ้นอยู่กับอัลกอริธึม มีหลายแบบ) และไคลเอนต์ต้องตอบกลับด้วยข้อความตอบกลับคำท้าที่เหมาะสม และรอให้เซิร์ฟเวอร์ยืนยันการตอบกลับ เมื่อลูกค้าได้รับการตรวจสอบแล้ว พวกเขาก็สามารถดำเนินขั้นตอนการสร้างเซสชันต่อไปได้
วิธีที่ง่ายที่สุดคือเลือกไม่รับรองความถูกต้องเลย โปรโตคอล RFB นั้นไม่ปลอดภัย โดยไม่คำนึงถึงกลไกการพิสูจน์ตัวตน หากการรักษาความปลอดภัยมีความสำคัญ วิธีที่เหมาะสมคือการเจาะสัญญาณเซสชัน RFB ผ่านการเชื่อมต่อ VPN หรือ SSH
ณ จุดนี้ VNC viewer จะส่งข้อความเดสก์ท็อปที่ใช้ร่วมกันซึ่งบอกว่าไคลเอ็นต์จะแชร์หรือไม่และอนุญาตให้ผู้ดู VNC รายอื่นเชื่อมต่อกับเดสก์ท็อปเดียวกัน ขึ้นอยู่กับการนำบริการ RFB ไปใช้ในการพิจารณาข้อความนั้นและอาจป้องกันไม่ให้ผู้ดู VNC หลายรายแชร์หน้าจอเดียวกัน ข้อความนี้มีความยาวเพียง 1 ไบต์ และค่าที่ถูกต้องคือ 0x00 หรือ 0x01
สุดท้าย เซิร์ฟเวอร์ RFB จะส่งข้อความเริ่มต้นของเซิร์ฟเวอร์ ซึ่งประกอบด้วยขนาดหน้าจอ บิตต่อพิกเซล ความลึก แฟล็ก endian ขนาดใหญ่ และแฟล็กสีจริง ค่าสูงสุดสำหรับสีแดง สีเขียว และสีน้ำเงิน ตำแหน่งบิตในพิกเซลสำหรับสีแดง สีเขียว และสีน้ำเงิน และสตริง/ชื่อเดสก์ท็อป สองไบต์แรกแสดงถึงความกว้างของหน้าจอเป็นพิกเซล สองไบต์ถัดไปคือความสูงของหน้าจอ หลังจากไบต์ความสูงของหน้าจอ บิตต่อไบต์พิกเซลควรมีอยู่ในข้อความ ค่ามักจะเป็น 8, 16 หรือ 32 สำหรับระบบที่ทันสมัยส่วนใหญ่ที่มีช่วงสีเต็มรูปแบบ บิตต่อไบต์พิกเซลมีค่า 32 (0x20) มันบอกลูกค้าว่าสามารถขอสีเต็มสำหรับแต่ละพิกเซลจากเซิร์ฟเวอร์ได้ ไบต์ endian ขนาดใหญ่จะไม่เป็นศูนย์ก็ต่อเมื่อพิกเซลอยู่ในลำดับ endian ใหญ่เท่านั้น หากไบต์ของสีจริงไม่ใช่ศูนย์ (จริง) หกไบต์ถัดไปจะระบุวิธีแยกความเข้มของสีแดง สีเขียว และสีน้ำเงินออกจากค่าพิกเซล หกไบต์ถัดไปเป็นค่าสูงสุดที่อนุญาตสำหรับองค์ประกอบสีแดง สีเขียว และสีน้ำเงินของพิกเซล นี่เป็นสิ่งสำคัญในโหมดสี 8 บิต ซึ่งมีเพียงไม่กี่บิตเท่านั้นสำหรับแต่ละองค์ประกอบสี การเลื่อนสีแดง สีเขียว และสีน้ำเงินจะกำหนดตำแหน่งบิตสำหรับแต่ละสี สามไบต์สุดท้ายเป็นช่องว่างภายใน และลูกค้าควรละเว้น หลังจากรูปแบบพิกเซล มีไบต์ที่กำหนดความยาวของสตริงสำหรับชื่อเดสก์ท็อป ชื่อเดสก์ท็อปคือสตริงที่เข้ารหัส ASCII ในอาร์เรย์ไบต์ที่มีความยาวตามอำเภอใจ
หลังจากข้อความเริ่มต้นของเซิร์ฟเวอร์ บริการ RFB ควรอ่านข้อความไคลเอ็นต์จากซ็อกเก็ตและถอดรหัส ข้อความมี 6 ประเภท:
- SetPixelFormat
- SetEncodings
- FramebufferUpdateRequest
- คีย์อีเวนท์
- PointerEvent
- ClientCutText
เอกสารโปรโตคอลค่อนข้างแม่นยำและอธิบายแต่ละข้อความ สำหรับแต่ละข้อความ มีการอธิบายทุกไบต์ ตัวอย่างเช่น ข้อความเริ่มต้นของเซิร์ฟเวอร์:
จำนวนไบต์ | พิมพ์ | คำอธิบาย |
---|---|---|
2 | U16 | framebuffer-ความกว้าง |
2 | U16 | framebuffer-ความสูง |
16 | PIXEL_FORMAT | เซิร์ฟเวอร์รูปแบบพิกเซล |
4 | U32 | ชื่อความยาว |
ชื่อความยาว | อาร์เรย์ U8 | ชื่อสตริง |
PIXEL_FORMAT คือ:
จำนวนไบต์ | พิมพ์ | คำอธิบาย |
---|---|---|
1 | U8 | บิตต่อพิกเซล |
1 | U8 | ความลึก |
1 | U8 | ใหญ่-endian-flag |
1 | U8 | ธงสีจริง |
2 | U16 | แดง-max |
2 | U16 | สีเขียว-max |
2 | U16 | blue-max |
1 | U8 | แดงกะ |
1 | U8 | เขียวกะ |
1 | U8 | blue-shift |
3 | การขยายความ |
U16 หมายถึงจำนวนเต็ม 16 บิตที่ไม่ได้ลงนาม (สองไบต์), U32 เป็นจำนวนเต็ม 32 บิตที่ไม่ได้ลงนาม, อาร์เรย์ U8 คืออาร์เรย์ของไบต์ ฯลฯ
การนำโปรโตคอลไปใช้ใน Java
แอปพลิเคชันเซิร์ฟเวอร์ 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 loop รอให้ไคลเอ็นต์เชื่อมต่อ เมธอด ServerSocket.accept() กำลังบล็อกและทำให้เธรดรอการเชื่อมต่อไคลเอ็นต์ใหม่ เมื่อไคลเอ็นต์เชื่อมต่อแล้ว จะมีการสร้างเธรดใหม่ RFBService ซึ่งจัดการข้อความโปรโตคอล RFB ที่ได้รับจากไคลเอ็นต์
คลาส RFBService ใช้อินเทอร์เฟซที่รันได้ เต็มไปด้วยวิธีการอ่านไบต์จากซ็อกเก็ต เมธอด 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.08\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 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 จะส่งข้อความผ่านโปรโตคอล 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 อย่างไรก็ตาม เพื่อให้สิ่งต่าง ๆ เรียบง่ายและเข้าใจง่าย เราจะใช้การเข้ารหัสดิบในการปรับใช้ของเรา
การเข้ารหัสแบบ Raw หมายความว่าสีพิกเซลจะถูกส่งเป็นองค์ประกอบ RGB หากไคลเอนต์ตั้งค่าการเข้ารหัสพิกเซลเป็น 32 บิต ระบบจะส่ง 4 ไบต์สำหรับแต่ละพิกเซล หากไคลเอนต์ใช้โหมดสี 8 บิต แต่ละพิกเซลจะถูกส่งเป็น 1 ไบต์ รหัสจะแสดงใน for-loop โปรดทราบว่าสำหรับแผนที่สีโหมด 8 บิตจะใช้เพื่อค้นหาการจับคู่ที่ดีที่สุดสำหรับแต่ละพิกเซลจากภาพหน้าจอ / บัฟเฟอร์ภาพ สำหรับโหมดพิกเซลแบบ 32 บิต บัฟเฟอร์รูปภาพประกอบด้วยอาร์เรย์ของจำนวนเต็ม แต่ละค่ามีคอมโพเนนต์ RGB แบบมัลติเพล็กซ์
แอปพลิเคชั่นสาธิตการสวิง
แอปพลิเคชันสาธิต Swing ประกอบด้วยตัวฟังการกระทำที่ทริกเกอร์ เมธอด 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; } } } }
รหัสของฟังการกระทำนี้ค่อนข้างง่าย: ใช้ภาพหน้าจอของหน้าต่างหลัก JFrameMain โดยใช้คลาส RobotScreen จากนั้นจะพิจารณาว่าจำเป็นต้องอัปเดตหน้าจอบางส่วนหรือไม่ ตัวแปร diffUpdateOfScreen ถูกใช้เป็นแฟล็กสำหรับการอัปเดตบางส่วน และสุดท้ายบัฟเฟอร์รูปภาพที่สมบูรณ์หรือเฉพาะแถวที่ต่างกันจะถูกส่งไปยังไคลเอนต์ รหัสนี้ยังพิจารณาไคลเอ็นต์ที่เชื่อมต่อมากขึ้น นั่นคือสาเหตุที่ใช้ iterator และรายชื่อไคลเอ็นต์ได้รับการดูแลรักษาใน สมาชิก RFBDemo.rfbClientList<RFBService>
ตัวฟังการดำเนินการอัพเดต 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() เพียงครั้งเดียว เมธอดจะตรวจสอบว่ามีไคลเอ็นต์เชื่อมต่ออยู่หรือไม่ และหากมี ตัวจับเวลาตัวจับเวลา UpdateFrameBuffer จะเริ่มทำงาน เมื่อตัวจับเวลาเริ่มต้นแล้ว Action Listener จะจับภาพหน้าจอและ sendFrameBufferUpdate() จะถูกดำเนินการ
รูปด้านบนแสดงความสัมพันธ์ของผู้ฟังกับขั้นตอนการอัพเดตบัฟเฟอร์เฟรม ผู้ฟังส่วนใหญ่จะถูกทริกเกอร์เมื่อผู้ใช้ดำเนินการ: คลิก เลือกข้อความ พิมพ์บางอย่างในพื้นที่ข้อความ ฯลฯ จากนั้นดำเนินการฟังก์ชันสมาชิก doIncrementalFramebufferUpdate() ซึ่งจะเริ่มตัวจับเวลา timerUpdateFrameBuffer ในที่สุดตัวจับเวลาจะเรียก เมธอด sendFrameBufferUpdate() ในคลาส RFBService และจะทำให้อัปเดตหน้าจอในฝั่งไคลเอ็นต์ (ตัวแสดง 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 จะต้องแปลให้ถูกต้องก่อน Class 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) จะส่งค่าคีย์ไปยัง Robot และเอฟเฟกต์จะเหมือนกับที่ผู้ใช้ในพื้นที่กดปุ่มบนแป้นพิมพ์:
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()
ผลลัพธ์ที่ได้คือแอปพลิเคชันที่ใช้ Swing ใน Java ซึ่งทำหน้าที่เป็นผู้ให้บริการ RFB และอนุญาตให้ผู้ดู VNC มาตรฐานเชื่อมต่อได้:
บทสรุป
โปรโตคอล RFB ใช้กันอย่างแพร่หลายและเป็นที่ยอมรับ การใช้งานไคลเอ็นต์ในรูปแบบของโปรแกรมดู VNC มีอยู่ในเกือบทุกแพลตฟอร์มและอุปกรณ์ วัตถุประสงค์หลักคือการแสดงเดสก์ท็อปจากระยะไกล แต่อาจมีแอปพลิเคชันอื่นด้วย ตัวอย่างเช่น คุณสามารถสร้างเครื่องมือกราฟิกที่สวยงามและเข้าถึงได้จากระยะไกลเพื่อปรับปรุงเวิร์กโฟลว์จากระยะไกลที่มีอยู่ของคุณ
บทความนี้ครอบคลุมพื้นฐานของโปรโตคอล RFB รูปแบบข้อความ วิธีส่งส่วนหนึ่งของหน้าจอ และวิธีจัดการกับแป้นพิมพ์และเมาส์ ซอร์สโค้ดแบบเต็มพร้อมแอปพลิเคชันสาธิต Swing มีอยู่ใน GitHub