京東到家基於netty與websocket的實踐

背景

在京東到家商家中心繫統中,商家提出在 Web 端實現自動打印的需求,不須要人工盯守點擊打印,直接打印小票,以節約人工成本。java

解決思路web

關於問題的思考邏輯:面試

第一種:想到的是能夠用ajax來輪詢服務端獲取最新訂單,也就是pullajax

第二種:咱們是否能夠用相似推送的設計來實現,也就是pushspring

兩種思路咱們評估其優缺點:tomcat

ajax方式實現簡單,只須要定時從服務端pull數據便可,但也增長了不少次無效的輪詢, 無形中增長服務端無效查詢。websocket

push方式實現稍複雜,須要服務端與PC端保持鏈接,這就須要創建長鏈接,最終經過長鏈接的方式來實現push效果。網絡

通過討論,咱們選擇了第二種,訂單中心生產出的新訂單,經過MQ的方式推送給web端,最終得到一個比較好的用戶體驗。session

方案介紹架構

關於長鏈接方案的選擇,咱們參考了很多帖子,最終選擇使用websocket協議來實現長鏈接,相似場景如IM,服務端即時推送等都使用了這個協議。

接下來咱們比較一下websocket的框架,比較主流的有netty、tomcat、socketIO 三個框架。

基於支持websocket的容器,開發簡單,例如tomcat,但在高併發的支持不是很好,鏈接的時候容易鏈接斷開,還有就是依賴容器。

netty-socketIO是在netty4基礎之上作了一層封裝,效率如同netty同樣,是一個全平臺方案,友好的API,京東的logbook也是用了socketIO來傳遞日誌,也是咱們的一個備選方案。

netty是業內主流的NIO框架,netty對javaNIO作了封裝,讓開發者更多關注業務,下降開發成本,不少著名的RPC框架都採用了netty做爲傳輸層,友好的API,功能強大,內置了不少編解碼協議,實現websocket協議也是十分方便。

那咱們橫向比較一下這些框架。

因此在選型方面咱們仍是定位在socketIO 與 netty 上面,在兼顧擴展性與靈活性的同時,咱們也考慮到netty能夠提供http的功能,最終咱們選擇了使用netty,固然socketIO封裝了不少功能,也是十分強大,相比較來講netty更適合咱們,比較輕量。

netty的特性

netty具備異步非阻塞的特性,傳統IO是面向流的,NIO是面向緩衝區的,這也是它的非阻塞緣由所在。

netty的線程模型如圖所示:

這種模型就是咱們常說的Reactor模型,boss線程實際上是一個獨立的NIO線程池,用於接收client請求,默認線程池大小爲1,worker線程池用於處理具體的讀寫操做,默認線程池大小爲2*cpu個

在上述模型中要特別注意ExecutionHandler,ExecutionHandler是運行在worker線程中的,因此耗時的操做最好在線程池中運行, 好比IO或者計算,否則會影響整個netty的吞吐。

瞭解了這些,咱們根據本身的業務設計出流程以下圖所示:

步驟(1) web端請求服務端進行註冊,註冊成功保持長鏈接。

步驟(2)服務端發送MQ。

步驟(3)netty將收到的消息推送給web端。

步驟(4)web端調用打印控件進行打印,打印控件需提早安裝好(打印控件是pc上安裝的一個驅動程序,用過JS方式來調用)。

若是調用JS成功,控件將把打印信息放入打印隊列,若是不成功,重複步驟(4)

固然如今的結構只是單機版,不知足生產條件,那未來的結構可能會演變成以下圖所示:

咱們會在服務端與netty之間創建路由層,路由層的主要職責:

第一:收集集羣存活信息。

第二:記錄落點,落在哪一臺機器上面

第三:接收消息與分發消息

有了這三種能力,咱們就能夠輕鬆的指定信息分發策略。這裏咱們但願使用http協議來路由,因此就須要netty有http短鏈接接收的能力 ,因此netty總體上須要長短鏈接兩種能力。

講了這麼多,仍是來點乾貨,下面是部分代碼。

netty啓動類,咱們經過spring來啓動netty,由於netty啓動會阻塞主線程,因此須要在子線程中來啓動netty,下面是啓動參數。

接着來寫咱們的ChannelInitializer,HttpServerCodec爲編解碼器,WSServerProtocolHandler爲websocket協議握手,其中咱們更關注業務層面自定義的兩個hander,httpRequestHandler,authorizeHandler。

httpRequestHandler的做用是處理url是否合法,接收參數,httpRequestHandler此方法中也能夠根據URI來過濾,自定義本身的短鏈接請求。

authorizeHandler的做用是校驗數據是否正確,若是正確會將channel保存到map中,經過map創建起業務ID與通道之間的關係。

校驗的過程咱們在authorizeHandler中的channelRead展開,若是未經過,直接關閉當前channel,若是經過校驗,則經過ctx.fireChannelRead(msg);方法將信息傳入下一個handler去處理。

在項目裏主要是以傳遞參數來進行數據校驗的,也就是經過URL傳參來實現。在httpRequestHandler中咱們將URL參數set到channel的attr中,並傳遞給了下一個handler,也就是authorizeHandler,因此在authorize方法中咱們能夠利用get()方法獲得參數值,u是通過加密的數據,咱們須要在這裏進行解密,解密失敗,可認爲校驗失敗。

固然若是有跨應用的服務,也能夠經過Cookie的方式來進行加密串的讀寫,經過request.getHeader 是能夠獲取Cookie中的信息,這就看具體業務了,示例代碼以下:

這個map 能夠理解爲servlet中的session,當有信息須要傳送給某個客戶端時,咱們調用map.get(key)方式的到當前該客戶端的channel,調用writeAndFlush方法將信息發送出去,下面舉例經過接收MQ消息後的處理邏輯。

接下來有人可能想到,那若是通道關閉了怎麼辦?map中的channel是否是就失效了呢?那其實咱們還須要有一個相似心跳的機制去維護channel,間接的去維護這個map,若是是通道正常關閉,能夠經過channelInactive方法來監聽,若是是長時間空閒:在項目中咱們使用了增長的IdleStateHandler來處理,經過覆蓋userEventTriggered方法來監聽空閒channel,當某個channel到達咱們設置的超時時間時,netty會回調此方法。

至此,核心部分已經處理完成,剩下的就是經過保存的channel來發送信息給客戶端了。

最後在web端,咱們採用了 reconnecting-websocket,它是一個小型的 JavaScript 庫,封裝了 WebSocket API, 提供了在鏈接斷開時自動重連的機制,很可以幫助咱們完成斷開重連的操做。

遇到的問題

通過測試,在ws的uri後面不能傳遞參數,否則在netty實現websocket協議握手的時候會出現斷開鏈接的狀況,針對這種狀況在websocketHandler以前作了一層httpHander過濾,將傳遞參數放入channel的attr中,而後重寫request的uri,並傳入下一個管道中,基本上解決這個問題。

在讀寫空閒的時候儘可能以發心跳包的方式維護鏈接,但在客戶端因爲網絡不穩定或者是服務端重啓,鏈接會斷開,瞬間有可能接收不到訂單消息,爲此在客戶端須要實現斷開重連機制,此問題咱們採用 reconnecting-websocket的js框架,此框架擴展了原生websocket的實現,作了斷開重連機制,有效的防止斷開後不能及時鏈接。

在測試過程當中因爲控件與小票機的問題,可能會出現打印異常或者小票機沒紙的狀況,Lodop控件實際上是將打印信息放入電腦的打印隊列,若是沒紙了,小票機會報警,再次放入小票紙,打印機會自動打印隊列中的數據。

出現調用控件異常偶爾發生,如今處理辦法是在js中進行了的try catch 若是失敗 進行重試,重試次數自定義,超太重試次數暫不作處理,此處還不太嚴謹,須要在進行優化。

總結

經過上面的實踐,咱們基本已經實現了web端的自動打印,通過長時間的內部測試,服務端與客戶端通訊穩定,咱們將灰度商家作用戶體驗。

在特定的場景下,選擇適當的技術會提升咱們的效率,不然會拔苗助長。選擇長鏈接,你們能夠把握三個大原則:

服務端是否須要主動推送數據到客戶端以實現控制的效果。

對於實時性的要求是否苛刻。

對於客戶端是否須要關注其在線狀態的實時變化。

以爲不錯請點贊支持,歡迎留言或進個人我的羣855801563領取【架構資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本羣專用於學習交流技術、分享面試機會,拒絕廣告,我也會在羣內不按期答題、探討。

相關文章
相關標籤/搜索