Netty框架學習及第一個Netty應用(建議收藏)

1.什麼是Netty?

Netty是一個利用Java的高級網絡的能力,隱藏其背後的複雜性而提供一個易於使用的API的客戶端/服務器框架。Netty提供高性能和可擴展性,讓你可以自由地專注於你真正感興趣的東西。

2.發展歷史:

網絡發展初期,花費很多時間學習socket的複雜、尋址等,在C socket庫上進行編碼,並需要在不同的操作系統上做不同的處理。

Java早期版本(1995-2002)介紹了足夠的面向對象的糖衣來隱藏一些複雜性,但實現客戶端-服務器協議仍需要大量的樣板代碼和大量的監視才能確保他們是對的。早期的API只能通過原生的socket庫來支持所謂的?blocking功能。

JAVA NIO,非阻塞API,阻塞性I/O一般工作流程如圖示:

這種方式在連接數比較少的時候還是可以接受的,當併發連接超過10000時,開銷會明顯增加。此外,每一個線程都有一個默認的堆棧內存分配了128K和1M之間的空間。考慮到整體的內存和操作系統需要處理更多的併發連接資源,所以這似乎不是一個理想的解決方案。

SELECTOR,實現Java的無阻塞I/O實現的關鍵,工作流程如下:

最終由Selector決定哪一組註冊的socket準備執行I/O。通過通知,一個線程可以同時處理多個併發連接(一個Selector通常由一個線程處理,但具體實施可以使用多個線程)因此,每次讀或寫操作執行能立即檢查完成。該模型可以用較少的線程處理更多連接,這意味着在內存和上下文切換上花費更少的開銷,當沒有I/O處理時,線程可以被重定向到其他任務上。

我們可以直接使用JavaAPI構建的NIO構建應用程序,但這樣做正確和安全無法保證,實現可靠和可擴展的event-processing(事件處理器)來處理和調度數據並保證儘可能有效,不過這是一個繁瑣和容易出錯的任務,而這些,就交給了Netty。

3.Netty特點

設計:

針對多種傳輸類型的同一接口-阻塞和非阻塞;

簡單但更強大的線程模型;

真正的無連接的數據報套接字支持;

連接邏輯支持複用。

易用性:

大量的javadoc和代碼實例;

除了在JDK1.6+額外的限制。

性能:

比核心Java API更好的吞吐量,較低的延時;

資源消耗更少,這個得益於於共享池和重用;

減少內存拷貝。

健壯性:

消除由於慢、快、或重載連接產生的OutOfMemoryError;

消除經常發現在NIO在告訴網絡中的應用中的不公平的讀/寫比。

安全:

完整的SSL/TLS和StartTLS的支持;

運行在受限的環境例如Applet或OSGI。

社區:

發佈的更早和更頻繁;

社區驅動。

4.異步和事件驅動

非阻塞I/O不會強迫我們等待操作的完成。

5.構成部分

Channel:NIO基本結構,代表一個用於連接到實體如硬件設備、文件、網絡套接字或程序組件,能否執行一個或多個不同的I/O操作的開放連接。

Callback:回調方法,提供給另一種方法作爲引用,時間接口可由ChannelHandler的實現來處理。

Future:提供了另一種通知應用操作已經完成的方式,這個對象作爲一個一步操作結果的佔位符,他將在將來的某個時候完成並提交結果。Netty提供自己的實現,ChannelFuture,用於執行異步操作時使用。每個Netty的outbound I/O操作都會返回一個ChannelFuture,這樣就不會阻塞,這便是Netty所謂的“自底向上的異步和事件驅動”。相關實現的步驟如下:

1.異步連接到遠程對等節點,調用立即返回並提供ChannelFuture;

2.操作完成後通知註冊一個ChannelFutureListener;

3.當operationComplete()調用時檢查操作的狀態;

4.如果成功就創建一個ByteBuf來保存數據;

5.異步發送數據到遠程,再次返回ChannelFuture;

6.如果有一個錯誤則拋出Throwable,描述錯誤原因。

Event和Handler:Netty使用不同的事件來通知我們更改的狀態或操作的狀態,這使我們能夠根據發生的事件觸發適當的行爲。這些行爲可能包括:日誌、數據轉換、流控制、應用程序邏輯,由於Netty是一個網絡框架,事件很清晰的跟入棧或出出站數據流相關,因爲一些事件可能觸發的傳入的數據或狀態的變化包括:活動或非活動連接、數據的讀取、用戶事件、錯誤,出站事件是由於在未來操作將觸發的一個動作,這些包括:打開或關閉一個連接到遠程、寫或沖刷數據到socket。每個事件都可以分配給用戶實現處理程序類的方法,這些範例可直接轉換爲應用程序構建塊,如圖:

Netty的ChannelHandler是各種處理程序的基本抽象,每個處理器實例就是一個回調,用於執行各種事件的響應。

6.整合

FUTURE,CALLBACK和HANDLER

Netty的異步編程模型是建立在future和callback的概念上的,所有這些元素的協同爲自己的設計提供了強大的力量。攔截操作和轉換入站或出站數據只需要提供回調或者利用future操作返回的,這是用的鏈操作簡單、高效,促進編寫可重用的、通用的代碼。一個Netty的設計的主要目標是促進“關注點分離”,即我們的業務邏輯從網絡基礎設施應用程序中分離。

SELECTOR,EVENT和EVENT LOOP

Netty通過觸發事件從應用程序中抽象出Selector,從而避免手寫調度代碼,EventLoop分配給每個Channel來處理所有的事件,包括:註冊感興趣的事件、調度事件到ChannelHandler、安排進一步行動。該EventLoop本身由只有一個線程驅動,它給一個Channel處理所有的I/O事件,並且在EventLoop的生命週期內不會改變,這個簡單而強大的線程模型消除你可能對你的ChannelHandler同步的任何關注,這樣你就可以專注提供正確的回調邏輯來執行。?

7.第一個Netty應用

7.1Netty客戶端/服務器總覽,Echo client/server

圖中顯示了連接到服務器的多個併發客戶端,理論上,客戶端可以支持的連接數只受限於使用的JDK版本中的制約。

echo(回聲)客戶端和服務器之間的交互是很簡單的:客戶端啓動後,建立一個連接發送一個或多個消息到服務器,其中每相呼應消息返回給客戶端。這個應用程序並不是非常有用,但這項工作是爲了更好的理解請求--相應交互本身,這是一個基本的模式的客戶端/服務器系統。

7.2 寫一個echo服務器

Netty實現的echo服務器需要下面內容:

一個服務器handler:該組件實現了服務器的業務邏輯,決定了連接創建後和接收到信息後如何處理;

Bootstrapping:這個配置服務器的啓動代碼,最少需要設置服務器綁定的端口,用來監聽連接請求。

通過ChannelHandler來實現服務器的邏輯,使用ChannelHandler的方式體現了“關注點分離”的設計原則,並簡化業務邏輯的迭代開發的要求,處理程序很簡單,每一個方法都可以覆蓋到“hook(鉤子)”在活動週期適當的點。牢記兩點:

ChannelHandler是給不同類型的事件調用;

應用程序實現或擴展ChannelHandler掛接到事件生命週期和提供自定義應用邏輯。

Echo server將接受到的數據拷貝發送給客戶端,因此,我們需要實現ChannelInboundHandler接口,用於自定義處理入站事件的方法,當前應用簡單,只需繼承ChannelInboundHandlerAdapter就行了,該類提供了默認ChannelInboundHandler的實現,所以只需覆蓋以下方法:

channelRead()--每個信息入站都會調用,覆蓋該方法是因爲我們需要處理所有接收到的數據;

channelReadComplete()--通知處理器最後的channelRead()是當前處理中的最後一條消息調用;

exceptionCaught()-讀操作時捕獲到異常時調用,覆蓋該方法使我們能夠應對任何Throwable的子類 ? 型,在這種情況下我們記錄、並關閉所有可能處在未知狀態的連接,它通常是難以從連接錯誤中恢復,所以乾脆關閉遠程連接,當然,也有可能的情況是可以從錯誤中恢復的,所以可以用一個更復雜的措施來嘗試識別和處理這樣的情況。每個Channel都有一個關聯的ChannelPipeline,它代表了ChannelHandler實例的鏈,適配器處理的實現知識講一個處理方法調用轉發到鏈中的下一個處理器,因此,如果一個Netty應用程序不覆蓋exceptionCaught,那麼這些錯誤最終將到達ChannelPipeline,並且結束警告將被記錄。

使用工具IntelliJ IDEA 2017.2.3工具創建Maven項目,命名爲echoserver,pom.xml內容如下

<?xml version="1.0" encoding="UTF-8"?>4.0.0comecho-server1.0-SNAPSHOTecho-server<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->io.nettynetty-all4.1.33.Final

創建業務核心處理邏輯EchoServerHandler,內容與註釋如下:

packagecom;importio.netty.buffer.ByteBuf;importio.netty.buffer.Unpooled;importio.netty.channel.ChannelFutureListener;importio.netty.channel.ChannelHandler;importio.netty.channel.ChannelHandlerContext;importio.netty.channel.ChannelInboundHandlerAdapter;importio.netty.util.CharsetUtil;@ChannelHandler.Sharable//1.標誌這類的實例之間可以再channel裏面共享publicclassEchoServerHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContext ctx, Object msg)throwsException{ByteBuf in = (ByteBuf)msg;System.out.println("Server received:"+in.toString(CharsetUtil.UTF_8));//2.日誌輸出到控制檯ctx.write(in);//3.將所接收的消息返回給發送者,注意,此時還沒有沖刷數據}@OverridepublicvoidchannelReadComplete(ChannelHandlerContext ctx)throwsException{ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)//4.沖刷所有待審消息到遠程節點,關閉通道後,操作完成.addListener(ChannelFutureListener.CLOSE);}@OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause)throwsException{ctx.fireExceptionCaught(cause);//5.打印異常堆棧跟蹤ctx.close();//6.關閉通道}}

創建完業務核心處理邏輯之後,創建引導服務器EchoServer,作用如下:

監聽和接收進來的連接請求;

配置Channel來通知一個關於入站消息的EchoServerHandler實例。

內容如下:

package com;importio.netty.bootstrap.ServerBootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioServerSocketChannel;importjava.net.InetSocketAddress;publicclassEchoServer{privatefinalintport;publicEchoServer(intport){this.port = port;}publicstaticvoidmain(String[] args)throws Exception{// if(args.length != 1){// System.err.println("Usage: " + EchoServer.class.getSimpleName() + "<port>");// return;// }// int port = Integer.parseInt(args[0]);//1.設置端口值intport =8990;//1.設置端口值newEchoServer(port).start();//2.啓動服務}publicvoidstart()throws Exception{EventLoopGroup group =newNioEventLoopGroup();//3.創建EventLoopGroup,一個線程try{ServerBootstrap b =newServerBootstrap();b.group(group)//4.創建ServerBootstrap,此處也可放入多個EventLoopGroup.channel(NioServerSocketChannel.class)//5.指定使用NIO的傳輸Channel,指定信道類型.localAddress(newInetSocketAddress(port))//6.設置socket地址使用所選端口.childHandler(newChannelInitializer() {//7.當有一個新的連接被接受,一個新的子Channel將被創建,ChannelInitializer添加EchoServerHandler到Channel的ChannelPipeline,@OverridepublicvoidinitChannel(SocketChannel ch) throws Exception{ch.pipeline().addLast(newEchoServerHandler());}});ChannelFuture f = b.bind().sync();//8.綁定的服務器,sync等待服務器關閉,調用sync()的原因是當前線程阻塞System.out.println(EchoServer.class.getName() +" started and listen on "+ f.channel().localAddress());f.channel().closeFuture().sync();//9.關閉Channel和塊} finally {group.shutdownGracefully().sync();//10.關閉EventLoopGroup,釋放所有資源}}}

服務器的主代碼組件是:EchoServerHandler實現了的業務邏輯、在main()方法中引導了服務器。

執行引導服務器所需的步驟是:

創建ServerBootstrap實例引導服務器並隨後綁定;

創建並分配一個NioEventLoopGroup實例來實現事件的處理,如接受新的連接和讀/寫數據;

指定本地InetSocketAddress給服務器綁定;?

通過EchoServerHandler實例給每一個新的Channel初始化;

最後調用ServerBootstrap.bind()綁定服務器。

7.3 寫一個echo客戶端

客戶端要做的就是:

連接服務器;

發送信息;

發送的每個信息,等待和接受從服務器返回的同樣的信息;

關閉連接。

用ChannelHandler實現客戶端邏輯

跟寫服務器一樣,Netty提供了ChannelInboundHandler來處理數據,下面的例子中,我們使用SimpleChannelInboundHandler來處理所有的任務,需要覆蓋三個方法:

channelActive()--服務器的連接被建立後調用,一旦建立了連接,字節序列被髮送到服務器;

channelRead0()--在接收到數據時被調用,由服務器所發送的消息可以以塊的形式被接收。即,當服務器發送五個字節是不是保證所有的5個字節會立刻收到,即使只有5個字節,channelRead0()方法可被調用兩次,第一次用一個ByteBuf裝載3個字節和第二次一個ByteBuf裝載2個字節,唯一要保證的是,該字節將按照他們發送的順序分別被接收;

exceptionCaught()--捕獲一個異常時調用。

創建Maven項目,命名echoclient,pom.xml文件內容如下

<?xml version="1.0" encoding="UTF-8"?>4.0.0comecho-client1.0-SNAPSHOTecho-client<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->io.nettynetty-all4.1.33.Final

創建EchoClientHandler.java,內容如下

packagecom;importio.netty.buffer.ByteBuf;importio.netty.buffer.Unpooled;importio.netty.channel.ChannelHandler;importio.netty.channel.ChannelHandlerContext;importio.netty.channel.SimpleChannelInboundHandler;importio.netty.util.CharsetUtil;@ChannelHandler.Sharable//1.標記這個類的實例可以在 channel 裏共享publicclassEchoClientHandlerextendsSimpleChannelInboundHandler{@OverridepublicvoidchannelActive(ChannelHandlerContext ctx){ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",//2.當被通知該 channel 是活動的時候就發送信息CharsetUtil.UTF_8));}@OverridepublicvoidchannelRead0(ChannelHandlerContext ctx, ByteBuf in)throwsException{System.out.println("Client received: "+ in.toString(CharsetUtil.UTF_8));//3.打印接收到的信息}@OverridepublicvoidexceptionCaught(ChannelHandlerContext ctx, Throwable cause)throwsException{cause.printStackTrace();//4.打印異常堆棧跟蹤ctx.close();//5.關閉通道}}

創建引導客戶端EchoClient,需要host、port兩個參數連接服務器,內容如下

packagecom;importio.netty.bootstrap.Bootstrap;importio.netty.channel.ChannelFuture;importio.netty.channel.ChannelInitializer;importio.netty.channel.EventLoopGroup;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.SocketChannel;importio.netty.channel.socket.nio.NioSocketChannel;importjava.net.InetSocketAddress;publicclassEchoClient{privatefinalString host;privatefinalintport;publicEchoClient(String host,intport){this.host = host;this.port = port;}publicvoidstart()throwsException{EventLoopGroup group =newNioEventLoopGroup();try{Bootstrap b =newBootstrap();//1.創建Bootstrapb.group(group)//2.指定EventLoopGroup來處理客戶端事件,由於我們NIO傳輸,所以用到了NioEventLoopGroup的實現.channel(NioSocketChannel.class)//3.使用的Channel類型是一個用於NIO傳輸,也可以使用和服務器不一樣的類型.remoteAddress(newInetSocketAddress(host,port))//4.設置服務器的InetSocketAddress.handler(newChannelInitializer(){//5.當建立一個連接和一個新的通道時,創建添加到 EchoClientHandler 實例 到 channelpipeline@OverridepublicvoidinitChannel(SocketChannel ch)throwsException{ch.pipeline().addLast(newEchoClientHandler());}});ChannelFuture f = b.connect().sync();//6.連接到遠程,等待連接完成f.channel().closeFuture().sync();//7.阻塞直到Channel關閉}finally{group.shutdownGracefully().sync();//8.調用shutdownGracefully來關閉線程池和釋放所有資源}}publicstaticvoidmain(String[] args)throwsException{// if(args.length != 2){// System.err.println("Usage: " + EchoClient.class.getSimpleName() + "<host> <port>");// return;// }// final String host = args[0];// final int port = Integer.parseInt(args[1]);finalString host ="localhost";finalintport =8990;newEchoClient(host,port).start();}}

啓動Echo服務端,直接運行main()方法,控制檯打印如下:

啓動Echo客戶端,直接運行main()方法,控制檯打印如下:

而在這之前的服務端控制檯則會打印如下語句:

至此一個簡單的Netty應用搭建完成.,看完點個關注再走吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章