標籤:netty netty in action netty in action中文版
註:本篇內容出自《Netty In Action》一書;
註:本人原創譯文,轉載請註明出處!
本章介紹
- 擷取Netty4最新版本
- 設定運行環境來構建和運行netty程式
- 建立一個基於Netty的伺服器和用戶端
- 攔截和處理異常
- 編寫和運行Netty伺服器和用戶端
本章將簡單介紹Netty的核心概念,這個狠心概念就是學習Netty是如何攔截和處理異常,對於剛開始學習netty的讀者,利用netty的異常攔截機制來偵錯工具問題很有協助。本章還會介紹其他一些核心概念,如伺服器和用戶端的啟動以及分離通道的處理常式。本章學習一些基礎以便後面章節的深入學習。本章中將編寫一個基於netty的伺服器和用戶端來互相通訊,我們首先來設定netty的開發環境。
2.1 設定開發環境 設定開發環境的步驟如下:
- 安裝JDK7,http://www.oracle.com/technetwork/java/javase/archive-139210.html
- 下載netty包,http://netty.io/
- 安裝Eclipse
《Netty In Action》中描述的比較多,沒啥用,這裡就不多說了。
本系列部落格將使用Netty4,需要JDK1.7+
2.2 Netty用戶端和伺服器概述 本節將引導你構建一個完整的Netty伺服器和用戶端。一般情況下,你可能只關心編寫伺服器,如一個http伺服器的用戶端是瀏覽器。然後在這個例子中,你若同時實現了伺服器和用戶端,你將會對他們的原理更加清晰。 一個Netty程式的工作圖如下
- 用戶端串連到伺服器
- 建立串連後,發送或接收資料
- 伺服器處理所有的用戶端串連
從中可以看出,伺服器會寫資料到用戶端並且處理多個用戶端的並發串連。從理論上來說,限制程式效能的因素只有系統資源和JVM。為了方便理解,這裡舉了個生活例子,在山穀或高山上大聲喊,你會聽見回聲,回聲是山返回的;在這個例子中,你是用戶端,山是伺服器。喊的行為就類似於一個Netty用戶端將資料發送到伺服器,聽到回聲就類似於伺服器將相同的資料返回給你,你離開山穀就斷開了串連,但是你可以返回進行重連伺服器並且可以發送更多的資料。
雖然將相同的資料返回給用戶端不是一個典型的例子,但是用戶端和伺服器之間資料的來來回回的傳輸和這個例子是一樣的。本章的例子會證明這一點,它們會越來越複雜。
接下來的幾節將帶著你完成基於Netty的用戶端和伺服器的應答程式。
2.3 編寫一個應答伺服器
寫一個Netty伺服器主要由兩部分組成:
- 設定管理員功能,如線程、連接埠
- 實現伺服器處理常式,它包含商務邏輯,決定當有一個請求串連或接收資料時該做什麼
2.3.1 啟動伺服器
通過建立ServerBootstrap對象來啟動伺服器,然後配置這個對象的相關選項,如連接埠、線程模式、事件迴圈,並且添加邏輯處理常式用來處理商務邏輯(下面是個簡單的應答伺服器例子)
package netty.example;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.Channel;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;public class EchoServer {private final int port;public EchoServer(int port) {this.port = port;}public void start() throws Exception {EventLoopGroup group = new NioEventLoopGroup();try {//create ServerBootstrap instanceServerBootstrap b = new ServerBootstrap();//Specifies NIO transport, local socket address//Adds handler to channel pipelineb.group(group).channel(NioServerSocketChannel.class).localAddress(port).childHandler(new ChannelInitializer<Channel>() {@Overrideprotected void initChannel(Channel ch) throws Exception {ch.pipeline().addLast(new EchoServerHandler());}});//Binds server, waits for server to close, and releases resourcesChannelFuture f = b.bind().sync();System.out.println(EchoServer.class.getName() + "started and listen on ?" + f.channel().localAddress());f.channel().closeFuture().sync();} finally {group.shutdownGracefully().sync();}}public static void main(String[] args) throws Exception {new EchoServer(65535).start();}} 從上面這個簡單的伺服器例子可以看出,啟動伺服器應先建立一個ServerBootstrap對象,因為使用NIO,所以指定NioEventLoopGroup來接受和處理新串連,指定通道類型為NioServerSocketChannel,設定InetSocketAddress讓伺服器監聽某個連接埠已等待用戶端串連。
接下來,調用childHandler放來指定串連後調用的ChannelHandler,這個方法傳ChannelInitializer類型的參數,ChannelInitializer是個抽象類別,所以需要實現initChannel方法,這個方法就是用來設定ChannelHandler。
最後綁定伺服器等待直到綁定完成,調用sync()方法會阻塞直到伺服器完成綁定,然後伺服器等待通道關閉,因為使用sync(),所以關閉操作也會被阻塞。現在你可以關閉EventLoopGroup和釋放所有資源,包括建立的線程。
這個例子中使用NIO,因為它是目前最常用的傳輸方式,你可能會使用NIO很長時間,但是你可以選擇不同的傳輸實現。例如,這個例子使用OIO方式傳輸,你需要指定OioServerSocketChannel。Netty架構中實現了多重傳輸方式,將再後面講述。
本小節重點內容:
- 建立ServerBootstrap執行個體來引導綁定和啟動伺服器
- 建立NioEventLoopGroup對象來處理事件,如接受新串連、接收資料、寫資料等等
- 指定InetSocketAddress,伺服器監聽此連接埠
- 設定childHandler執行所有的串連請求
- 都設定完畢了,最後調用ServerBootstrap.bind() 方法來綁定伺服器
2.3.2 實現伺服器商務邏輯 Netty使用futures和回調概念,它的設計允許你處理不同的事件類型,更詳細的介紹將再後面章節講述,但是我們可以接收資料。你的channel handler必須繼承ChannelInboundHandlerAdapter並且重寫channelRead方法,這個方法在任何時候都會被調用來接收資料,在這個例子中接收的是位元組。
下面是handler的實現,其實現的功能是將用戶端發給伺服器的資料返回給用戶端:
package netty.example;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelFutureListener;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class EchoServerHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {System.out.println("Server received: " + msg);ctx.write(msg);}@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}} Netty使用多個Channel Handler來達到對事件處理的分離,因為可以很容的添加、更新、刪除商務邏輯處理handler。Handler很簡單,它的每個方法都可以被重寫,它的所有的方法中只有channelRead方法是必須要重寫的。2.3.3 捕獲異常 重寫ChannelHandler的exceptionCaught方法可以捕獲伺服器的異常,比如用戶端串連伺服器後強制關閉,伺服器會拋出"用戶端主機強制關閉錯誤",通過重寫exceptionCaught方法就可以處理異常,比如發生異常後關閉ChannelHandlerContext。2.4 編寫應答程式的用戶端 伺服器寫好了,現在來寫一個用戶端串連伺服器。應答程式的用戶端包括以下幾步:
- 串連伺服器
- 寫資料到伺服器
- 等待接受伺服器返回相同的資料
- 關閉串連
2.4.1 引導用戶端 引導用戶端啟動和引導伺服器很類似,用戶端需同時指定host和port來告訴用戶端串連哪個伺服器。看下面代碼:
package netty.example;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.example.echo.EchoClientHandler;import java.net.InetSocketAddress;public class EchoClient {private final String host;private final int port;public EchoClient(String host, int port) {this.host = host;this.port = port;}public void start() throws Exception {EventLoopGroup group = new NioEventLoopGroup();try {Bootstrap b = new Bootstrap();b.group(group).channel(NioSocketChannel.class).remoteAddress(new InetSocketAddress(host, port)).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new EchoClientHandler());}});ChannelFuture f = b.connect().sync();f.channel().closeFuture().sync();} finally {group.shutdownGracefully().sync();}}public static void main(String[] args) throws Exception {new EchoClient("localhost", 20000).start();}}建立啟動一個用戶端包含下面幾步:
- 建立Bootstrap對象用來引導啟動用戶端
- 建立EventLoopGroup對象並設定到Bootstrap中,EventLoopGroup可以理解為是一個線程池,這個線程池用來處理串連、接受資料、發送資料
- 建立InetSocketAddress並設定到Bootstrap中,InetSocketAddress是指定已連線的服務器地址
- 添加一個ChannelHandler,用戶端成功串連伺服器後就會被執行
- 調用Bootstrap.connect()來串連伺服器
- 最後關閉EventLoopGroup來釋放資源
2.4.2 實現用戶端的商務邏輯 用戶端的商務邏輯的實現依然很簡單,更複雜的用法將在後面章節詳細介紹。和編寫伺服器的ChannelHandler一樣,在這裡將自訂一個繼承SimpleChannelInboundHandler的ChannelHandler來處理業務;通過重寫父類的三個方法來處理感興趣的事件:
- channelActive():用戶端串連伺服器後被調用
- channelRead0():從伺服器接收到資料後調用
- exceptionCaught():發生異常時被調用
實現代碼如下
package netty.example;import io.netty.buffer.ByteBuf;import io.netty.buffer.ByteBufUtil;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.util.CharsetUtil;public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.write(Unpooled.copiedBuffer("Netty rocks!",CharsetUtil.UTF_8));}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {System.out.println("Client received: " + ByteBufUtil.hexDump(msg.readBytes(msg.readableBytes())));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}} 可能你會問為什麼在這裡使用的是SimpleChannelInboundHandler而不使用ChannelInboundHandlerAdapter?主要原因是ChannelInboundHandlerAdapter在處理完訊息後需要負責釋放資源。在這裡將調用ByteBuf.release()來釋放資源。SimpleChannelInboundHandler會在完成channelRead0後釋放訊息,這是通過Netty處理所有訊息的ChannelHandler實現了ReferenceCounted介面達到的。
為什麼在伺服器中不使用SimpleChannelInboundHandler呢?因為伺服器要返回相同的訊息給用戶端,在伺服器執行完成寫操作之前不能釋放調用讀取到的訊息,因為寫操作是非同步,一旦寫操作完成後,Netty中會自動釋放訊息。 用戶端的編寫完了,下面讓我們來測試一下
2.5 編譯和運行echo(應答)程式用戶端和伺服器
注意,netty4需要jdk1.7+。
本人測試,可以正常運行。
2.6 總結
本章介紹了如何編寫一個簡單的基於Netty的伺服器和用戶端並進行通訊發送資料。介紹了如何建立伺服器和用戶端以及Netty的異常處理機制。