文來自“糊糊糊糊糊了”的分享,原題《實時消息推送整理》,有優(yōu)化和改動。
對Web端即時通訊技術熟悉的開發(fā)者來說,我們回顧網頁端IM的底層通信技術,從短輪詢、長輪詢,到后來的SSE以及WebSocket,使用門檻越來越低(早期的長輪詢Comet這類技術實際屬于hack手段,使用門檻并不低),技術手段越來越先進,網頁端即時通訊技術的體驗也因此越來越好。
但上周在編輯《IM掃碼登錄技術專題》系列文章第3篇的時候忽然想到,之前的這些所謂的網頁端即時通訊“老技術”相對于當紅的WebSocket,并非毫無用武之地。就拿IM里的掃碼登錄功能來說,用短輪詢技術就非常合適,完全沒必要大炮打蚊子上WebSocket。
所以,很多時候沒必要盲目追求新技術,相對應用場景來說適合的才是最好的。對于即時通訊網的im和消息推送這類即時通訊技術開發(fā)者來說,掌握WebSocket固然很重要,但了解短輪詢、長輪詢等這些所謂的Web端即時通訊“老技術”仍然大有裨益,這也正是整理分享本文的重要原因。
[1] 新手入門貼:史上最全Web端即時通訊技術原理詳解
[2] 詳解Web端通信方式的演進:從Ajax、JSONP 到 SSE、Websocket
[3] Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE
對于IM/消息推送這類即時通訊系統(tǒng)而言,系統(tǒng)的關鍵就是“實時通信”能力。
從表面意思上來看,“實時通信”指的是:
類比于傳統(tǒng)的C/S請求模型,“實時通信”時客戶端不需要主觀地發(fā)送請求去獲取自己關心的內容,而是由服務器端進行“推送”。
注意:上面的“推送”二字打了引號,實際上現(xiàn)有的幾種技術實現(xiàn)方式中,并不是服務器端真正主動地推送,而是通過一定的手段營造了一種“實時通信”的假象。
就目前現(xiàn)有的幾種技術而言,主要有以下幾類:
以下正文將針對這幾種技術方案,為你一一解惑。
為了幫助讀者更好的理解本文內容,筆者專門寫了一個較完整的Demo,Demo會以一個簡易聊天室的例子來分別通過上述的四種技術方式實現(xiàn)(代碼存在些許bug,主要是為了做演示用,別介意)。
完整Demo源碼打包下載:
(請從同步鏈接附件中下載:http://www.52im.net/thread-3555-1-1.html)
Demo的運行效果(動圖):
有興趣可以自行下載研究學習。
短輪詢的實現(xiàn)原理:
邏輯如下圖所示:
使用短輪詢的優(yōu)點:基礎不需要額外的開發(fā)成本,請求數據,解析數據,作出響應,僅此而已,然后不斷重復。
缺點也顯而易見:
短輪詢客戶的代碼實現(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運行效果(動圖):
長輪詢的基本原理:
邏輯如下圖所示:
長輪詢是基于短輪詢上的改進版本:主要是減少了客戶端發(fā)起Http連接的開銷,改成了在服務器端主動地去判斷所關心的內容是否變化。
所以其實輪詢的本質并沒有多大變化,變化的點在于:
長輪詢客戶的代碼實現(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值,判斷是否返回新數據。
以下是具體思路:
代碼如下:
// 服務器端
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,注意觀察下面的截圖:
可以看到,斷開連接的兩種方式,要么是超時,要么是請求有數據返回。
這是長輪詢技術的另一個種實現(xiàn)方案。
該方案的具體的原理為:
因不篇幅原因,在此不作深入介紹,有興趣的同學可以詳讀《新手入門貼:史上最全Web端即時通訊技術原理詳解》一文中的“3.3.2 基于iframe的數據流”一節(jié)。
從純技術的角度講:上兩節(jié)介紹的短輪詢和長輪詢技術,服務器端是無法主動給客戶端推送消息的,都是客戶端主動去請求服務器端獲取最新的數據。
本節(jié)要介紹的SSE是一種可以主動從服務端推送消息的技術。
SSE的本質其實就是一個HTTP的長連接,只不過它給客戶端發(fā)送的不是一次性的數據包,而是一個stream流,格式為text/event-stream。所以客戶端不會關閉連接,會一直等著服務器發(fā)過來的新的數據流,視頻播放就是這樣的例子。
簡單來說,SSE就是:
SSE的技術原理如下圖所示:
SSE基本的使用方法,可以參看 SSE 的API文檔,地址是:https://developer.mozilla.org/en ... _server-sent_events。
目前除了IE以及低版本的瀏覽器不支持,絕大多數的現(xiàn)代瀏覽器都支持SSE:
(上圖來自:https://caniuse.com/?search=Server-Sent-Events)
// 客戶端
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端消息推送》。
PS:本小節(jié)內容引用自《Web端即時通訊實踐干貨:如何讓WebSocket斷網重連更快速?》一文的“3、快速了解WebSocket”。
WebSocket誕生于2008年,在2011年成為國際標準,現(xiàn)在所有的瀏覽器都已支持(詳見《新手快速入門:WebSocket簡明教程》)。它是一種全新的應用層協(xié)議,是專門為web客戶端和服務端設計的真正的全雙工通信協(xié)議,可以類比HTTP協(xié)議來了解websocket協(xié)議。
(圖片引用自《WebSocket詳解(四):刨根問底HTTP與WebSocket的關系(上篇)》)
它們的不同點:
它們的相同點:
兩者和TCP的關系圖:
(圖片引用自《新手快速入門:WebSocket簡明教程》)
有關Http和WebSocket的關系,可以詳讀:
《WebSocket詳解(四):刨根問底HTTP與WebSocket的關系(上篇)》
《WebSocket詳解(五):刨根問底HTTP與WebSocket的關系(下篇)》
有關WebSocket和Socket的關系,可以詳讀:《WebSocket詳解(六):刨根問底WebSocket與Socket的關系》.
WebSocket技術特征總結下就是:
WebSocket的技術原理如下圖所示:
關于WebSocket API方面的知識,這里不再作講解,可以自己查閱:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
WebSocket兼容性良好,基本支持所有現(xiàn)代瀏覽器。
(上圖來自:https://caniuse.com/mdn-api_websocket)
筆者這里采用的是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é)。
響應格式如下:
隨著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實時推送網關技術實踐》
短輪詢、長輪詢實現(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 是一個基于 HTML 5 和 JavaScript 的 VNC 查看器,服務端基于 Java 的 VNC-to-XML 代理開發(fā)。要求瀏覽器支持 HTML 5。目前該項目是 Apache 基金會的孵化項目。
該項目包含三個組件,分別是:
Guacamole 是無客戶端的遠程桌面網關,Guacamole 支持標準協(xié)議,比如 VNC 和 RDP。
Guacamole 不要求任何插件和客戶端軟件。
Guacamole 是提供遠程桌面的解決方案的開源項目,通過瀏覽器就能操作虛擬機,適用于Chrome,F(xiàn)irefox,IE10等瀏覽器(瀏覽器需要支持HTML5)。 由于使用 HTML5,Guancamole 只要在一個服務器安裝成功,你訪問你的桌面就是訪問一個 web 瀏覽器。
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)
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是Guacamole的“心臟”,插件式的動態(tài)支持遠程桌面協(xié)議,根據Web應用程序發(fā)來的指令連接到遠程桌面。
guacd是和Guzcamole一起被安裝到機器的,以駐留后臺進程形式提供代理服務,接收來自Web應用程序的Tcp連接。guacd同樣也不 知道具體的遠程桌面協(xié)議,它只是需要決定加載那個插件并且傳送特定的參數給插件。(這個插件就是用來解析具體遠程桌面協(xié)議的)一旦這個插件被加載,插件就 獨立于guacd運行,對Web應用程序和自己之間的會話(conmunication)具有絕對的控制權,直到插件被關閉。
在Guacamole中與用戶打交道的就是Web應用程序。
之前說過,Web應用程序自己不實現(xiàn)任何的遠程桌面協(xié)議。Web應用程序依賴guacd,只關心優(yōu)美的界面以及權限認證。
Web應用程序用Java語言編寫,對,只要你愿意,完全可以用其他語言實現(xiàn)。因為,事實上,Guacamole被設計成API,我們鼓勵開發(fā)者使用API開發(fā)自己的應用。
說到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這個項目就被遺棄了。
開發(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的隧道解決方案應運而生。
一個更快的基于文本的協(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 的實例化開始長輪詢定時機制的源碼之旅;
我們從NacosPropertySourceLocator.locate()開始【斷點步入】:
客戶端的長輪詢定時任務是在 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 對象;
進入 NacosConfigService.NacosConfigService() 構造方法,里面設置了一些跟遠程任務相關的屬性;
MetricsHttpAgent 類的設計如下:
ServerHttpAgent 類的設計如下:
進入 ClientWorker.ClientWorker() 構造方法,主要是創(chuàng)建了兩個定時調度的線程池,并啟動一個定時任務;
進入 ClientWorker.checkConfigInfo(),每隔 10s 檢查一次配置是否發(fā)生變化;
因為我們沒有這么多配置項,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),進入后才好理解;
我們點進 ClientWorker.checkUpdateDataIds()? 方法,發(fā)現(xiàn)其最終調用的是 ClientWorker.checkUpdateConfigStr() 方法,其實現(xiàn)邏輯與源碼如下:
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();
}
}
進入 ClientWorker.getServerConfig()? 方法;讀取服務器上的變更配置;最終調用的是 MetricsHttpAgent.httpGet()? 方法(上面 1.2.1 有提到),調用 /v1/cs/configs? 接口獲取配置;然后通過調用 LocalConfigInfoProcessor.saveSnapshot() 將變更的配置保存到本地;
Nacos客戶端 通過 HTTP 協(xié)議與服務器通信,那么在服務器源碼里必然有對應接口的實現(xiàn);
在 nacos-config 模塊下的 controller 包,提供了個 ConfigController 類來處理請求,其中有個 /listener 接口,是客戶端發(fā)起數據監(jiān)聽的接口,其主要邏輯和源碼如下:
進入 ConfigServletInner.doPollingConfig() 方法,該方法封裝了長輪詢的實現(xiàn)邏輯,同時兼容短輪詢邏輯;
進入 LongPollingService.addLongPollingClient() 方法,里面是長輪詢的核心處理邏輯,主要作用是把客戶端的長輪詢請求封裝成 ClientPolling 交給 scheduler 執(zhí)行;
我們找到 ClientLongPolling.run() 方法,這里可以體現(xiàn)長輪詢定時機制的核心原理,通俗來說,就是:
服務端收到請求之后,不立即返回,沒有變更則在延后 (30-0.5)s 把請求結果返回給客戶端;
這就使得客戶端和服務端之間在 30s 之內數據沒有發(fā)生變化的情況下一直處于連接狀態(tài);
當我們在 Nacos 服務器或通過 API 方式變更配置后,會發(fā)布一個 LocalDataChangeEvent 事件,該事件會被 LongPollingService 監(jiān)聽;
這里 LongPollingService 為什么具有監(jiān)聽功能在 1.3.1 版本后有些變化:
在 Nacos 1.3.1 版本之前,通過 LongPollingService 繼承 AbstractEventListener 實現(xiàn)監(jiān)聽,覆蓋 onEvent() 方法;
而在 1.3.2 版本之后,通過構造訂閱者實現(xiàn)
效果是一樣的,實現(xiàn)了對 LocalDataChangeEvent 事件的監(jiān)聽,并通過通過線程池執(zhí)行 DataChangeTask 任務;
我們找到 DataChangeTask.run() 方法,這個線程任務實現(xiàn)了
Nacos 服務器上的配置發(fā)生變更后,發(fā)布一個 LocalDataChangeEvent 事件;
Subscriber.onEvent() :監(jiān)聽 LocalDataChangeEvent 事件(1.3.2 版本后);
原文鏈接:https://developer.51cto.com/article/713995.html
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。