ในการคำนวณ 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 เป็นต้น
ข้อกำหนดโปรโตคอล RFB ค่อนข้างสวย กำหนดไว้อย่างดี . ตามวิกิพีเดียโปรโตคอล RFB มีหลายเวอร์ชัน สำหรับบทความนี้เรามุ่งเน้นไปที่ข้อความทั่วไปที่ควรเข้าใจอย่างถูกต้องโดยการใช้งาน VNC ส่วนใหญ่โดยไม่คำนึงถึงเวอร์ชันโปรโตคอล
หลังจาก VNC viewer (ไคลเอนต์) สร้างการเชื่อมต่อ 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 ต้องตอบกลับด้วยค่า 0x02 เนื่องจากเป็นประเภทเดียวที่เป็นไปได้ที่เซิร์ฟเวอร์สนับสนุนในตัวอย่างนี้
จากนั้นเซิร์ฟเวอร์จะส่งคำท้าพิสูจน์ตัวตน (ขึ้นอยู่กับอัลกอริทึมใดมีหลายข้อ) และลูกค้าจะต้องตอบกลับด้วยข้อความตอบรับการท้าทายที่เหมาะสมและรอให้เซิร์ฟเวอร์ยืนยันการตอบกลับ เมื่อไคลเอ็นต์ได้รับการตรวจสอบสิทธิ์แล้วพวกเขาสามารถดำเนินการต่อในขั้นตอนการสร้างเซสชันได้
วิธีที่ง่ายที่สุดคือเลือกไม่รับรองความถูกต้องเลย โปรโตคอล RFB นั้นไม่ปลอดภัยอยู่แล้วโดยไม่คำนึงถึงกลไกการตรวจสอบสิทธิ์ หากการรักษาความปลอดภัยมีความสำคัญวิธีที่เหมาะสมคือการอุโมงค์เซสชัน RFB ผ่านการเชื่อมต่อ VPN หรือ SSH
ณ จุดนี้โปรแกรมดู VNC จะส่งข้อความบนเดสก์ท็อปที่แชร์ซึ่งจะบอกว่าไคลเอนต์จะแชร์หรือไม่และอนุญาตให้ผู้ดู VNC อื่นเชื่อมต่อกับเดสก์ท็อปเดียวกัน การใช้บริการ RFB ขึ้นอยู่กับการพิจารณาข้อความนั้นและอาจป้องกันไม่ให้ผู้ชม VNC หลายคนแชร์หน้าจอเดียวกัน ข้อความนี้มีความยาวเพียง 1 ไบต์และค่าที่ถูกต้องคือ 0x00 หรือ 0x01
ในที่สุดเซิร์ฟเวอร์ RFB จะส่งข้อความเริ่มต้นเซิร์ฟเวอร์ซึ่งประกอบด้วยขนาดหน้าจอบิตต่อพิกเซลความลึกแฟล็ก endian ขนาดใหญ่และแฟล็กสีจริงค่าสูงสุดสำหรับสีแดงเขียวและน้ำเงินตำแหน่งบิตเป็นพิกเซลสำหรับสีแดงเขียวและน้ำเงิน และสตริงเดสก์ท็อป / ชื่อเรื่อง สองไบต์แรกแสดงถึงความกว้างของหน้าจอเป็นพิกเซลสองไบต์ถัดไปคือความสูงของหน้าจอ หลังจากไบต์ความสูงของหน้าจอบิตต่อพิกเซลไบต์ควรอยู่ในข้อความ โดยปกติค่าจะเป็น 8, 16 หรือ 32 ในระบบสมัยใหม่ส่วนใหญ่ที่มีช่วงสีเต็มบิตต่อพิกเซลไบต์จะมีค่า 32 (0x20) มันบอกไคลเอนต์ว่าสามารถขอสีเต็มสำหรับแต่ละพิกเซลจากเซิร์ฟเวอร์ได้ ไบต์ endian ใหญ่ไม่เป็นศูนย์ก็ต่อเมื่อพิกเซลอยู่ในลำดับ endian ใหญ่ ถ้าไบต์สีจริงไม่ใช่ศูนย์ (จริง) หกไบต์ถัดไปจะระบุวิธีแยกความเข้มของสีแดงเขียวและน้ำเงินออกจากค่าพิกเซล หกไบต์ถัดไปคือค่าสูงสุดที่อนุญาตสำหรับองค์ประกอบสีแดงเขียวและน้ำเงินของพิกเซล สิ่งนี้มีความสำคัญในโหมดสี 8 บิตซึ่งมีเพียงไม่กี่บิตสำหรับแต่ละองค์ประกอบสี การเปลี่ยนสีแดงเขียวและน้ำเงินจะกำหนดตำแหน่งบิตสำหรับแต่ละสี สามไบต์สุดท้ายเป็นช่องว่างภายในและไคลเอนต์ควรละเว้น หลังจากรูปแบบพิกเซลมีไบต์ที่กำหนดความยาวของสตริงสำหรับหัวเรื่องเดสก์ท็อป ชื่อเดสก์ท็อปเป็นสตริงที่เข้ารหัส ASCII ในอาร์เรย์ไบต์ของความยาวโดยพลการ
หลังจากข้อความเริ่มเซิร์ฟเวอร์บริการ RFB ควรอ่านข้อความไคลเอนต์จากซ็อกเก็ตและถอดรหัส ข้อความมี 6 ประเภท:
เอกสารโปรโตคอลค่อนข้างแน่นอนและอธิบายแต่ละข้อความ สำหรับแต่ละข้อความจะมีการอธิบายทุกไบต์ ตัวอย่างเช่นข้อความเริ่มต้นของเซิร์ฟเวอร์:
จำนวนไบต์ | ประเภท | คำอธิบาย |
---|---|---|
2 | U16 | framebuffer-width |
2 | U16 | framebuffer-height |
16 | PIXEL_FORMAT | เซิร์ฟเวอร์พิกเซลรูปแบบ |
4 | U32 | ชื่อ - ความยาว |
ชื่อ - ความยาว | อาร์เรย์ U8 | ชื่อสตริง |
ที่นี่ PIXEL_FORMAT คือ:
จำนวนไบต์ | ประเภท | คำอธิบาย |
---|---|---|
หนึ่ง | U8 | บิตต่อพิกเซล |
หนึ่ง | U8 | ความลึก |
หนึ่ง | U8 | ธงใหญ่ endian |
หนึ่ง | U8 | จริงสีธง |
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 ใช้อินเทอร์เฟซที่รันได้ เต็มไปด้วยวิธีการอ่านไบต์จากซ็อกเก็ต วิธี วิ่ง() มีความสำคัญซึ่งจะดำเนินการทันทีเมื่อเธรดเริ่มต้นที่ส่วนท้ายของลูป:
@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 () แน่นอนว่าเป็นการปิดกั้นเช่นเดียวกับวิธีการใด ๆ ที่ชื่อขึ้นต้นด้วยคำอ่าน
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 คล้ายกับ อ่าน U8Array (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 ค่า Padding มักจะเป็น 0x00 และ VNC viewer ควรละเว้น จำนวนรูปสี่เหลี่ยมเป็นค่าสองไบต์และกำหนดจำนวนสี่เหลี่ยมตามหลังในข้อความ
สี่เหลี่ยมผืนผ้าแต่ละอันมีพิกัดด้านซ้ายบนความกว้างและความสูงประเภทการเข้ารหัสและข้อมูลพิกเซล มีรูปแบบการเข้ารหัสที่มีประสิทธิภาพบางอย่างที่สามารถใช้ได้เช่น zrle, hextile และ tight อย่างไรก็ตามเพื่อให้สิ่งต่างๆเรียบง่ายและเข้าใจง่ายเราจะใช้การเข้ารหัสดิบในการใช้งานของเรา
การเข้ารหัสแบบ Raw หมายความว่าสีของพิกเซลจะถูกส่งเป็นส่วนประกอบ RGB หากไคลเอนต์ตั้งค่าการเข้ารหัสพิกเซลเป็น 32 บิตระบบจะส่งข้อมูล 4 ไบต์สำหรับแต่ละพิกเซล หากไคลเอนต์ใช้โหมดสี 8 บิตแต่ละพิกเซลจะถูกส่งเป็น 1 ไบต์ รหัสแสดงใน for-loop โปรดทราบว่าสำหรับแผนที่สีในโหมด 8 บิตจะใช้เพื่อค้นหาการจับคู่ที่ดีที่สุดสำหรับแต่ละพิกเซลจากภาพหน้าจอ / บัฟเฟอร์รูปภาพ สำหรับโหมดพิกเซล 32 บิตบัฟเฟอร์รูปภาพจะมีอาร์เรย์ของจำนวนเต็มโดยแต่ละค่าจะมีส่วนประกอบ RGB แบบมัลติเพล็กซ์
แอปพลิเคชั่น Swing Demo
แอปพลิเคชั่นสาธิต 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 update action listener สามารถใช้ใน Timer ซึ่งสามารถเริ่มได้โดยการเปลี่ยนแปลง 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 () method:
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)
จับภาพหน้าจอเล่นการกดแป้นพิมพ์และเลื่อนตัวชี้เมาส์บนหน้าจอ
Java มีคลาส Robot ในตัวที่ช่วยให้นักพัฒนาสามารถเขียนแอปพลิเคชันที่จะจับภาพหน้าจอส่งคีย์จัดการตัวชี้เมาส์สร้างการคลิกและอื่น ๆ
ในการจับพื้นที่ของหน้าจอที่แสดงหน้าต่าง JFrame จะใช้ RobotScreen วิธีหลักคือ getSc screenshot (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) ส่งผ่านค่าคีย์ไปยัง 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(); ... }
ในแอปพลิเคชันสาธิตนี้อินสแตนซ์เหล่านี้ถูกสร้างขึ้นใน หลัก() ฟังก์ชัน
ผลลัพธ์คือแอปพลิเคชันที่ใช้ Swing ใน Java ซึ่งทำหน้าที่เป็นผู้ให้บริการ RFB และอนุญาตให้ผู้ชม VNC มาตรฐานเชื่อมต่อกับมัน:

สรุป
โปรโตคอล RFB ใช้กันอย่างแพร่หลายและเป็นที่ยอมรับ การใช้งานไคลเอ็นต์ในรูปแบบของ VNC viewer มีอยู่ในเกือบทุกแพลตฟอร์มและอุปกรณ์ จุดประสงค์หลักคือการแสดงเดสก์ท็อปจากระยะไกล แต่อาจมีแอปพลิเคชันอื่น ๆ ได้เช่นกัน ตัวอย่างเช่นคุณสามารถสร้างเครื่องมือกราฟิกที่ดีและเข้าถึงจากระยะไกลเพื่อปรับปรุงที่มีอยู่ของคุณ เวิร์กโฟลว์ระยะไกล .
บทความนี้ครอบคลุมพื้นฐานของโปรโตคอล RFB รูปแบบข้อความวิธีส่งส่วนหนึ่งของหน้าจอและวิธีจัดการกับแป้นพิมพ์และเมาส์ ซอร์สโค้ดแบบเต็มพร้อมแอพพลิเคชั่นสาธิต Swing คือ พร้อมใช้งานบน GitHub .