您好,登錄后才能下訂單哦!
背景
在京東到家商家中心系統(tǒng)中,商家提出在 Web 端實(shí)現(xiàn)自動(dòng)打印的需求,不需要人工盯守點(diǎn)擊打印,直接打印小票,以節(jié)約人工成本。
解決思路
關(guān)于問題的思考邏輯:
第一種:想到的是可以用ajax來輪詢服務(wù)端獲取最新訂單,也就是pull。
第二種:我們是否可以用類似推送的設(shè)計(jì)來實(shí)現(xiàn),也就是push。
兩種思路我們?cè)u(píng)估其優(yōu)缺點(diǎn):
ajax方式實(shí)現(xiàn)簡單,只需要定時(shí)從服務(wù)端pull數(shù)據(jù)即可,但也增加了很多次無效的輪詢, 無形中增加服務(wù)端無效查詢。
push方式實(shí)現(xiàn)稍復(fù)雜,需要服務(wù)端與PC端保持連接,這就需要建立長連接,最終通過長連接的方式來實(shí)現(xiàn)push效果。
經(jīng)過討論,我們選擇了第二種,訂單中心生產(chǎn)出的新訂單,通過MQ的方式推送給web端,最終獲得一個(gè)比較好的用戶體驗(yàn)。
方案介紹
關(guān)于長連接方案的選擇,我們參考了不少帖子,最終選擇使用websocket協(xié)議來實(shí)現(xiàn)長連接,類似場(chǎng)景如IM,服務(wù)端即時(shí)推送等都使用了這個(gè)協(xié)議。
接下來我們比較一下websocket的框架,比較主流的有netty、tomcat、socketIO 三個(gè)框架。
基于支持websocket的容器,開發(fā)簡單,例如tomcat,但在高并發(fā)的支持不是很好,連接的時(shí)候容易連接斷開,還有就是依賴容器。
netty-socketIO是在netty4基礎(chǔ)之上做了一層封裝,效率如同netty一樣,是一個(gè)全平臺(tái)方案,友好的API,京東的logbook也是用了socketIO來傳遞日志,也是我們的一個(gè)備選方案。
netty是業(yè)內(nèi)主流的NIO框架,netty對(duì)javaNIO做了封裝,讓開發(fā)者更多關(guān)注業(yè)務(wù),降低開發(fā)成本,很多著名的RPC框架都采用了netty作為傳輸層,友好的API,功能強(qiáng)大,內(nèi)置了很多編解碼協(xié)議,實(shí)現(xiàn)websocket協(xié)議也是十分方便。
那我們橫向比較一下這些框架。
所以在選型方面我們還是定位在socketIO 與 netty 上面,在兼顧擴(kuò)展性與靈活性的同時(shí),我們也考慮到netty可以提供http的功能,最終我們選擇了使用netty,當(dāng)然socketIO封裝了很多功能,也是十分強(qiáng)大,相比較來說netty更適合我們,比較輕量。
netty的特性
netty具有異步非阻塞的特性,傳統(tǒng)IO是面向流的,NIO是面向緩沖區(qū)的,這也是它的非阻塞原因所在。
netty的線程模型如圖所示:
這種模型就是我們常說的Reactor模型,boss線程其實(shí)是一個(gè)獨(dú)立的NIO線程池,用于接收client請(qǐng)求,默認(rèn)線程池大小為1,worker線程池用于處理具體的讀寫操作,默認(rèn)線程池大小為2*cpu個(gè)數(shù)。
在上述模型中要特別注意ExecutionHandler,ExecutionHandler是運(yùn)行在worker線程中的,所以耗時(shí)的操作最好在線程池中運(yùn)行, 比如IO或者計(jì)算,不然會(huì)影響整個(gè)netty的吞吐。
了解了這些,我們根據(jù)自己的業(yè)務(wù)設(shè)計(jì)出流程如下圖所示:
步驟(1) web端請(qǐng)求服務(wù)端進(jìn)行注冊(cè),注冊(cè)成功保持長連接。
步驟(2)服務(wù)端發(fā)送MQ。
步驟(3)netty將收到的消息推送給web端。
步驟(4)web端調(diào)用打印控件進(jìn)行打印,打印控件需提前安裝好(打印控件是pc上安裝的一個(gè)驅(qū)動(dòng)程序,用過JS方式來調(diào)用)。
如果調(diào)用JS成功,控件將把打印信息放入打印隊(duì)列,如果不成功,重復(fù)步驟(4)
當(dāng)然現(xiàn)在的結(jié)構(gòu)只是單機(jī)版,不滿足生產(chǎn)條件,那將來的結(jié)構(gòu)可能會(huì)演變成如下圖所示:
我們會(huì)在服務(wù)端與netty之間建立路由層,路由層的主要職責(zé):
第一:收集集群存活信息。
第二:記錄落點(diǎn),就是落在哪一臺(tái)機(jī)器上面。
第三:接收消息與分發(fā)消息。
有了這三種能力,我們就可以輕松的指定信息分發(fā)策略。這里我們希望使用http協(xié)議來路由,所以就需要netty有http短連接接收的能力 ,所以netty整體上需要長短連接兩種能力。
講了這么多,還是來點(diǎn)干貨,下面是部分代碼。
netty啟動(dòng)類,我們通過spring來啟動(dòng)netty,因?yàn)閚etty啟動(dòng)會(huì)阻塞主線程,所以需要在子線程中來啟動(dòng)netty,下面是啟動(dòng)參數(shù)。
接著來寫我們的ChannelInitializer,HttpServerCodec為編×××,WSServerProtocolHandler為websocket協(xié)議握手,其中我們更關(guān)注業(yè)務(wù)層面自定義的兩個(gè)hander,httpRequestHandler,authorizeHandler。
httpRequestHandler的作用是處理url是否合法,接收參數(shù),httpRequestHandler此方法中也可以根據(jù)URI來過濾,自定義自己的短連接請(qǐng)求。
authorizeHandler的作用是校驗(yàn)數(shù)據(jù)是否正確,如果正確會(huì)將channel保存到map中,通過map建立起業(yè)務(wù)ID與通道之間的關(guān)系。
校驗(yàn)的過程我們?cè)赼uthorizeHandler中的channelRead展開,如果未通過,直接關(guān)閉當(dāng)前channel,如果通過校驗(yàn),則通過ctx.fireChannelRead(msg);方法將信息傳入下一個(gè)handler去處理。
在項(xiàng)目里主要是以傳遞參數(shù)來進(jìn)行數(shù)據(jù)校驗(yàn)的,也就是通過URL傳參來實(shí)現(xiàn)。在httpRequestHandler中我們將URL參數(shù)set到channel的attr中,并傳遞給了下一個(gè)handler,也就是authorizeHandler,所以在authorize方法中我們可以利用get()方法得到參數(shù)值,u是經(jīng)過加密的數(shù)據(jù),我們需要在這里進(jìn)行解密,解密失敗,可認(rèn)為校驗(yàn)失敗。
當(dāng)然如果有跨應(yīng)用的服務(wù),也可以通過Cookie的方式來進(jìn)行加密串的讀寫,通過request.getHeader 是可以獲取Cookie中的信息,這就看具體業(yè)務(wù)了,示例代碼如下:
這個(gè)map 可以理解為servlet中的session,當(dāng)有信息需要傳送給某個(gè)客戶端時(shí),我們調(diào)用map.get(key)方式的到當(dāng)前該客戶端的channel,調(diào)用writeAndFlush方法將信息發(fā)送出去,下面舉例通過接收MQ消息后的處理邏輯。
接下來有人可能想到,那如果通道關(guān)閉了怎么辦?map中的channel是不是就失效了呢?那其實(shí)我們還需要有一個(gè)類似心跳的機(jī)制去維護(hù)channel,間接的去維護(hù)這個(gè)map,如果是通道正常關(guān)閉,可以通過channelInactive方法來監(jiān)聽,如果是長時(shí)間空閑:在項(xiàng)目中我們使用了增加的IdleStateHandler來處理,通過覆蓋userEventTriggered方法來監(jiān)聽空閑channel,當(dāng)某個(gè)channel到達(dá)我們?cè)O(shè)置的超時(shí)時(shí)間時(shí),netty會(huì)回調(diào)此方法。
至此,核心部分已經(jīng)處理完成,剩下的就是通過保存的channel來發(fā)送信息給客戶端了。
最后在web端,我們采用了 reconnecting-websocket,它是一個(gè)小型的 JavaScript 庫,封裝了 WebSocket API, 提供了在連接斷開時(shí)自動(dòng)重連的機(jī)制,很能夠幫助我們完成斷開重連的操作。
遇到的問題
經(jīng)過測(cè)試,在ws的uri后面不能傳遞參數(shù),不然在netty實(shí)現(xiàn)websocket協(xié)議握手的時(shí)候會(huì)出現(xiàn)斷開連接的情況,針對(duì)這種情況在websocketHandler之前做了一層httpHander過濾,將傳遞參數(shù)放入channel的attr中,然后重寫request的uri,并傳入下一個(gè)管道中,基本上解決這個(gè)問題。
在讀寫空閑的時(shí)候盡量以發(fā)心跳包的方式維護(hù)連接,但在客戶端由于網(wǎng)絡(luò)不穩(wěn)定或者是服務(wù)端重啟,連接會(huì)斷開,瞬間有可能接收不到訂單消息,為此在客戶端需要實(shí)現(xiàn)斷開重連機(jī)制,此問題我們采用?reconnecting-websocket的js框架,此框架擴(kuò)展了原生websocket的實(shí)現(xiàn),做了斷開重連機(jī)制,有效的防止斷開后不能及時(shí)連接。
在測(cè)試過程中由于控件與小票機(jī)的問題,可能會(huì)出現(xiàn)打印異常或者小票機(jī)沒紙的情況,Lodop控件其實(shí)是將打印信息放入電腦的打印隊(duì)列,如果沒紙了,小票機(jī)會(huì)報(bào)警,再次放入小票紙,打印機(jī)會(huì)自動(dòng)打印隊(duì)列中的數(shù)據(jù)。
出現(xiàn)調(diào)用控件異常偶爾發(fā)生,現(xiàn)在處理辦法是在js中進(jìn)行了的try catch 如果失敗 進(jìn)行重試,重試次數(shù)自定義,超過重試次數(shù)暫不做處理,此處還不太嚴(yán)謹(jǐn),需要在進(jìn)行優(yōu)化。
總結(jié)
通過上面的實(shí)踐,我們基本已經(jīng)實(shí)現(xiàn)了web端的自動(dòng)打印,經(jīng)過長時(shí)間的內(nèi)部測(cè)試,服務(wù)端與客戶端通信穩(wěn)定,我們將灰度商家做用戶體驗(yàn)。
在特定的場(chǎng)景下,選擇適當(dāng)?shù)募夹g(shù)會(huì)提高我們的效率,否則會(huì)適得其反。選擇長連接,大家可以把握三個(gè)大原則:
服務(wù)端是否需要主動(dòng)推送數(shù)據(jù)到客戶端以實(shí)現(xiàn)控制的效果。
對(duì)于實(shí)時(shí)性的要求是否苛刻。
對(duì)于客戶端是否需要關(guān)注其在線狀態(tài)的實(shí)時(shí)變化。
覺得不錯(cuò)請(qǐng)點(diǎn)贊支持,歡迎留言或進(jìn)我的個(gè)人群855801563領(lǐng)取【架構(gòu)資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用于學(xué)習(xí)交流技術(shù)、分享面試機(jī)會(huì),拒絕廣告,我也會(huì)在群內(nèi)不定期答題、探討。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。