(入门篇 NettyNIO开发指南)第三章-Netty入门应用
作为Netty的第一个应用程序,我们依然以第2章的时间服务器为例进行开发,通过
Netty版本的时间服务报的开发,让初学者尽快学到如何搭建Netty开发环境和!运行Netty
应用程序。
如果你已经熟悉Netty 的基础应用,可以跳过本章,继续后面知识的学习。本章主要内容包括:
。Netty开发环境的搭建
。服务端程序TimeServer开发
。客户端程序TimeClient开发时间服务器的运行和调试
3.1 Netty开发环境的搭建
首先假设你已经在本机安装了JDKI.7贯配置了JDK的环境变量path,同时下载并正确启动了IDE工具.Eclipseo如果你是个Java初学者,从来没有在本机搭建过Java开发环境F建议你先选择一本Java基础入门的书籍或者课程进行学习。假如你习惯于使用其他IDE工具进行Java开发,例如NetBeansIDE,也可以运行本节的入门例程。但是,你需要根据自己实际使用的IDE进行对应的配置修改和调整,本书统一使用eclipse-jee-kepler-SRI-wi1132作为Java发工具。
下面我们开始学习如何搭建Netty的开发环境。
3.1.1 下载Netty的软件包
这时会发现里面包含了各个模块的.jar包和源码,由于我们直接以二道制类库的方式使用Netty,所以只需要获取ne忧y-a]l-5.0.0.AlphaI.jar即可。
3 .1.2 搭建 Netty 应用工程
使用 Ecl ipse 创建普边 的 Java 工程 ,同时创建 Java源文件 的 package ,
如 图 3-3 所示 。
到此结束,我们的Netty 应用开发环境己经搭建完成,下面的小节将演示如何基于Netty开发时间服务器程序。
3.2 Netty服务端开发
作为第一个Netty的应用例程,为了让读者能够将精力集中在Netty 的使用上,我们依然选择第2章的时间服务器为例进行源码开发和代码讲解。
TimeServer开发
在开始使用Ne町开发TimeServer之前,先回顾一下使用NIO进行服务端开发的步骤。
(1)创建ServerSocketChannel,配置它为非阻塞模式;
(2)绑定监听,配置TCP参数,例如backlog大小:
(3)创建一个独立的I/0线程,用于轮询多路复用器Selector;
(4)创建Selector,将之前创建的ServerSocketChannel 注册到Selector上,监听SelectionKey.ACCEPT;
(5)启动1/0线程在循环体中执行Selector.select()方法,轮询就绪的Channel;
(6)当轮询到了处于就绪状态的Channel时,需要对其进行判断,如果是OP_ACCEPT状态,说明是新的客户端接入,则调用ServerSocketChannel.accep,t()方法接受新的客户端;
(7)设置新接入的客户端链路SocketCbannel为非阻塞模式,配置其他的一些TCP参数;
(8)将SocketChannel注册到Selector,监听OPREAD操作位:
(9)如果轮询的Channel为OP_READ,则说明SocketCbannel中有新的就绪的数据包需要读取,则构造ByteBuffer对象,读取数据包:
(10)如果轮询的Channel为OP_WRITE,说明还有数据没有发送完成,需要继续发送。
一个简单的NIO服务端程序,如果我们直接使用JDK的NIO类库进行开发,竟然需要经过烦琐的十多步操作才能完成最基本的消息读取和发送,这也是我们要选择Netty等NIO框架的原因了,下面我们看看使用Netty是如何轻松搞定服务端开发的。
3- 1 Netty时间服务器服务端 Timeserver
1 package com.phei.netty.basic; 2 3 import io.netty.bootstrap.ServerBootstrap; 4 import io.netty.channel.ChannelFuture; 5 import io.netty.channel.ChannelInitializer; 6 import io.netty.channel.ChannelOption; 7 import io.netty.channel.EventLoopGroup; 8 import io.netty.channel.nio.NioEventLoopGroup; 9 import io.netty.channel.socket.SocketChannel; 10 import io.netty.channel.socket.nio.NioServerSocketChannel; 11 /** 12 * @author lilinfeng 13 * @date 2014年2月14日 14 * @version 1.0 15 */ 16 public class TimeServer { 17 18 public void bind(int port) throws Exception { 19 // 配置服务端的NIO线程组 20 EventLoopGroup bossGroup = new NioEventLoopGroup(); 21 EventLoopGroup workerGroup = new NioEventLoopGroup(); 22 try { 23 ServerBootstrap b = new ServerBootstrap(); 24 b.group(bossGroup, workerGroup) 25 .channel(NioServerSocketChannel.class) 26 .option(ChannelOption.SO_BACKLOG, 1024) 27 .childHandler(new ChildChannelHandler()); 28 // 绑定端口,同步等待成功 29 ChannelFuture f = b.bind(port).sync(); 30 31 // 等待服务端监听端口关闭 32 f.channel().closeFuture().sync(); 33 } finally { 34 // 优雅退出,释放线程池资源 35 bossGroup.shutdownGracefully(); 36 workerGroup.shutdownGracefully(); 37 } 38 } 39 40 private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { 41 @Override 42 protected void initChannel(SocketChannel arg0) throws Exception { 43 arg0.pipeline().addLast(new TimeServerHandler()); 44 } 45 46 } 47 48 /** 49 * @param args 50 * @throws Exception 51 */ 52 public static void main(String[] args) throws Exception { 53 int port = 8080; 54 if (args != null && args.length > 0) { 55 try { 56 port = Integer.valueOf(args[0]); 57 } catch (NumberFormatException e) { 58 // 采用默认值 59 } 60 } 61 new TimeServer().bind(port); 62 } 63 }
由于本章的重点是讲解Netty的应用开发,所以对于一些Netty的类库和用法仅仅做基础性的讲解,我们从黑盒的角度理解,这些概念即可。后续源码分析章节会专门对Netty核心的类库和功能进行分析,感兴趣的同学可以跳到源码分析章节进行后续的学习。
我们从bind方法开始学习,在
第20~21行创建了两个NioEventLoopGroup实例。NioEventLoopGroup是个线程组,它包含了一组N10线程,专门用于网络事件的处理,实际上它们就是Reactor线程组。这里创建两个的原因是一个用于服务端接受客户端的连接,另一个用于进行SocketChannel的网络读写。
第23行我们创建ServerBootstrap对象,它是Netty用于启动NlO服务端的辅助启动类,目的是降低服务端的开发复杂度。
第24行调用ServerBootstrap的group方法,将两个NIO线程组当作入参传递到ServerBootstrap中。接着设置创建的Channel 为NioServerSocketChannel,它的功能对应于JDK的NIO 类库中的ServerSocketChannel类。然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024,最后绑定1/0事件的处理类ChildChannelHandler,它的作用类似于Reactor模式中的handler类,主要用于处理网络I/0事件,例如记录日志、对消息进行编解码等。
服务端启动辅助类配置完成之后,调用它的bind方法绑定监听端口,随后,调用它的同步阻塞方法sync等待绑定操作完成。完成之后Netty会返回一个ChannelFuture,它的功能类似于JDK的java.util.concurrent.Future,主要用于异步操作的通知回调。
第32行使用f.channel().closeFut1ue().sync()方法进行阻塞,等待服务端链路关闭之后main函数才退出。
第34 ~36 行调用NIO线程组的shutdownGracefully进行优雅退出,它会释放跟shutdownGracefully关联的资源。
下面看看 TimeServe1·Hand le1·类 是如何实现 的。
3-2 Netty 时 间服务器服务端 TimeServerHandler
1 package com.phei.netty.basic; 2 3 import io.netty.buffer.ByteBuf; 4 import io.netty.buffer.Unpooled; 5 import io.netty.channel.ChannelHandlerAdapter; 6 import io.netty.channel.ChannelHandlerContext; 7 /** 8 * @author lilinfeng 9 * @date 2014年2月14日 10 * @version 1.0 11 */ 12 public class TimeServerHandler extends ChannelHandlerAdapter { 13 14 @Override 15 public void channelRead(ChannelHandlerContext ctx, Object msg) 16 throws Exception { 17 ByteBuf buf = (ByteBuf) msg; 18 byte[] req = new byte[buf.readableBytes()]; 19 buf.readBytes(req); 20 String body = new String(req, "UTF-8"); 21 System.out.println("The time server receive order : " + body); 22 String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date( 23 System.currentTimeMillis()).toString() : "BAD ORDER"; 24 ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); 25 ctx.write(resp); 26 } 27 28 29 @Override 30 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 31 ctx.flush(); 32 } 33 34 @Override 35 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 36 ctx.close(); 37 } 38 }
TimeServerHandler继承自ChannelHandlerAdapter,它用于对网络事件进行读写操作
通常我们只需要关注channelRead和exceptionCaught方法。下面对这两个方法进行简单说明。
第17行做类型转换,将msg转换成Netty的ByteBuf对象。ByteBuf类似于JDK中的java.nio.ByteBuffer对象,不过它提供了更加强大和灵活的功能。通过ByteBuf的readableBytes方法可以获取缓冲区可读的字节数,根据可读的字节数创建byte数组,通过ByteBuf的readBytes方法将缓冲区中的字节数组复制到新建的byte数组中,最后通过newString构造函数获取请求消息。这时对请求消息进行判断,如果是QUERYTIMEORDER,则创建应答消息,通过ChannelHandlerContext的write方法异步发送应答消息给客户端。
第30行我们发现还调用了ChannelHandlerContext 的flush方法,它的作用是将消息发送队列中的消息写入到SocketChannel 中发送给对方。从性能角度考虑,为了防止频繁地唤醒Selector进行消息发送,Netty的write方法并不直接将消息写入SocketChannel中,调用write方法只是把待发送的消息放到发送缓冲数组中,再通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中。
第35行,当发生异常时,关闭ChannelHandlerContext,释放和ChannelHandlerContext相关联的句柄等资源。
通过对代码进行统计分析可以看出,不到30行的业务逻辑代码,即完成了NIO服务端的开发,相比于传统基于JDKNIO原生类库的服务端,代码量大大减少,开发难度也降低了很多。
下面我们继续学习客户端的开发,并使用Netty改造TimeClient
3.3 Netty客户端开发
Netty客户端的开发相比于服务端更简单,下面我们就看下客户端的代码如何实现。
TimeClient开发
3-3 Netty时间服务器客户端TimeClient
1 package com.phei.netty.basic; 2 3 import io.netty.bootstrap.Bootstrap; 4 import io.netty.channel.ChannelFuture; 5 import io.netty.channel.ChannelInitializer; 6 import io.netty.channel.ChannelOption; 7 import io.netty.channel.EventLoopGroup; 8 import io.netty.channel.nio.NioEventLoopGroup; 9 import io.netty.channel.socket.SocketChannel; 10 import io.netty.channel.socket.nio.NioSocketChannel; 11 /** 12 * @author lilinfeng 13 * @date 2014年2月14日 14 * @version 1.0 15 */ 16 public class TimeClient { 17 18 public void connect(int port, String host) throws Exception { 19 // 配置客户端NIO线程组 20 EventLoopGroup group = new NioEventLoopGroup(); 21 try { 22 Bootstrap b = new Bootstrap(); 23 b.group(group).channel(NioSocketChannel.class) 24 .option(ChannelOption.TCP_NODELAY, true) 25 .handler(new ChannelInitializer<SocketChannel>() { 26 @Override 27 public void initChannel(SocketChannel ch) 28 throws Exception { 29 ch.pipeline().addLast(new TimeClientHandler()); 30 } 31 }); 32 33 // 发起异步连接操作 34 ChannelFuture f = b.connect(host, port).sync(); 35 36 // 当代客户端链路关闭 37 f.channel().closeFuture().sync(); 38 } finally { 39 // 优雅退出,释放NIO线程组 40 group.shutdownGracefully(); 41 } 42 } 43 44 /** 45 * @param args 46 * @throws Exception 47 */ 48 public static void main(String[] args) throws Exception { 49 int port = 8080; 50 if (args != null && args.length > 0) { 51 try { 52 port = Integer.valueOf(args[0]); 53 } catch (NumberFormatException e) { 54 // 采用默认值 55 } 56 } 57 new TimeClient().connect(port, "127.0.0.1"); 58 } 59 }
我们从connect方法讲起,在
第20行首先创建客户端处理1/0读写的NioEventLoopGroup线程组,然后继续创建客户端辅助启动类Bootstrap,随后需要对其进行配置。与服务端不同的是,它的Channel需要设置为NioSocketChannel,然后为其添加handler,此处为了简单直接创建匿名内部类,实现initChannel方法p其作用是当创建NioSocketChannel成功之后,在初始化它的时候将它的CbannelHandler设置到ChannelPipeline中,用于处理网络1/0事件。
客户端启动辅助类设置完成之后,调用connect方法发起异步连接,然后调用同步方法等待连接成功。
最后,当客户端连接关闭之后,客户端主函数退出,在退出之前,释放NIO线程组的资源。
3-4 Netty时间服务器客户端TimeClientHandler
1 package com.phei.netty.basic; 2 3 import io.netty.buffer.ByteBuf; 4 import io.netty.buffer.Unpooled; 5 import io.netty.channel.ChannelHandlerAdapter; 6 import io.netty.channel.ChannelHandlerContext; 7 8 import java.util.logging.Logger; 9 10 /** 11 * @author lilinfeng 12 * @date 2014年2月14日 13 * @version 1.0 14 */ 15 public class TimeClientHandler extends ChannelHandlerAdapter { 16 17 private static final Logger logger = Logger 18 .getLogger(TimeClientHandler.class.getName()); 19 20 private final ByteBuf firstMessage; 21 22 /** 23 * Creates a client-side handler. 24 */ 25 public TimeClientHandler() { 26 byte[] req = "QUERY TIME ORDER".getBytes(); 27 firstMessage = Unpooled.buffer(req.length); 28 firstMessage.writeBytes(req); 29 30 } 31 32 @Override 33 public void channelActive(ChannelHandlerContext ctx) { 34 ctx.writeAndFlush(firstMessage); 35 } 36 @Override 37 public void channelRead(ChannelHandlerContext ctx, Object msg) 38 throws Exception { 39 ByteBuf buf = (ByteBuf) msg; 40 byte[] req = new byte[buf.readableBytes()]; 41 buf.readBytes(req); 42 String body = new String(req, "UTF-8"); 43 System.out.println("Now is : " + body); 44 } 45 46 @Override 47 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 48 // 释放资源 49 logger.warning("Unexpected exception from downstream : " 50 + cause.getMessage()); 51 ctx.close(); 52 } 53 }
这里重点关注三个方法:channelActive、channelRead和exceptionCaught当客户端和服务端TCP链路建立成功之后,Netty的NIO线程会调用cbannelActive方法,发送查询时间的指令给服务端,调用ChannelHandlerContext的writeAndFlush方法将请求消息发送给服务端。
当服务端返回应答消息时,channelRead方法被调用,第39~43行从Netty的ByteBuf中读取并打印应答消息。
第47~52行,当发生异常时,打印异常日志,释放客户端资源。
服务端运行结果
客户端运行结果如图
运行结果正确。可以发现,通过Netty开发的NIO服务端和客户端非常简单,短短几十行代码,就能完成之前NIO程序需要几百行才能完成的功能。基于Netty的应用开发不但APl使用简单、开发模式固定,而且扩展性和定制性非常好,后面,我们会通过更多应用来介绍Netty的强大功能。
需要指出的是,本例程依然没有考虑读半包的处理,对于功能演示或者测试,上述程序没有问题F但是稍加改造迸行性能或者压力测试,它就不能正确地工作了。在下一个章节我们会给出能够正确处理半包消息的应用实例。
3.4 打包和部署
基于Netty开发的都是非Web的Java应用,它的打包形态非常简单,就是一个普通的.jar包,通常情况下,在正式的商业开发中,我们会使用三种打包方式对源码进行打包:
(1) Eclipse提供的导出功能。它可以将指定的Java工程或者源码包、代码输出成指定的.jar包,它属于手工操作,当项目模块多之后非常不方便,所以一般不使用这种方式;
(2) 使用ant脚本对工程进行打包。将Netty的应用程序去了包成指定的.jar包,一般会输出一个软件安装包:xxxx_install.gz;
(3) 使用Maven进行工程构建。它可以对模块间的依赖进行管理,支持版本的自动化测试、编译和构建,是目前主流的项目管理工具。
3.5 总结
本章节讲解了Netty的入门应用,通过使用Netty重构时间服务器程序,可以发现相比于传统的NIO程序,Netty的代码更加简洁、开发难度更低,扩展性也更好,非常适合作为基础通信框架被用户集成和使用。
在介绍Netty服务端和客户端时,简单地对代码进行了讲解,由于后续会有专门章节对Netty进行源码分析,所以在Netty应用部分我们不进行详细的源码解读和分析。
第4章会讲解一个稍微复杂的应用,它利用Netty提供的默认编解码功能解决了我们之前没有解决的读半包问题。事实上,对于读半包问题,Netty提供了很多种好的解决方案。下面一起学习一下如何利用Netty默认的编解码功能解决半包读取问题。