項目需求,要求在瀏覽器端進行遠程桌面的訪問,如圖所示:javascript
實現遠程桌面,須要依賴VNC
協議:html
VNC(Virtual Network Computing)
,爲一種使用RFB
協議的屏幕畫面分享及遠程操做軟件。此軟件藉由網絡,可發送鍵盤與鼠標的動做及即時的屏幕畫面。java
相關的參考比較少,去谷歌搜索出來的文章大多都是如何使用客戶端進行VNC
的搭建與訪問,不多有將其內嵌到web
裏的,騰訊雲有相關的功能,但由於業務安全性,咱也看不着人家咋實現的。node
再見,百度。用百度查了一次以後,我才知道原來VNC
是口紅。nginx
因此VNC
實踐之路就是以下流程:web
VNC
方案。從總體的最開始設計,到最終落地方案,大約經歷瞭如下七個方案的迭代:spring
SpringBoot
調用REALVNC
的C++
類庫,先後臺進行數據交互。失敗,由於REALVNC
太貴了,客戶承受不起。SpringBoot
中模仿TightVNC
實現JavaViewer
獲取數據,先後臺進行數據交互。失敗,由於TightVNC JavaViewer
的源碼沒註釋,看不懂。SpringBoot
中手寫VNC
客戶端,先後臺數據交互。失敗,由於從0
實現一個協議太複雜了,時間成本過高。VNC
連接,使用原生客戶端,直接訪問主機。失敗,須要安裝軟件,且只能訪問局域網中的主機。nginx
數據轉發。失敗,須要安裝軟件,沒法實現動態轉發(沒法動態變動nginx
配置文件)。no-vnc
+ nginx
數據轉發。失敗,沒法實現動態轉發(沒法動態變動nginx
配置文件)。no-vnc
+ node.js
數據轉發。成功,完美實現。總體思想以下圖所示:nginx
轉發前臺的websocket
鏈接,爲了實現外網轉發,添加開發的node.js
服務器做爲代理,將瀏覽器端no-vnc
的websocket
數據報在運輸層轉發給目標主機。typescript
若是思考過的話,其實發現不用nginx
也能實現功能,這裏使用nginx
主要是減小了前臺對後臺架構的耦合。shell
添加網關轉發全部請求,對前臺只暴露一個端口,無論後臺用什麼技術,用什麼架構,用什麼微服務,在前臺看來,就好像在訪問單體應用同樣。npm
就像目前的華軟項目同樣,後臺用了spring-boot
、.net
、node.js
,各語言各框架發揮各自的優點,經過nginx
的轉發將各模塊鏈接起來,不管後臺的架構怎麼變,對前臺毫無影響,這應該是微服務架構的最佳實踐。
這是spring
官方推薦的微服務架構圖,咱們學習並實踐了api
網關,spring
推薦netflix zuul
,咱們用的nginx
,在請求轉發上,兩者性能不相上下。
隨着業務需求的增加,咱們確定也會服務拆分,服務註冊,服務發現,消息隊列,RPC
調用。而後用上eureka
、zookeeper
、hystrix
、feign
等一個個優秀的開源組件,一塊兒探索spring-cloud
的最佳實踐。
以前一直不瞭解websocket
,就是知道個名,具體細節沒有學習。
http
協議:請求響應,客戶端請求,服務器響應,一次請求就結束。服務端沒法主動向客戶端推送數據。
爲了解決這個問題,websocket
應運而生。若是所示,不作贅述。
官網連接:noVNC
安裝依賴:
npm install @novnc/novnc
一個空div
,同時在組件中引用。
<div class="container" #container> </div>
@ViewChild('container') private container: ElementRef<HTMLDivElement>;
核心的代碼其實就這幾行,全部協議的細節都被封裝在no-vnc
中的RFB
類中了。
全部描述以訪問192.168.0.104
主機的5900
端口爲例,websocket
地址爲:ws://127.0.0.1:8013/vnc/192.168.0.104:5900
。
/** * VNC鏈接 */ private VNCConnect(): void { /** 訪問 /vnc/ websocket */ const url = `ws://${this.host}/vnc/${this.ip}:${this.port}`; /** 新建遠程控制對象 */ this.rfb = new RFB(this.container.nativeElement, url, { credentials: { password: this.password, }, }); /** 添加connect事件監聽器 */ this.rfb.addEventListener('connect', () => { this.rfb.focus(); }); }
nginx
監聽本地的8013
端口。
ws://127.0.0.1:8013/vnc/192.168.0.104:5900
請求發給了nginx
,根據前綴匹配,以/vnc/
開頭的轉發給8112
端口。
location /vnc/ { proxy_pass http://127.0.0.1:8112/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; }
node.js
監聽8112
端口,處理當前的websocket
請求。
/** 創建基於 vnc_port 的 websocket 服務器 */ const vnc_server = http.createServer(); vnc_server.listen(vnc_port, function () { const web_socket_server = new WebSocketServer({server: vnc_server}); web_socket_server.on('connection', web_socket_handler); });
轉發的核心代碼在方法web_socket_handler
中,如下是完整代碼:
這裏說一句,以前寫的註釋都不規範,全部註釋都應該是文檔註釋,單行註釋使用/** 內容 */
的格式。
/** 引入 http 包 */ const http = require('http'); /** 引入 net 包 */ const net = require('net'); /** 引入 websocket 類 */ const WebSocketServer = require('ws').Server; /** 本機 ip 地址 */ const localhost = '127.0.0.1'; /** 開放的 vnc websocket 轉發端口 */ const vnc_port = '8112'; /** 打印提示信息 */ console.log(`成功建立 WebSocket 代理 : ${localhost} : ${vnc_port}`); /** 創建基於 vnc_port 的 websocket 服務器 */ const vnc_server = http.createServer(); vnc_server.listen(vnc_port, function () { const web_socket_server = new WebSocketServer({server: vnc_server}); web_socket_server.on('connection', web_socket_handler); }); /** websocket 處理器 */ const web_socket_handler = function (client, req) { /** 獲取請求url */ const url = req.url; /** 截取主機地址 */ const host = url.substring(url.indexOf('/') + 1, url.indexOf(':')); /** 截取端口號 */ const port = Number(url.substring(url.indexOf(':') + 1)); /** 打印日誌 */ console.log(`WebSocket 鏈接 : 版本 ${client.protocolVersion}, 協議 ${client.protocol}`); /** 鏈接到 VNC Server */ const target = net.createConnection(port, host, function () { console.log('鏈接至目標主機'); }); /** 數據事件 */ target.on('data', function (data) { try { client.send(data); } catch (error) { console.log('客戶端已關閉,清理到目標主機的鏈接'); target.end(); } }); /** 結束事件 */ target.on('end', function () { console.log('目標主機已關閉'); client.close(); }); /** 錯誤事件 */ target.on('error', function () { console.log('目標主機鏈接錯誤'); target.end(); client.close(); }); /** 消息事件 */ client.on('message', function (msg) { target.write(msg); }); /** 關閉事件 */ client.on('close', function (code, reason) { console.log(`WebSocket 客戶端斷開鏈接:${code} [${reason}]`); target.end(); }); /** 錯誤事件 */ client.on('error', function (error) { console.log(`WebSocket 客戶端出錯:${error}`); target.end(); }); };
爲了這個功能犯愁了半個月,覺也睡很差,客戶都在騰訊雲上看到過的功能,寫不出來就特別的難受,現在終於圓滿解決。
擁抱開源,互幫互助,若是過去的我能看到我這篇博客,我就不會浪費這麼久的時間進行一次次的嘗試了。