整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          網頁端IM通信技術快速入門:短輪詢、長輪詢、SSE、WebSocket

          文來自“糊糊糊糊糊了”的分享,原題《實時消息推送整理》,有優(yōu)化和改動。

          1、寫在前面

          對Web端即時通訊技術熟悉的開發(fā)者來說,我們回顧網頁端IM的底層通信技術,從短輪詢、長輪詢,到后來的SSE以及WebSocket,使用門檻越來越低(早期的長輪詢Comet這類技術實際屬于hack手段,使用門檻并不低),技術手段越來越先進,網頁端即時通訊技術的體驗也因此越來越好。

          但上周在編輯《IM掃碼登錄技術專題》系列文章第3篇的時候忽然想到,之前的這些所謂的網頁端即時通訊“老技術”相對于當紅的WebSocket,并非毫無用武之地。就拿IM里的掃碼登錄功能來說,用短輪詢技術就非常合適,完全沒必要大炮打蚊子上WebSocket。

          所以,很多時候沒必要盲目追求新技術,相對應用場景來說適合的才是最好的。對于即時通訊網的im和消息推送這類即時通訊技術開發(fā)者來說,掌握WebSocket固然很重要,但了解短輪詢、長輪詢等這些所謂的Web端即時通訊“老技術”仍然大有裨益,這也正是整理分享本文的重要原因。

          2、推薦閱讀

          [1] 新手入門貼:史上最全Web端即時通訊技術原理詳解

          [2] 詳解Web端通信方式的演進:從Ajax、JSONP 到 SSE、Websocket

          [3] Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE

          3、正文引言

          對于IM/消息推送這類即時通訊系統(tǒng)而言,系統(tǒng)的關鍵就是“實時通信”能力。

          從表面意思上來看,“實時通信”指的是:

          • 1)客戶端能隨時主動發(fā)送數據給服務端;
          • 2)當客戶端關注的內容在發(fā)生改變時,服務器能夠實時地通知客戶端。

          類比于傳統(tǒng)的C/S請求模型,“實時通信”時客戶端不需要主觀地發(fā)送請求去獲取自己關心的內容,而是由服務器端進行“推送”。

          注意:上面的“推送”二字打了引號,實際上現(xiàn)有的幾種技術實現(xiàn)方式中,并不是服務器端真正主動地推送,而是通過一定的手段營造了一種“實時通信”的假象。

          就目前現(xiàn)有的幾種技術而言,主要有以下幾類:

          • 1)客戶端輪詢:傳統(tǒng)意義上的短輪詢(Short Polling);
          • 2)服務器端輪詢:長輪詢(Long Polling);
          • 3)單向服務器推送:Server-Sent Events(SSE);
          • 4)全雙工通信:WebSocket。

          以下正文將針對這幾種技術方案,為你一一解惑。

          4、本文配套Demo和代碼

          為了幫助讀者更好的理解本文內容,筆者專門寫了一個較完整的Demo,Demo會以一個簡易聊天室的例子來分別通過上述的四種技術方式實現(xiàn)(代碼存在些許bug,主要是為了做演示用,別介意)。

          完整Demo源碼打包下載:

          請從同步鏈接附件中下載:http://www.52im.net/thread-3555-1-1.html)

          Demo的運行效果(動圖):

          有興趣可以自行下載研究學習。

          5、理解短輪詢(Short Polling)

          短輪詢的實現(xiàn)原理:

          • 1)客戶端向服務器端發(fā)送一個請求,服務器返回數據,然后客戶端根據服務器端返回的數據進行處理;
          • 2)客戶端繼續(xù)向服務器端發(fā)送請求,繼續(xù)重復以上的步驟,如果不想給服務器端太大的壓力,一般情況下會設置一個請求的時間間隔。

          邏輯如下圖所示:

          使用短輪詢的優(yōu)點:基礎不需要額外的開發(fā)成本,請求數據,解析數據,作出響應,僅此而已,然后不斷重復。

          缺點也顯而易見:

          • 1)不斷的發(fā)送和關閉請求,對服務器的壓力會比較大,因為本身開啟Http連接就是一件比較耗資源的事情;
          • 2)輪詢的時間間隔不好控制。如果要求的實時性比較高,顯然使用短輪詢會有明顯的短板,如果設置interval的間隔過長,會導致消息延遲,而如果太短,會對服務器產生壓力。

          短輪詢客戶的代碼實現(xiàn)(片段節(jié)選):

          var ShortPollingNotification = {

          datasInterval: null,

          subscribe: function() {

          this.datasInterval = setInterval(function() {

          Request.getDatas().then(function(res) {

          window.ChatroomDOM.renderData(res);

          });

          }, TIMEOUT);

          return this.unsubscribe;

          },

          unsubscribe: function() {

          this.datasInterval && clearInterval(this.datasInterval);

          }

          }

          PS:完整代碼,請見本文“4、本文配套Demo和代碼”一節(jié)。

          對應本文配套Demo的運行效果如下(動圖):

          下面是對應的請求,注意左下角的請求數量一直在變化:

          在上圖中,每隔1s就會發(fā)送一個請求,看起來效果還不錯,但是如果將timeout的值設置成5s,效果將大打折扣。如下圖所示。

          將timeout值設置成5s時的Demo運行效果(動圖):

          6、理解長輪詢(Long Polling)

          6.1 基本原理

          長輪詢的基本原理:

          • 1)客戶端發(fā)送一個請求,服務器會hold住這個請求;
          • 2)直到監(jiān)聽的內容有改變,才會返回數據,斷開連接(或者在一定的時間內,請求還得不到返回,就會因為超時自動斷開連接);
          • 3)客戶端繼續(xù)發(fā)送請求,重復以上步驟。

          邏輯如下圖所示:

          長輪詢是基于短輪詢上的改進版本:主要是減少了客戶端發(fā)起Http連接的開銷,改成了在服務器端主動地去判斷所關心的內容是否變化。

          所以其實輪詢的本質并沒有多大變化,變化的點在于:

          • 1)對于內容變化的輪詢由客戶端改成了服務器端(客戶端會在連接中斷之后,會再次發(fā)送請求,對比短輪詢來說,大大減少了發(fā)起連接的次數);
          • 2)客戶端只會在數據改變時去作相應的改變,對比短輪詢來說,并不是全盤接收。

          6.2 代碼實現(xiàn)

          長輪詢客戶的代碼實現(xiàn)(片段節(jié)選):

          // 客戶端

          var LongPollingNotification = {

          // ....

          subscribe: function() {

          var that = this;


          // 設置超時時間

          Request.getV2Datas(this.getKey(),{ timeout: 10000 }).then(function(res) {

          var data = res.data;

          window.ChatroomDOM.renderData(res);

          // 成功獲取數據后會再次發(fā)送請求

          that.subscribe();

          }).catch(function(error) {

          // timeout 之后也會再次發(fā)送請求

          that.subscribe();

          });

          return this.unsubscribe;

          }

          // ....

          }

          筆者采用的是express,默認不支持hold住請求,因此用了一個express-longpoll的庫來實現(xiàn)。

          下面是一個原生不用庫的實現(xiàn)(這里只是介紹原理),整體的思路是:如果服務器端支持hold住請求的話,那么在一定的時間內會自輪詢,然后期間通過比較key值,判斷是否返回新數據。

          以下是具體思路:

          • 1)客戶端第一次會帶一個空的key值,這次會立即返回,獲取新內容,服務器端將計算出的contentKey返回給客戶端;
          • 2)然后客戶端發(fā)送第二次請求,帶上第一次返回的contentKey作為key值,然后進行下一輪的比較;
          • 3)如果兩次的key值相同,就會hold請求,進行內部輪詢,如果期間有新內容或者客戶端timeout,就會斷開連接;
          • 4)重復以上步驟。

          代碼如下:

          // 服務器端

          router.get('/v2/datas', function(req, res) {

          const key = _.get(req.query, 'key', '');

          let contentKey = chatRoom.getContentKey();


          while(key === contentKey) {

          sleep.sleep(5);

          contentKey = chatRoom.getContentKey();

          }


          const connectors = chatRoom.getConnectors();

          const messages = chatRoom.getMessages();

          res.json({

          code: 200,

          data: { connectors: connectors, messages: messages, key: contentKey },

          });

          });

          以下是用 express-longpoll的實現(xiàn)片段:

          // mini-chatroom/public/javascripts/server/longPolling.js

          function pushDataToClient(key, longpoll) {

          var contentKey = chatRoom.getContentKey();


          if(key !== contentKey) {

          var connectors = chatRoom.getConnectors();

          var messages = chatRoom.getMessages();


          long poll.publish(

          '/v2/datas',

          {

          code: 200,

          data: {connectors: connectors, messages: messages, key: contentKey},

          }

          );

          }

          }


          long poll.create("/v2/datas", function(req, res, next) {

          key = _.get(req.query, 'key', '');

          pushDataToClient(key, longpoll);

          next();

          });


          intervalId = setInterval(function() {

          pushDataToClient(key, longpoll);

          }, LONG_POLLING_TIMEOUT);

          PS:完整代碼,請見本文“4、本文配套Demo和代碼”一節(jié)。

          為了方便演示,我將客戶端發(fā)起請求的timeout改成了4s,注意觀察下面的截圖:

          可以看到,斷開連接的兩種方式,要么是超時,要么是請求有數據返回。

          6.3 基于iframe的長輪詢模式

          這是長輪詢技術的另一個種實現(xiàn)方案。

          該方案的具體的原理為:

          • 1)在頁面中嵌入一個iframe,地址指向輪詢的服務器地址,然后在父頁面中放置一個執(zhí)行函數,比如execute(data);
          • 2)當服務器有內容改變時,會向iframe發(fā)送一個腳本<script>parent.execute(JSON.stringify(data))</script>;
          • 3)通過發(fā)送的腳本,主動執(zhí)行父頁面中的方法,達到推送的效果。

          因不篇幅原因,在此不作深入介紹,有興趣的同學可以詳讀《新手入門貼:史上最全Web端即時通訊技術原理詳解》一文中的“3.3.2 基于iframe的數據流”一節(jié)。

          7、什么是Server-Sent Events(SSE)

          7.1 基本介紹

          從純技術的角度講:上兩節(jié)介紹的短輪詢和長輪詢技術,服務器端是無法主動給客戶端推送消息的,都是客戶端主動去請求服務器端獲取最新的數據。

          本節(jié)要介紹的SSE是一種可以主動從服務端推送消息的技術。

          SSE的本質其實就是一個HTTP的長連接,只不過它給客戶端發(fā)送的不是一次性的數據包,而是一個stream流,格式為text/event-stream。所以客戶端不會關閉連接,會一直等著服務器發(fā)過來的新的數據流,視頻播放就是這樣的例子。

          簡單來說,SSE就是:

          • 1)SSE 使用 HTTP 協(xié)議,現(xiàn)有的服務器軟件都支持。WebSocket 是一個獨立協(xié)議。
          • 2)SSE 屬于輕量級,使用簡單;WebSocket 協(xié)議相對復雜。
          • 3)SSE 默認支持斷線重連,WebSocket 需要自己實現(xiàn)。
          • 4)SSE 一般只用來傳送文本,二進制數據需要編碼后傳送,WebSocket 默認支持傳送二進制數據。
          • 5)SSE 支持自定義發(fā)送的消息類型。

          SSE的技術原理如下圖所示:

          SSE基本的使用方法,可以參看 SSE 的API文檔,地址是:https://developer.mozilla.org/en ... _server-sent_events。

          目前除了IE以及低版本的瀏覽器不支持,絕大多數的現(xiàn)代瀏覽器都支持SSE:

          上圖來自:https://caniuse.com/?search=Server-Sent-Events)

          7.2 代碼實現(xiàn)

          // 客戶端

          var SSENotification = {

          source: null,

          subscribe: function() {

          if('EventSource'inwindow) {

          this.source = newEventSource('/sse');


          this.source.addEventListener('message', function(res) {

          const d = res.data;

          window.ChatroomDOM.renderData(JSON.parse(d));

          });

          }

          return this.unsubscribe;

          },

          unsubscribe: function() {

          this.source && this.source.close();

          }

          }


          // 服務器端

          router.get('/sse', function(req, res) {

          const connectors = chatRoom.getConnectors();

          const messages = chatRoom.getMessages();

          const response = { code: 200, data: { connectors: connectors, messages: messages } };


          res.writeHead(200, {

          "Content-Type":"text/event-stream",

          "Cache-Control":"no-cache",

          "Connection":"keep-alive",

          "Access-Control-Allow-Origin": '*',

          });


          res.write("retry: 10000\n");

          res.write("data: "+ JSON.stringify(response) + "\n\n");


          var unsubscribe = Event.subscribe(function() {

          const connectors = chatRoom.getConnectors();

          const messages = chatRoom.getMessages();

          const response = { code: 200, data: { connectors: connectors, messages: messages } };

          res.write("data: "+ JSON.stringify(response) + "\n\n");

          });


          req.connection.addListener("close", function() {

          unsubscribe();

          }, false);

          });

          下面是控制臺的情況,注意觀察響應類型:

          詳情中注意查看請求類型,以及EventStream消息類型:

          PS:有關SSE更詳盡的資料就不在這里展開了,有興趣的同學可以詳讀《SSE技術詳解:一種全新的HTML5服務器推送事件技術》、《使用WebSocket和SSE技術實現(xiàn)Web端消息推送》。

          8、什么是WebSocket

          8.1 基本介紹

          PS:本小節(jié)內容引用自《Web端即時通訊實踐干貨:如何讓WebSocket斷網重連更快速?》一文的“3、快速了解WebSocket”。

          WebSocket誕生于2008年,在2011年成為國際標準,現(xiàn)在所有的瀏覽器都已支持(詳見《新手快速入門:WebSocket簡明教程》)。它是一種全新的應用層協(xié)議,是專門為web客戶端和服務端設計的真正的全雙工通信協(xié)議,可以類比HTTP協(xié)議來了解websocket協(xié)議。

          圖片引用自《WebSocket詳解(四):刨根問底HTTP與WebSocket的關系(上篇)

          它們的不同點:

          • 1)HTTP的協(xié)議標識符是http,WebSocket的是ws;
          • 2)HTTP請求只能由客戶端發(fā)起,服務器無法主動向客戶端推送消息,而WebSocket可以;
          • 3)HTTP請求有同源限制,不同源之間通信需要跨域,而WebSocket沒有同源限制。

          它們的相同點:

          • 1)都是應用層的通信協(xié)議;
          • 2)默認端口一樣,都是80或443;
          • 3)都可以用于瀏覽器和服務器間的通信;
          • 4)都基于TCP協(xié)議。

          兩者和TCP的關系圖:

          圖片引用自《新手快速入門:WebSocket簡明教程

          有關Http和WebSocket的關系,可以詳讀:

          《WebSocket詳解(四):刨根問底HTTP與WebSocket的關系(上篇)》

          《WebSocket詳解(五):刨根問底HTTP與WebSocket的關系(下篇)》

          有關WebSocket和Socket的關系,可以詳讀:《WebSocket詳解(六):刨根問底WebSocket與Socket的關系》.

          8.2 技術特征

          WebSocket技術特征總結下就是:

          • 1)可雙向通信,設計的目的主要是為了減少傳統(tǒng)輪詢時http連接數量的開銷;
          • 2)建立在TCP協(xié)議之上,握手階段采用 HTTP 協(xié)議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器;
          • 3)與HTTP兼容性良好,同樣可以使用80和443端口;
          • 4)沒有同源限制,客戶端可以與任意服務器通信;
          • 5)可以發(fā)送文本,也可以發(fā)送二進制數據;
          • 6)協(xié)議標識符是ws(如果加密,則為wss),服務器網址就是 URL.

          WebSocket的技術原理如下圖所示:

          關于WebSocket API方面的知識,這里不再作講解,可以自己查閱:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket

          8.3 瀏覽器兼容性

          WebSocket兼容性良好,基本支持所有現(xiàn)代瀏覽器。

          上圖來自:https://caniuse.com/mdn-api_websocket)

          8.4 代碼實現(xiàn)

          筆者這里采用的是socket.io,是基于WebSocket的封裝,提供了客戶端以及服務器端的支持。

          // 客戶端

          var WebsocketNotification = {

          // ...

          subscribe: function(args) {

          var connector = args[1];

          this.socket = io();

          this.socket.emit('register', connector);

          this.socket.on('register done', function() {

          window.ChatroomDOM.renderAfterRegister();

          });

          this.socket.on('data', function(res) {

          window.ChatroomDOM.renderData(res);

          });

          this.socket.on('disconnect', function() {

          window.ChatroomDOM.renderAfterLogout();

          });

          }

          // ...

          }


          // 服務器端

          var io = socketIo(httpServer);

          io.on('connection', (socket) => {

          socket.on('register', function(connector) {

          chatRoom.onConnect(connector);

          io.emit('register done');

          var data = chatRoom.getDatas();

          io.emit('data', { data });

          });

          socket.on('chat', function(message) {

          chatRoom.receive(message);

          var data = chatRoom.getDatas();

          io.emit('data', { data });

          });

          });

          PS:完整代碼,請見本文“4、本文配套Demo和代碼”一節(jié)。

          響應格式如下:

          8.5 深入學習

          隨著HTML5的普及率越來越高,WebSocket的應用也越來越普及,關于WebSocket的學習資料網上很容易找到,限于篇幅本文就不深入展開這個話題。

          如果想進一步深入學習WebSocket的方方面面,以下文章值得一讀:

          《新手快速入門:WebSocket簡明教程》

          《WebSocket詳解(一):初步認識WebSocket技術》

          《WebSocket詳解(二):技術原理、代碼演示和應用案例》

          《WebSocket詳解(三):深入WebSocket通信協(xié)議細節(jié)》

          《WebSocket詳解(四):刨根問底HTTP與WebSocket的關系(上篇)》

          《WebSocket詳解(五):刨根問底HTTP與WebSocket的關系(下篇)》

          《WebSocket詳解(六):刨根問底WebSocket與Socket的關系》

          《理論聯(lián)系實際:從零理解WebSocket的通信原理、協(xié)議格式、安全性》

          《微信小程序中如何使用WebSocket實現(xiàn)長連接(含完整源碼)》

          《八問WebSocket協(xié)議:為你快速解答WebSocket熱門疑問》

          《Web端即時通訊實踐干貨:如何讓你的WebSocket斷網重連更快速?》

          《WebSocket從入門到精通,半小時就夠!》

          《WebSocket硬核入門:200行代碼,教你徒手擼一個WebSocket服務器》

          《長連接網關技術專題(四):愛奇藝WebSocket實時推送網關技術實踐》

          9、本文小結

          短輪詢、長輪詢實現(xiàn)成本相對比較簡單,適用于一些實時性要求不高的消息推送,在實時性要求高的場景下,會存在延遲以及會給服務器帶來更大的壓力。

          SSE只能是服務器端推送消息,因此對于不需要雙向通信的項目比較適用。

          WebSocket目前而言實現(xiàn)成本相對較低,適合于雙工通信,對于多人在線,要求實時性較高的項目比較實用。

          學習交流:

          - 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》

          - 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK

          本文已同步發(fā)布于“即時通訊技術圈”公眾號。

          同步發(fā)布鏈接是:http://www.52im.net/thread-3555-1-1.html

          之前在搭建jumpserver的時候有用到了Guacamole,但是沒有對Guacamole做一個詳細的介紹,剛好在網上看到一篇Guacamole官方手冊的翻譯,所以整理了一些相關內容,下面介紹一下Guacamole的具體實現(xiàn)和架構。


          Guacamole

          Guacamole 是一個基于 HTML 5 和 JavaScript 的 VNC 查看器,服務端基于 Java 的 VNC-to-XML 代理開發(fā)。要求瀏覽器支持 HTML 5。目前該項目是 Apache 基金會的孵化項目。

          該項目包含三個組件,分別是:

          • apache/incubator-guacamole-server
          • apache/incubator-guacamole-client
          • apache/incubator-guacamole-website

          Guacamole 是無客戶端的遠程桌面網關,Guacamole 支持標準協(xié)議,比如 VNC 和 RDP。

          Guacamole 不要求任何插件和客戶端軟件。

          Guacamole 是提供遠程桌面的解決方案的開源項目,通過瀏覽器就能操作虛擬機,適用于Chrome,F(xiàn)irefox,IE10等瀏覽器(瀏覽器需要支持HTML5)。 由于使用 HTML5,Guancamole 只要在一個服務器安裝成功,你訪問你的桌面就是訪問一個 web 瀏覽器。


          Guacamole的介紹以及架構

          Guacamole不是一個獨立的Web應用程序,而是由許多部件組成的。Web應用程序實際上是整個項目里最小最輕量的,大部分的功能依靠Guacamole的底層組件來完成。

          用戶通過瀏覽器連接到Guacamole的服務端。Guacamole的客戶端是用javascript編寫的,Guacamole server通過web容器(比如tomcat)把服務提供給用戶。一旦加載,客戶端通過http承載著Guacamole自己的定義的協(xié)議與服務端通信。

          部署在Guacamole server這邊的Web應用程序,解析到的Guacamole protocal,就傳給Guacamole的代理guacd,這個代理(guacd)實際上就是解析Guacamole protocal,替用戶連接到遠程機器。

          Guacamole protocal協(xié)議本身以及guacd的存在,實現(xiàn)了協(xié)議的透明:Guacamole客戶端(瀏覽器運行的js)和Web應用程序,都不需要知道遠程桌面具體用哪個協(xié)議(VNC,RDP etc)


          Guacamole protocal協(xié)議

          Web應用程序是不知道任何遠程桌面協(xié)議。應用程序不包含支持VNC,RDP等其他協(xié)議的Guacamole模塊。應用程序只需要理解 Guacamole protocal協(xié)議,這個協(xié)議支持顯示渲染和消息傳輸。盡管Guacamole protocal的這些功能與一個遠程桌面協(xié)議類似,不過遠程桌面協(xié)議和Guacamole protocal背后的設計原則是不一樣的:Guacamole protocal并不是為了實現(xiàn)一個特定的桌面環(huán)境的遠程功能。

          Guacamole protocal作為一個遠程顯示和交互協(xié)議,實現(xiàn)了現(xiàn)有的遠程桌面協(xié)議的超集(superset)。為了讓Guacamole支持一個新的遠程桌面協(xié)議 (比如RDP),需要寫一個中間層(middle layer)來實現(xiàn)Guacamole protocal和這個新協(xié)議的轉換。實現(xiàn)這樣的轉換機制和本地客戶端遠程訪問桌面的實現(xiàn)沒什么區(qū)別,唯一的不同是這個轉換是要渲染遠程的顯示器(瀏覽 器),而不是本地客戶端(比如real vnc)。

          而實現(xiàn)這個協(xié)議互相轉換的中間層就是guacd。


          guacd

          guacd是Guacamole的“心臟”,插件式的動態(tài)支持遠程桌面協(xié)議,根據Web應用程序發(fā)來的指令連接到遠程桌面。

          guacd是和Guzcamole一起被安裝到機器的,以駐留后臺進程形式提供代理服務,接收來自Web應用程序的Tcp連接。guacd同樣也不 知道具體的遠程桌面協(xié)議,它只是需要決定加載那個插件并且傳送特定的參數給插件。(這個插件就是用來解析具體遠程桌面協(xié)議的)一旦這個插件被加載,插件就 獨立于guacd運行,對Web應用程序和自己之間的會話(conmunication)具有絕對的控制權,直到插件被關閉。


          Web應用程序

          在Guacamole中與用戶打交道的就是Web應用程序。

          之前說過,Web應用程序自己不實現(xiàn)任何的遠程桌面協(xié)議。Web應用程序依賴guacd,只關心優(yōu)美的界面以及權限認證。

          Web應用程序用Java語言編寫,對,只要你愿意,完全可以用其他語言實現(xiàn)。因為,事實上,Guacamole被設計成API,我們鼓勵開發(fā)者使用API開發(fā)自己的應用。


          RealMint

          說到Guacamole,一般是指一個遠程桌面的網關,但是也不完全是這樣。起初,Guacamole用javascript寫了一個純文本的Telnet客戶端叫做:RealMint(RealMint是一個終端的名字)。這個項目主要是寫了個示范程序,目的希望它能有用,它一度聲名鵲起是因為RealMint是只用javascript寫的。

          RealMint的隧道是用PHP寫的。跟Guacamole的HTTP的隧道相比,RealMint的隧道用的是簡單的長輪詢技術,比較低效。RealMint有一個比較像樣的鍵盤實現(xiàn)是保存至今,被應用到Guacamole的鍵盤部分的代碼,也許這就是RealMint唯一保留下來的功能和特性。

          鑒于RealMint只是實現(xiàn)了一個古老的協(xié)議(telnet),業(yè)界內還有幾個其他的比較成熟穩(wěn)定的javascript客戶端模擬器的實現(xiàn),RealMint這個項目就被遺棄了。


          VNC Client

          開發(fā)者一旦接觸HTML5的canvas標簽,就會發(fā)現(xiàn)這個標簽Firefox和Chrome已經支持,而且開始替代哪些所謂的Javascript實現(xiàn)的VNC的客戶端了。

          完全用javascript實現(xiàn)的客戶端加上java服務器的組件,工作原理是把VNC協(xié)議轉化成一樣的基于XML的版本。這樣的實現(xiàn)受限于 VNC的特性,無法把一個連接傳送給多個用戶。概念上的項目需要很好的線上環(huán)境,雖然發(fā)展有點滯后,一個HTML5的VNC客戶端已經在SourceForge上以“Guacamole”的名字注冊了。

          當Guacamole慢慢發(fā)展,變得不僅僅是一個概念上的產品時,需求快速增長,過去的像RealMint一樣采取長輪詢的XML的解決方案就慢慢被廢棄了。

          因為Websocket此時還無法被完全信賴,Java有沒有Websocket的servlets標準,一個替代品,基于HTTP的隧道解決方案應運而生。


          遠程桌面網關(Remote Desktop Gateway)

          一個更快的基于文本的協(xié)議被提出,它可以支持現(xiàn)有的大多數的遠程桌面協(xié)議,不僅僅是VNC。整個系統(tǒng)被重新設計成一個標準的后臺駐留進程(guacd)和一些公用的庫文件(libguac),可擴展地支持遠程桌面協(xié)議。

          這個項目從一個完整的VNC客戶端擴展成一個高性能的HTML5遠程桌面網關以及通用API。目前,Guacamole被用做一個中心網關,可以支持連接任意數量的,運行著不同遠程桌面協(xié)議的機器。提供可擴展地認證體系,這樣你可以做一些特定的適配,Guacamole也提供一個用戶Html5遠程 連接的通用API。


          篇幅有限,關于Guacamole的內容就介紹到這了,后面會分享更多devops和DBA方面的內容,感興趣的朋友可以關注一下~

          天這篇文章來介紹一下Nacos配置中心的原理之一:長輪詢機制的應用

          為方便理解與表達,這里把 Nacos 控制臺和 Nacos 注冊中心稱為 Nacos 服務器(就是 web 界面那個),我們編寫的業(yè)務服務稱為 Nacso 客戶端;

          Nacos 動態(tài)監(jiān)聽的長輪詢機制原理圖,本篇將圍繞這張圖剖析長輪詢定時機制的原理:

          ConfigService 是 Nacos 客戶端提供的用于訪問實現(xiàn)配置中心基本操作的類型,我們將從 ConfigService 的實例化開始長輪詢定時機制的源碼之旅;

          1. 客戶端的長輪詢定時機制

          我們從NacosPropertySourceLocator.locate()開始【斷點步入】:


          1.1 利用反射機制實例化 NacosConfigService 對象

          客戶端的長輪詢定時任務是在 NacosFactory.createConfigService() 方法中,構建 ConfigService 對象是實時啟動的,我們接著 1.1 處的源碼;

          進入 NacosFactory.createConfigService():

          public static ConfigService createConfigService(Properties properties) throws NacosException {
              //【斷點步入】創(chuàng)建 ConfigService
              return ConfigFactory.createConfigService(properties);
          }

          進入 ConfigFactory.createConfigService(),發(fā)現(xiàn)其使用反射機制實例化 NacosConfigService 對象;

          1.2 NacosConfigService 的構造方法里啟動長輪詢定時任務

          進入 NacosConfigService.NacosConfigService() 構造方法,里面設置了一些跟遠程任務相關的屬性;

          1.2.1 初始化 HttpAgent

          MetricsHttpAgent 類的設計如下:

          ServerHttpAgent 類的設計如下:

          1.2.2 初始化 ClientWorker

          進入 ClientWorker.ClientWorker() 構造方法,主要是創(chuàng)建了兩個定時調度的線程池,并啟動一個定時任務;

          進入 ClientWorker.checkConfigInfo(),每隔 10s 檢查一次配置是否發(fā)生變化;

          • cacheMap:是一個 AtomicReference<Map<String, CacheData>> 對象,用來存儲監(jiān)聽變更的緩存集合,key 是根據 datalD/group/tenant(租戶)拼接的值。Value 是對應的存儲在 Nacos 服務器上的配置文件的內容;
          • 長輪詢任務拆分:默認情況下,每個長輪詢 LongPollingRunnable 任務處理3000個監(jiān)聽配置集。如果超過3000個,則需要啟動多個 LongPollingRunnable 去執(zhí)行;

          1.3 檢查配置變更,讀取變更配置 LongPollingRunnable.run()

          因為我們沒有這么多配置項,debug 不進去,所以直接找到 LongPollingRunnable.run() 方法,該方法的主要邏輯是:

          根據 taskld 對 cacheMap 進行數據分割;

          再通過checkLocalConfig() 方法比較本地配置文件(在${user}\nacos\config\ 里)的數據是否存在變更,如果有變更則直接觸發(fā)通知;

          public void run() {
              List<CacheData> cacheDatas = new ArrayList();
              ArrayList inInitializingCacheList = new ArrayList();
              try {
                  //遍歷 CacheData,檢查本地配置
                  Iterator var3 = ((Map)ClientWorker.this.cacheMap.get()).values().iterator();
                  while(var3.hasNext()) {
                      CacheData cacheData = (CacheData)var3.next();
                      if (cacheData.getTaskId() == this.taskId) {
                          cacheDatas.add(cacheData);
                          try {
                              //檢查本地配置
                              ClientWorker.this.checkLocalConfig(cacheData);
                              if (cacheData.isUseLocalConfigInfo()) {
                                  cacheData.checkListenerMd5();
                              }
                          } catch (Exception var13) {
                              ClientWorker.LOGGER.error("get local config info error", var13);
                          }
                      }
                  }
                  //【斷點步入 1.3.1】通過長輪詢請求檢查服務端對應的配置是否發(fā)生變更
                  List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
                  //遍歷存在變更的 groupKey,重新加載最新數據
                  Iterator var16 = changedGroupKeys.iterator();
                  while(var16.hasNext()) {
                      String groupKey = (String)var16.next();
                      String[] key = GroupKey.parseKey(groupKey);
                      String dataId = key[0];
                      String group = key[1];
                      String tenant = null;
                      if (key.length == 3) {
                          tenant = key[2];
                      }
                      try {
                          //【斷點步入 1.3.2】讀取變更配置,這里的 dataId、group 和 tenant 是【1.3.1】里獲取的
                          String content = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
                          CacheData cache = (CacheData)((Map)ClientWorker.this.cacheMap.get()).get(GroupKey.getKeyTenant(dataId, group, tenant));
                          cache.setContent(content);
                          ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(content)});
                      } catch (NacosException var12) {
                          String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
                          ClientWorker.LOGGER.error(message, var12);
                      }
                  }
                  //觸發(fā)事件通知
                  var16 = cacheDatas.iterator();
                  while(true) {
                      CacheData cacheDatax;
                      do {
                          if (!var16.hasNext()) {
                              inInitializingCacheList.clear();
                              //繼續(xù)定時執(zhí)行當前線程
                              ClientWorker.this.executorService.execute(this);
                              return;
                          }
                          cacheDatax = (CacheData)var16.next();
                      } while(cacheDatax.isInitializing() && !inInitializingCacheList.contains(GroupKey.getKeyTenant(cacheDatax.dataId, cacheDatax.group, cacheDatax.tenant)));
                      cacheDatax.checkListenerMd5();
                      cacheDatax.setInitializing(false);
                  }
              } catch (Throwable var14) {
                  ClientWorker.LOGGER.error("longPolling error : ", var14);
                  ClientWorker.this.executorService.schedule(this, (long)ClientWorker.this.taskPenaltyTime, TimeUnit.MILLISECONDS);
              }
          }

          注意:這里的斷點需要注意 Nacos 服務器上修改配置(間隔大于 30s),進入后才好理解;

          1.3.1 檢查配置變更 ClientWorker.checkUpdateDataIds()

          我們點進 ClientWorker.checkUpdateDataIds()? 方法,發(fā)現(xiàn)其最終調用的是 ClientWorker.checkUpdateConfigStr() 方法,其實現(xiàn)邏輯與源碼如下:

          • 通過MetricsHttpAgent.httpPost()? 方法(上面 1.2.1 有提到)調用/v1/cs/configs/listener 接口實現(xiàn)長輪詢請求;
          • 輪詢請求在實現(xiàn)層面只是設置了一個比較長的超時時間,默認是 30s;
          • 如果服務端的數據發(fā)生了變更,客戶端會收到一個 HttpResult ,服務端返回的是存在數據變更的 Data ID、Group、Tenant;
          • 獲得這些信息之后,在LongPollingRunnable.run() 方法中調用 getServerConfig() 去 Nacos 服務器上讀取具體的配置內容;
          List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
              List<String> params = Arrays.asList("Listening-Configs", probeUpdateString);
              List<String> headers = new ArrayList(2);
              headers.add("Long-Pulling-Timeout");
              headers.add("" + this.timeout);
              if (isInitializingCacheList) {
                  headers.add("Long-Pulling-Timeout-No-Hangup");
                  headers.add("true");
              }
              if (StringUtils.isBlank(probeUpdateString)) {
                  return Collections.emptyList();
              } else {
                  try {
                      //調用 /v1/cs/configs/listener 接口實現(xiàn)長輪詢請求,返回的 HttpResult 里包含存在數據變更的 Data ID、Group、Tenant
                      HttpResult result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), this.timeout);
                      if (200 == result.code) {
                          this.setHealthServer(true);
                          //
                          return this.parseUpdateDataIdResponse(result.content);
                      }
                      this.setHealthServer(false);
                      LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.code);
                  } catch (IOException var6) {
                      this.setHealthServer(false);
                      LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var6);
                      throw var6;
                  }
                  return Collections.emptyList();
              }
          }

          1.3.2 讀取變更配置 ClientWorker.getServerConfig()

          進入 ClientWorker.getServerConfig()? 方法;讀取服務器上的變更配置;最終調用的是 MetricsHttpAgent.httpGet()? 方法(上面 1.2.1 有提到),調用 /v1/cs/configs? 接口獲取配置;然后通過調用 LocalConfigInfoProcessor.saveSnapshot() 將變更的配置保存到本地;

          2. 服務端的長輪詢定時機制

          2.1 服務器接收請求 ConfigController.listener()

          Nacos客戶端 通過 HTTP 協(xié)議與服務器通信,那么在服務器源碼里必然有對應接口的實現(xiàn);

          在 nacos-config 模塊下的 controller 包,提供了個 ConfigController 類來處理請求,其中有個 /listener 接口,是客戶端發(fā)起數據監(jiān)聽的接口,其主要邏輯和源碼如下:

          • 獲取客戶端需要監(jiān)聽的可能發(fā)生變化的配置,并計算 MD5 值;
          • ConfigServletInner.doPollingConfig() 開始執(zhí)行長輪詢請求;

          2.2 執(zhí)行長輪詢請求 ConfigSer

          vletInner.doPollingConfig()

          進入 ConfigServletInner.doPollingConfig() 方法,該方法封裝了長輪詢的實現(xiàn)邏輯,同時兼容短輪詢邏輯;

          進入 LongPollingService.addLongPollingClient() 方法,里面是長輪詢的核心處理邏輯,主要作用是把客戶端的長輪詢請求封裝成 ClientPolling 交給 scheduler 執(zhí)行;

          2.3 創(chuàng)建線程執(zhí)行定時任務 ClientLongPolling.run()

          我們找到 ClientLongPolling.run() 方法,這里可以體現(xiàn)長輪詢定時機制的核心原理,通俗來說,就是:

          服務端收到請求之后,不立即返回,沒有變更則在延后 (30-0.5)s 把請求結果返回給客戶端;

          這就使得客戶端和服務端之間在 30s 之內數據沒有發(fā)生變化的情況下一直處于連接狀態(tài);

          2.4 監(jiān)聽配置變更事件

          2.4.1 監(jiān)聽 LocalDataChangeEvent 事件的實現(xiàn)

          當我們在 Nacos 服務器或通過 API 方式變更配置后,會發(fā)布一個 LocalDataChangeEvent 事件,該事件會被 LongPollingService 監(jiān)聽;

          這里 LongPollingService 為什么具有監(jiān)聽功能在 1.3.1 版本后有些變化:

          • 1.3.1 前:LongPollingService.onEvent();
          • 1.3.1 后:Subscriber.onEvent();

          在 Nacos 1.3.1 版本之前,通過 LongPollingService 繼承 AbstractEventListener 實現(xiàn)監(jiān)聽,覆蓋 onEvent() 方法;

          而在 1.3.2 版本之后,通過構造訂閱者實現(xiàn)

          效果是一樣的,實現(xiàn)了對 LocalDataChangeEvent 事件的監(jiān)聽,并通過通過線程池執(zhí)行 DataChangeTask 任務;

          2.4.2 監(jiān)聽事件后的處理邏輯 DataChangeTask.run()

          我們找到 DataChangeTask.run() 方法,這個線程任務實現(xiàn)了

          3. 源碼結構圖小結

          3.1 客戶端的長輪詢定時機制

          • NacosPropertySourceLocator.locate() :初始化 ConfigService 對象,定位配置;
          • NacosConfigService.NacosConfigService():NacosConfigService 的構造方法;
          • Executors.newScheduledThreadPool():創(chuàng)建 executor 線程池;
          • Executors.newScheduledThreadPool():創(chuàng)建 executorService 線程池;
          • ClientWorker.checkConfigInfo():使用 executor 線程池檢查配置是否發(fā)生變化;
          • ClientWorker.checkLocalConfig():檢查本地配置;
          • ClientWorker.checkUpdateDataIds():檢查服務端對應的配置是否發(fā)生變更;
          • ClientWorker.getServerConfig():讀取變更配置
          • MetricsHttpAgent.httpPost():調用 /v1/cs/configs/listener 接口實現(xiàn)長輪詢請求;
          • ClientWorker.checkUpdateConfigStr():檢查服務端對應的配置是否發(fā)生變更;
          • MetricsHttpAgent.httpGet():調用 /v1/cs/configs 接口獲取配置;
          • LongPollingRunnable.run():運行長輪詢定時線程;
          • MetricsHttpAgent.MetricsHttpAgent():初始化 HttpAgent;
          • ClientWorker.ClientWorker():初始化 ClientWorker;
          • NacosFactory.createConfigService():創(chuàng)建配置服務器;
          • ConfigFactory.createConfigService():利用反射機制創(chuàng)建配置服務器;

          3.2 服務端的長輪詢定時機制

          • ConfigController.listener() :服務器接收請求;
          • LongPollingService.addLongPollingClient():長輪詢的核心處理邏輯,提前 500ms 返回響應;
          • ClientLongPolling.run():長輪詢定時機制的實現(xiàn)邏輯;
          • Map.put():將 ClientLongPolling 實例本身添加到 allSubs 隊列中;
          • Queue.remove():把 ClientLongPolling 實例本身從 allSubs 隊列中移除;
          • MD5Util.compareMd5():比較數據的 MD5 值;
          • LongPollingService.sendResponse():將變更的結果通過 response 返回給客戶端;
          • ConfigExecutor.scheduleLongPolling():啟動定時任務,延時時間為 29.5s;
          • HttpServletRequest.getHeader():獲取客戶端設置的請求超時時間;
          • MD5Util.compareMd5():和服務端的數據進行 MD5 對比;
          • ConfigExecutor.executeLongPolling():創(chuàng)建 ClientLongPolling 線程執(zhí)行定時任務;
          • MD5Util.getClientMd5Map():計算 MD5 值;
          • ConfigServletInner.doPollingConfig():執(zhí)行長輪詢請求;

          3.3 Nacos 服務器配置變更的事件監(jiān)聽

          Nacos 服務器上的配置發(fā)生變更后,發(fā)布一個 LocalDataChangeEvent 事件;

          Subscriber.onEvent() :監(jiān)聽 LocalDataChangeEvent 事件(1.3.2 版本后);

          • DataChangeTask.run():根據 groupKey 返回配置;
          • ConfigExecutor.executeLongPolling():通過線程池執(zhí)行 DataChangeTask 任務;

          原文鏈接:https://developer.51cto.com/article/713995.html


          主站蜘蛛池模板: 国产在线无码一区二区三区视频| 激情综合丝袜美女一区二区| 国产在线视频一区二区三区98 | 亚洲午夜一区二区三区| 美女视频在线一区二区三区| 在线精品亚洲一区二区三区 | 亚洲av乱码一区二区三区按摩 | 亚洲高清一区二区三区| 国产精品成人一区二区三区| 一区二区三区高清在线 | 亚洲AV噜噜一区二区三区| 国产精品亚洲综合一区在线观看 | 亚洲欧美日韩一区二区三区| 国产伦理一区二区三区| 国产亚洲欧洲Aⅴ综合一区| 色窝窝无码一区二区三区色欲| 亚洲福利电影一区二区?| 国产嫖妓一区二区三区无码| 亚洲一区精品无码| 韩国一区二区三区视频| 2014AV天堂无码一区| 亚洲一区二区三区久久| 日本在线视频一区| 亚洲福利电影一区二区?| 亚洲视频在线一区二区| 精品少妇ay一区二区三区| 八戒久久精品一区二区三区| 免费精品一区二区三区第35| 国产精品一区二区四区| 亚洲AV无码一区二区三区DV| 国产一区内射最近更新| 国产精品一区二区久久精品| 国产A∨国片精品一区二区| 国产精品综合AV一区二区国产馆 | 国产成人无码一区二区在线播放| 爆乳无码AV一区二区三区| 亚州日本乱码一区二区三区| 无码精品人妻一区二区三区AV| 国产经典一区二区三区蜜芽| 亚洲一区二区女搞男| 一区二区三区在线免费观看视频|