本章介紹
- 獲取Netty4最新版本
- 設置運行環境來構建和運行netty程序
- 創建一個基於Netty的服務器和客戶端
- 攔截和處理異常
- 編寫和運行Netty服務器和客戶端
2.1 設置開發環境
- 安裝JDK7,下載地址http://www.oracle.com/technetwork/java/javase/archive-139210.html
- 下載netty包,下載地址http://netty.io/
- 安裝Eclipse
2.2 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 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();
}
}
從上面這個簡單的服務器例子可以看出,啓動服務器應先創建一個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 {
@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很簡單,它的每個方法都可以被重寫,它的所有的方法中只有channelRead方法是必須要重寫的。2.3.3 捕獲異常
2.4 編寫應答程序的客戶端
- 連接服務器
- 寫數據到服務器
- 等待接受服務器返回相同的數據
- 關閉連接
2.4.1 引導客戶端
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();
}
}
創建啓動一個客戶端包含下面幾步:- 創建Bootstrap對象用來引導啓動客戶端
- 創建EventLoopGroup對象並設置到Bootstrap中,EventLoopGroup可以理解爲是一個線程池,這個線程池用來處理連接、接受數據、發送數據
- 創建InetSocketAddress並設置到Bootstrap中,InetSocketAddress是指定連接的服務器地址
- 添加一個ChannelHandler,客戶端成功連接服務器後就會被執行
- 調用Bootstrap.connect()來連接服務器
- 最後關閉EventLoopGroup來釋放資源
2.4.2 實現客戶端的業務邏輯
- 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> {
@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接口達到的。2.5 編譯和運行echo(應答)程序客戶端和服務器
注意,netty4需要jdk1.7+。
本人測試,可以正常運行。
2.6 總結
本章介紹瞭如何編寫一個簡單的基於Netty的服務器和客戶端並進行通信發送數據。介紹瞭如何創建服務器和客戶端以及Netty的異常處理機制。