更新時間:2019-08-04 09:00:00 來源:動力節(jié)點 瀏覽2439次
Netty是Java程序員通向高階之路必須要過的門檻之一。干了幾年的Java程序員發(fā)現業(yè)務開發(fā)似乎就是在SSH的世界里摸滾打爬的時候,會開始感到迷茫,難道程序員的日子就是如此枯燥么?深入使用一下Netty,另一個世界的大門就會開始打開。枯燥的編碼會漸漸變得有趣,自主思考的能力也會開始加強。
Netty是建立在Java NIO基礎之上最廣泛使用的高性能網絡框架。了解Netty之前,必須對NIO的概念有所了解。
NIO的意思是非阻塞IO,也就是說單個線程可以同時進行多個IO操作,而不會被任何IO操作阻塞住。同一個線程即能同時Accept網絡套件字,又可以同時對套件字進行讀寫操作,然后還可以同時處理消息。
NIO基于事件機制,所有的IO操作都能抽象成一個事件。當新連接到來時,可以從內核中拿到ServerSocket的可讀事件。當連接上的消息到來時,可以從內核中拿到Socket的讀事件。當Socket中的緩沖區(qū)未滿的時候,可以從內核中拿到Socket的可寫事件。
當NIO線程從內核中拿到一個事件Event,就會開始使用相應的事件處理器EventHandler對這個事件進行處理。如果拿到ServerSocket可讀事件,就會調用ServerSocket.accept獲取一個新的Socket連接,然后將這個Socket連接加入到感興趣的描述符列表中,如果拿到Socket可讀事件就會開始調用Socket.read讀取套件字的消息進行處理,處理完畢將返回結果序列化成一個字節(jié)數組,當Socket可以拿到可寫事件時,說明套件字緩沖區(qū)未滿,就拼命的將字節(jié)數組往Socket里灌,也就是調用Socket.write進行IO的寫操作。
NIO從內核中拿事件的操作使用的是Selector.select函數調用,它對應操作系統(tǒng)界面的IO多路復用API。在現代操作系統(tǒng)里mac平臺上對應的是kqueue模型,linux平臺對應的是epoll模型,windows平臺對應的是iocp模型。Java是一個跨平臺的語言,JVM底層對操作系統(tǒng)的具體實現進行了抽象,統(tǒng)一向上層提供的是Selector系列API。用戶只需要使用Selector提供的通用API來處理NIO相關功能即可,而無需關系底層具的操作系統(tǒng)API的差別了。
Selector可以理解為一個描述符對象[SocketChannel]列表,Selector通過調用操作系統(tǒng)API,傳遞一個描述符列表參數,然后就可以拿到內核提供的與所有的描述符相關的事件[Key]列表。
上面提到的NIO線程是一個單線程,但是實際上它可以是一個線程池,線程池中的每個線程負責一部分描述符的讀寫操作。它也可以是兩個線程池,一個線程池只用來處理ServerSocket描述符建立新連接,另一個線程池專門干Socket讀寫的事。
Netty提供了良好的封裝,可以讓我們很方便的配置線程池的功用。代碼中的NioEventLoopGroup代表的就是一個線程池,池中每個線程都是一個獨立的NioEventLoop,即Nio事件循環(huán)。當acceptor線程池接收到一個新連接后會將這個連接通過隊列發(fā)送到讀寫線程池繼續(xù)進行處理。線程池分開的好處是當讀寫線程池繁忙的時候不影響acceptor接收新連接。
NIO的讀寫操作也是一系列復雜的過程。當NIO讀事件發(fā)生時,線程使用read操作讀取到的消息可能是不完整的,剩下的部分可能還要在接下來多次讀事件發(fā)生后才能讀到完整的一個消息對象字節(jié)數組。也可能read操作讀取到的消息包含多個消息對象,最后剩下的部分又是一個不完整的消息,這就需要在每個描述符關聯(lián)對象中保存中間半包的狀態(tài)。消息和消息之間又有組合關系,比如HTTP POST消息包含HTTP Header和HTTP Body兩個部分,而HTTP Body又可能因為太大而分解為多個HTTP Chunks進行傳輸,這就要求NIO的讀寫消息的設計包含結構層級。寫操作也不是一個簡單的write操作就了事了,寫操作要考慮到內核為每個套件字分配的buffer大小,如果buffer不夠了,write寫進去的數組是不能完全寫進去的,寫不進去的字節(jié)數據必須保留起來,等待下次寫事件發(fā)生時,也就是內核緩沖有空閑空間了,才可以將剩下的數據發(fā)送過去。
Netty將消息的讀寫抽象為pipeline消息管道,結構上有點類似于計算機網絡分層結構。pipeline的每一層會對應一個Handler,以上一層輸出的消息結構作為輸入,輸出新的消息結構作為下一層的輸入。pipeline對象掛接在每一個Socket鏈路上。
代碼中我們在pipeline里定義了四層Handler,第一個是處理ReadTimeout,當一個連接長達60s沒有任何消息的情況下會向下一層輸出一個讀超時消息。第二層是一個Redis消息解碼器,將Socket中的字節(jié)流轉換成Redis命令對象,第三層是一個Redis消息編碼器,將Redis輸出對象轉稱字節(jié)流,第四層是消息處理器,用來逐個處理Redis命令邏輯,這里一般就是我們復雜的業(yè)務邏輯所在地,我們會在業(yè)務邏輯里最終給Socket回饋消息輸出,這個消息輸出又會走一遍pipeline的每一層,直到轉換成字節(jié)流寫到內核socket緩沖區(qū)中才算完事。
然后我們設置一些套件字的特殊屬性,比如監(jiān)聽隊列大小、讀寫緩沖警戒水位大小、是否延遲發(fā)送等,然后綁定監(jiān)聽指定端口,服務器就可以開始永無止盡地工作了。