(入门篇 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,它用于对网络事件进行读写操作
通常我们只需要关注channelReadexceptionCaught方法。下面对这两个方法进行简单说明。

第17行做类型转换,将msg转换成Netty的ByteBuf对象。ByteBuf类似于JDK中的java.nio.ByteBuffer对象,不过它提供了更加强大和灵活的功能。通过ByteBufreadableBytes方法可以获取缓冲区可读的字节数,根据可读的字节数创建byte数组,通过ByteBufreadBytes方法将缓冲区中的字节数组复制到新建的byte数组中,最后通过newString构造函数获取请求消息。这时对请求消息进行判断,如果是QUERYTIMEORDER,则创建应答消息,通过ChannelHandlerContextwrite方法异步发送应答消息给客户端。


第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 }

 

 

这里重点关注三个方法:channelActivechannelReadexceptionCaught当客户端和服务端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默认的编解码功能解决半包读取问题。

 

posted @ 2015-10-21 17:15  crazyYong  阅读(1526)  评论(0编辑  收藏  举报