本章介紹

  • 獲得Netty4最新的版本號
  • 設定執行環境,以構建和執行netty程式
  • 建立一個基於Netty的server和client
  • 攔截和處理異常
  • 編制和執行Nettyserver和client

本章將簡介Netty的核心概念,這個狠心概念就是學習Netty是怎樣攔截和處理異常。對於剛開始學習netty的讀者。利用netty的異常攔截機制來除錯程式問題非常有幫助。本章還會介紹其它一些核心概念。如server和client的啟動以及分離通道的處理程式。本章學習一些基礎以便後面章節的深入學習。

本章中將編寫一個基於netty的server和client來互相通訊,我們首先來設定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 Nettyclient和server概述

        本節將引導你構建一個完整的Nettyserver和client。普通情況下。你可能僅僅關心編寫server,如一個httpserver的client是瀏覽器。然後在這個樣例中,你若同一時候實現了server和client。你將會對他們的原理更加清晰。
        一個Netty程式的工作圖例如以下

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYWJjX2tleQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="">

  1. client連線到server
  2. 建立連線後,傳送或接收資料
  3. server處理全部的client連線

從上圖中能夠看出,server會寫資料到client而且處理多個client的併發連線。從理論上來說,限制程式效能的因素僅僅有系統資源和JVM。為了方便理解,這裡舉了個生活樣例。在山谷或高山上大聲喊,你會聽見回聲。回聲是山返回的;在這個樣例中,你是client。山是server。喊的行為就類似於一個Nettyclient將資料傳送到server,聽到回聲就類似於server將同樣的資料返回給你,你離開山谷就斷開了連線。可是你能夠返回進行重連server而且能夠傳送很多其它的資料。

儘管將同樣的資料返回給client不是一個典型的樣例。可是client和server之間資料的來來回回的傳輸和這個樣例是一樣的。本章的樣例會證明這一點,它們會越來越複雜。

        接下來的幾節將帶著你完畢基於Netty的client和server的應答程式。

2.3 編寫一個應答server

寫一個Nettyserver主要由兩部分組成:

  • 配置server功能,如執行緒、port
  • 實現server處理程式,它包括業務邏輯,決定當有一個請求連線或接收資料時該做什麼

2.3.1 啟動server

通過建立ServerBootstrap物件來啟動server。然後配置這個物件的相關選項。如port、執行緒模式、事件迴圈,而且加入邏輯處理程式用來處理業務邏輯(以下是個簡單的應答server樣例)

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 instance
ServerBootstrap b = new ServerBootstrap();
//Specifies NIO transport, local socket address
//Adds handler to channel pipeline
b.group(group).channel(NioServerSocketChannel.class).localAddress(port)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new EchoServerHandler());
}
});
//Binds server, waits for server to close, and releases resources
ChannelFuture 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();
} }

從上面這個簡單的server樣例能夠看出,啟動server應先建立一個ServerBootstrap物件。由於使用NIO,所以指定NioEventLoopGroup來接受和處理新連線。指定通道型別為NioServerSocketChannel,設定InetSocketAddress讓server監聽某個埠已等待client連線。

        接下來,呼叫childHandler放來指定連線後呼叫的ChannelHandler。這種方法傳ChannelInitializer型別的引數。ChannelInitializer是個抽象類,所以須要實現initChannel方法,這種方法就是用來設定ChannelHandler。

最後繫結server等待直到繫結完畢。呼叫sync()方法會堵塞直到server完畢繫結,然後server等待通道關閉。由於使用sync(),所以關閉操作也會被堵塞。如今你能夠關閉EventLoopGroup和釋放全部資源,包含建立的執行緒。

這個樣例中使用NIO,由於它是眼下最經常使用的傳輸方式。你可能會使用NIO非常長時間。可是你能夠選擇不同的傳輸實現。比如。這個樣例使用OIO方式傳輸,你須要指定OioServerSocketChannel。Netty框架中實現了多重傳輸方式,將再後面講述。

        本小節重點內容:

  • 建立ServerBootstrap例項來引導繫結和啟動server
  • 建立NioEventLoopGroup物件來處理事件,如接受新連線、接收資料、寫資料等等
  • 指定InetSocketAddress。server監聽此port
  • 設定childHandler執行全部的連線請求
  • 都設定完成了,最後呼叫ServerBootstrap.bind() 方法來繫結server

2.3.2 實現server業務邏輯

Netty使用futures和回撥概念。它的設計同意你處理不同的事件型別。更具體的介紹將再後面章節講述。可是我們能夠接收資料。

你的channel handler必須繼承ChannelInboundHandlerAdapter而且重寫channelRead方法,這種方法在不論什麼時候都會被呼叫來接收資料,在這個樣例中接收的是位元組。

以下是handler的實現,事實上現的功能是將client發給server的資料返回給client:

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 { @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("Server received: " + msg);
ctx.write(msg);
} @Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
} }

Netty使用多個Channel Handler來達到對事件處理的分離,由於能夠非常容的加入、更新、刪除業務邏輯處理handler。

Handler非常easy,它的每一個方法都能夠被重寫,它的全部的方法中僅僅有channelRead方法是必需要重寫的。

2.3.3 捕獲異常

        重寫ChannelHandler的exceptionCaught方法能夠捕獲server的異常,比方client連線server後強制關閉,server會丟擲"client主機強制關閉錯誤",通過重寫exceptionCaught方法就能夠處理異常,比方發生異常後關閉ChannelHandlerContext。

2.4 編寫應答程式的client

        server寫好了。如今來寫一個client連線server。應答程式的client包含下面幾步:
  • 連線server
  • 寫資料到server
  • 等待接受server返回同樣的資料
  • 關閉連線

2.4.1 引導client

        引導client啟動和引導server非常類似,client需同一時候指定host和port來告訴client連線哪個server。看以下程式碼:
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>() {
@Override
protected 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();
}
}

建立啟動一個client包括以下幾步:

  • 建立Bootstrap物件用來引導啟動client
  • 建立EventLoopGroup物件並設定到Bootstrap中,EventLoopGroup能夠理解為是一個執行緒池。這個執行緒池用來處理連線、接受資料、傳送資料
  • 建立InetSocketAddress並設定到Bootstrap中。InetSocketAddress是指定連線的server地址
  • 加入一個ChannelHandler,client成功連線server後就會被執行
  • 呼叫Bootstrap.connect()來連線server
  • 最後關閉EventLoopGroup來釋放資源

2.4.2 實現client的業務邏輯

        client的業務邏輯的實現依舊非常easy,更復雜的使用方法將在後面章節具體介紹。

和編寫server的ChannelHandler一樣,在這裡將自己定義一個繼承SimpleChannelInboundHandler的ChannelHandler來處理業務;通過重寫父類的三個方法來處理感興趣的事件:

  • channelActive():client連線server後被呼叫
  • channelRead0():從server接收到資料後呼叫
  • 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> { @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.write(Unpooled.copiedBuffer("Netty rocks!",CharsetUtil.UTF_8));
} @Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println("Client received: " + ByteBufUtil.hexDump(msg.readBytes(msg.readableBytes())));
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

可能你會問為什麼在這裡使用的是SimpleChannelInboundHandler而不使用ChannelInboundHandlerAdapter?主要原因是ChannelInboundHandlerAdapter在處理完訊息後須要負責釋放資源。

在這裡將呼叫ByteBuf.release()來釋放資源。SimpleChannelInboundHandler會在完畢channelRead0後釋放訊息,這是通過Netty處理全部訊息的ChannelHandler實現了ReferenceCounted介面達到的。

        為什麼在server中不使用SimpleChannelInboundHandler呢?由於server要返回同樣的訊息給client,在server執行完畢寫操作之前不能釋放呼叫讀取到的訊息,由於寫操作是非同步的,一旦寫操作完畢後,Netty中會自己主動釋放訊息。
        client的編寫完了,以下讓我們來測試一下

2.5 編譯和執行echo(應答)程式client和server

注意,netty4須要jdk1.7+。

本人測試,能夠正通常執行。

2.6 總結一下

本章介紹如何編寫基於一個簡單的Netty的server和client和傳送資料通訊。它描述瞭如何建立server和client以及Netty異常處理機制。