Netty In Action中文版 - 第二章:第一個Netty程序

本章介紹

  • 獲取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程序的工作圖如下

  1. 客戶端連接到服務器
  2. 建立連接後,發送或接收數據
  3. 服務器處理所有的客戶端連接

        從上圖中可以看出,服務器會寫數據到客戶端並且處理多個客戶端的併發連接。從理論上來說,限制程序性能的因素只有系統資源和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 捕獲異常

        重寫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>() {
						@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 實現客戶端的業務邏輯

        客戶端的業務邏輯的實現依然很簡單,更復雜的用法將在後面章節詳細介紹。和編寫服務器的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> {
	
	@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接口達到的。
        爲什麼在服務器中不使用SimpleChannelInboundHandler呢?因爲服務器要返回相同的消息給客戶端,在服務器執行完成寫操作之前不能釋放調用讀取到的消息,因爲寫操作是異步的,一旦寫操作完成後,Netty中會自動釋放消息。
        客戶端的編寫完了,下面讓我們來測試一下

2.5 編譯和運行echo(應答)程序客戶端和服務器

        注意,netty4需要jdk1.7+。

        本人測試,可以正常運行。

2.6 總結

        本章介紹瞭如何編寫一個簡單的基於Netty的服務器和客戶端並進行通信發送數據。介紹瞭如何創建服務器和客戶端以及Netty的異常處理機制。


發佈了32 篇原創文章 · 獲贊 11 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章