Netty4.0 开发指导 1

Netty4.0 开发指导 1

原文: http://netty.io/wiki/user-guide-for-4.x.html

前言

1. 问题
如今我们使用通用的程序或者第三方的库去与对方交互. 比如,我们经常使用HTTP库从web服务器检索信息或者通过web服务调用远程方法.
然而,通用的协议或者它的实现有时扩展性不好. 这就像我们不使用通用的HTTP服务器去交换大文件, e-mail消息, 以及近实时消息像股票信息和多人游戏数据. 多么需要一个专门为了特殊用途高度优化的协议实现. 比如, 你可能想实现一个优化的HTTP服务器用于基于AJAX的聊天应用, 媒体流传输, 或者大文件传输.  你甚至可能想去设计和实现一个完整的新协议,为你的需求量身定制.
另一个不可避免的情况是当你必须确保一个遗留专有协议和一个老的系统交互. 在这种情况下, 重要的是多快我们能实现这个协议, 还不牺牲应用程序所产生的稳定性和性能.

2.解决方案
Netty项目努力提供一个异步的事件驱动网络程序框架和工具, 为快速开发易于维护的高性能, 高可扩展性的协议服务器和客户端.
换句话说, Netty是一个NIO客户端/服务器架构, 可以快速和容易的开发网络程序就像协议服务器和客户端.它极大的简化了网络开发, 如TCP和UDP套接字服务器的开发.
"快速和容易"不是意味着产生的程序将受到来自于可维护性和性能问题的损害. 带着来自于大量协议如FTP, SMTP, HTTP以及各种二进制和基于文本的传统协议的实现的经验, Netty被精心设计.  所以, Netty成功的找到一种方法去实现简易开发, 性能, 稳定性和灵活性不冲突.
一些用户可能已经发现其他的一些网络程序框架声称有相同的优势, 你可能想问什么使Netty与他们如此的不同. 答案在它建立的理念. Netty的设计给你来自于API条款和实施之日起两者最舒适的体验. 这不是有形的东西,但你将意识到这个理念将使你的生活更容易当你阅读这个指南和玩转Netty.

第1章 Getting Started
这章围绕着Netty的核心结构和一些简单例子可以让你快速上手.  当你读完本章你将能够马上写一个基于Netty的客户端和服务端. 

在开始之前
在本章介绍的例子运行的最低需求只有两个, 最新版本的Netty和JDK1.6或以上.

写一个Discard服务端
世界最简单的协议不是"Hello World!"是DISCARD. 这个协议会丢弃任务接收到的数据没有响应.
去实现DISCARD协议, 唯一需要做的是忽略所有接收到的数据. 让我们直接从handler的实现开始, 它处理Netty产生的I/O事件.

01 package io.netty.example.discard;
02  
03 import io.netty.channel.ChannelHandlerContext;
04 import io.netty.channel.ChannelInboundHandlerAdapter;
05 import io.netty.channel.MessageList;
06  
07 /**
08  * Handles a server-side channel.
09  */
10 public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
11  
12     @Override
13     public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { // (2)
14         // Discard the received data silently.
15         msgs.releaseAllAndRecycle();
16     }
17  
18     @Override
19     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (3)
20         // Close the connection when an exception is raised.
21         cause.printStackTrace();
22         ctx.close();
23     }
24 }

 

1. DiscardServerHandler继承了ChannelInboundHandlerAdapter, 这是一个ChannelInboundHandler的实现. ChannelInboundHandlerAdapter提供了各种事件处理方法, 这些方法你可以覆盖. 暂时, 它仅仅足够扩展ChannelInboundHandlerAdapter而不是实现自己的处理接口.
2. 我们在这重写messageReceived事件处理方法. 每当接收到来自客户端的新数据, 这个方法被调用时会传进去一个MessageList的参数, 这个参数包含着收到字节列表. 在这个例子中, 我们仅仅通过调用releaseAllAndRecycle方法丢弃接收到的数据去实现DISCARD协议.
3. exceptionCaught()方法会带着Throwable调用,当Netty发生一个异常.你可能想去发送一个带着错误编码的返回消息在关闭连接前.

到目前为止进展顺利, 我们已经实现了DISCARD服务端的一半. 现在剩下的是写main()方法.

01 package io.netty.example.discard;
02      
03 import io.netty.bootstrap.ServerBootstrap;
04 import io.netty.channel.ChannelFuture;
05 import io.netty.channel.ChannelInitializer;
06 import io.netty.channel.EventLoopGroup;
07 import io.netty.channel.nio.NioEventLoopGroup;
08 import io.netty.channel.socket.SocketChannel;
09 import io.netty.channel.socket.nio.NioServerSocketChannel;
10      
11 /**
12  * Discards any incoming data.
13  */
14 public class DiscardServer {
15      
16     private final int port;
17      
18     public DiscardServer(int port) {
19         this.port = port;
20     }
21      
22     public void run() throws Exception {
23         EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
24         EventLoopGroup workerGroup = new NioEventLoopGroup();
25         try {
26             ServerBootstrap b = new ServerBootstrap(); // (2)
27             b.group(bossGroup, workerGroup)
28              .channel(NioServerSocketChannel.class) // (3)
29              .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
30                  @Override
31                  public void initChannel(SocketChannel ch) throws Exception {
32                      ch.pipeline().addLast(new DiscardServerHandler());
33                  }
34              })
35              .option(ChannelOption.SO_BACKLOG, 128)          // (5)
36              .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
37      
38             // Bind and start to accept incoming connections.
39             ChannelFuture f = b.bind(port).sync(); // (7)
40      
41             // Wait until the server socket is closed.
42             // In this example, this does not happen, but you can do that to gracefully
43             // shut down your server.
44             f.channel().closeFuture().sync();
45         } finally {
46             workerGroup.shutdownGracefully();
47             bossGroup.shutdownGracefully();
48         }
49     }
50      
51     public static void main(String[] args) throws Exception {
52         int port;
53         if (args.length > 0) {
54             port = Integer.parseInt(args[0]);
55         } else {
56             port = 8080;
57         }
58         new DiscardServer(port).run();
59     }
60 }

1. NioEventLoopGroup是一个多线程事件循环处理I/O操作.  Netty提供各种EventLoopGroup为实现不同的传输协议. 在这个例子中, 我们实现了服务端的程序,  因此两个NioEventLoopGroup被使用. 第一个叫作"boss", 被用来处理接收到的新连接, 第二个叫作"worker", 一旦"boss"接受了连接并且注册了这个连接就会交给"worker"处理. 使用了多少线程以及根据实现他们是如何映射到被创建的channel并且可以通过构造方法设置.  
2. ServerBootstrap是设置服务端的辅助类. 你可以设置服务端直接用channel. 然而, 请注意这一个繁锁的过程,大多数情况下你不需要这样做. 
3. 这里, 我们指定使用NioServerSocketChannel类, 这个类被用来初始化一个新的Channel以接用连上来的连接. 
4. 这个处理单元将一直被一个新的连接上来的Channel运行. ChannelInitialize是被指定的处理单元, 用来帮助用户设置一个新的Channel. 这里最有可能的操作是通过添加一些处理单元到一个新的Channel的ChannelPipeline, 这些处理单元像 就像DiscardServerHandler来实现你的网络程序  由于程序变的复杂, 很可能你将添加更多的handlers到流水线上, 最终扩展这个匿名类成为顶级类.  
5.  你也可以设置参数指定channel的实现. 我们是在写一个TCP/IP的服务端,所以我们允许设置套接字选项就像tcpNoDelay和keepAlive.  
6. 你有注意到option()和childOption()吗? option()是针对NioServerSocketChannel用来接受连上来的连接. childOption()是针对通过上层ServerChannel接受到所有Channel.  

7. 我们准备要开始了.剩下的就是绑定端口和运行server了. 我们绑定本机所有网卡的8080端口. 你可以调用多次bind()方法只要你想.

观察接收到的数据
现在我们已经写完了第一个服务端程序, 我们需要测试他是否真的可以工作. 最简单的测试方式是使用telnet命令. 例如, 你可以在命令行输入"telnet localhost 8080" 然后再输入一些东西.
然而,我们能说这个服务端程序工作的很好吗? 我们不能真正的知道因为这是一个丢弃协议的服务端程序. 你得不到任务的响应. 为了证明他真的可以工作, 让我们修改一下服务端程序让他打印他接收到的数据.
我们已经知道每当收到数据MessageList会被填充并且messageReceived方法会被调用. 让我们加一个代码到DiscardServerHandler中的messageReceived方法:

01 @Override
02 public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) {
03     MessageList<ByteBuf> messages = msgs.cast();
04     try {
05         for (ByteBuf in: messages) {
06             while (in.readable()) { // (1)
07                 System.out.println((char) buf.readByte());
08                 System.out.flush();
09             }
10         }
11     } finally {
12         msgs.releaseAllAndRecycle(); // (2)
13     }
14 }

1. 这个循环很低效实际上可以简化为: System.out.println(buf.toString(io.netty.util.CharsetUtil.US_ASCII)) 
2. MessageList.releaseAllAndRecycle()将释放池化消息的所有引用计数并将自己返回到对象池中. 
如果你再次运行telnet命令, 你将看到服务端接收到的数据. 

写一个Echo服务端
到目前为止, 我们一直消费数据没有任何的响应. 服务端通常情况是响应请求. 让我们学习如何写一个通过实现ECHO协议返回消息到客户端, ECHO协议是收到任何数据都发送回来.
和DISCARD服务端程序唯一的不同的是,之前章节我们已经实现的将收到的数据打印到控制台替换为将收到的数据发送回去.

1 @Override
2 public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) {
3     ctx.write(msgs); // (1)
4 }

1. ChannelHandlerContext对象有一个与他相关联的Channel的引用. 这里, 返回的Channel代表的是收到MessageList的连接. 我们可以拿到这个Channel并调用write()方法往远程节点写点东西.  

Time协议服务端 
这部分实现的是TIME协议. 这里与之前例子的不同是, 发送一个消息, 他包含一个32位整型, 一旦这个消息发出后将不接收任何请求和断开连接. 在这个例子里, 你将学习如何构建和发送消息, 以及在完成后关闭连接. 
因为我们将忽略任务收到的数据但是一旦建立了连接就会发送消息, 我们不能使用messageReceived()方法. 我们应该覆盖channelActivie()方法代替. 下面是实现: 

01 package io.netty.example.time;
02  
03 public class TimeServerHandler extends ChannelInboundHandlerAdapter {
04  
05     @Override
06     public void channelActive(final ChannelHandlerContext ctx) { // (1)
07         final ByteBuf time = ctx.alloc().buffer(4); // (2)
08         time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L);
09          
10         final ChannelFuture f = ctx.write(time); // (3)
11         f.addListener(new ChannelFutureListener() {
12             @Override
13             public void operationComplete(ChannelFuture future) {
14                 assert f == future;
15                 ctx.close();
16             }
17         }); // (4)
18     }
19      
20     @Override
21     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
22         cause.printStackTrace();
23         ctx.close();
24     }
25 }

1. 正如上文, 当有连接建立将会调用channelActive()方法以及准备产生的交互消息. 让我们在这个方法里写一个32位的整型数字代表当前时间.  

2. 为了发送消息, 我们需要分配一个新的包含消息的buffer. 我们将写一个32位整型, 因此我们需要一个容量至少4字节的ByteBuf.  通过ChannelHandlerContext.alloc()拿到当前的ByteBufAllocator用来分配一个新的 buffer. 
3. 通常, 我们会写一个构造好的消息. 
但 是等等, flip在哪? 在NIO我们在发送消息没有调用ByteBuffer.flip()方法? ByteBuf没有这样一个方法因为他有两个指针; 一个是读操作另一个是写操作. 当你写东西到一个ByteBuf时写索引将增长而读索引不改变. 读索引和写索引分别代表的是消息的开始和结束.  

比较之下, NIO的缓冲区没有提供一个清楚的方式的找出消息内容的开始和结束没有调用flip方法. 当你忘记flip缓冲区时你将有大麻烦因为没有数据或者错误的数据将被发送. 类似这种错误在Netty中不会发生因为我们有不同的指针对应不同的操作类型.  
另一个注意的要点是写方法会返回一个ChannelFuture. ChannelFuture代表一个还没有完成的I/O操作. 这意味着, 请求操作可能还没有被执行完成因为在Netty中所有的操作都是异步的. 例如, 下面的代码可能关闭操作甚至在发送消息之前: 

1 ChannelHandlerContext ctx = ...;
2 ctx.write(message);
3 ctx.close();

因为, 你需要在写操作完成通知你后再调用关闭方法. 请注意, 关闭操作也不是立即关闭, 而是返回一个ChannelFuture. 
4. 当你写请求完成后我们如何得到通知? 这是尽量简单的给返回的ChannelFuture添加一个ChannelFutureListener. 这里, 当操作结束我们创建了一个匿名ChannelFutureListener关闭Channel.  
或者, 你可以简化代码使用预定义listener: 

1 f.addListener(ChannelFutureListener.CLOSE);


Time协议客户端
不像DISCARD和ECHO服务端, 我们需要给TIME协议写一个客户端因为普通人不能转换32位二进制的日历数据. 这部分, 我们讨论如何确实服务端工作正常以及学习如何写一个Netty的客户端.
在Netty里服务端和客户端最大以及唯一的不同是需要的Bootstrap. 看一下接下来的代码段:

01 package io.netty.example.time;
02  
03  
04 public class TimeClient {
05     public static void main(String[] args) throws Exception {
06         String host = args[0];
07         int port = Integer.parseInt(args[1]);
08         EventLoopGroup workerGroup = new NioEventLoopGroup();
09          
10         try {
11             Bootstrap b = new Bootstrap(); // (1)
12             b.group(workerGroup); (2)
13             b.channel(NioSocketChannel.class); // (3)
14             b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
15             b.handler(new ChannelInitializer<SocketChannel>() {
16                 @Override
17                 public void initChannel(SocketChannel ch) throws Exception {
18                     ch.pipeline().addLast(new TimeClientHandler());
19                 }
20             });
21              
22             // Start the client.
23             ChannelFuture f = b.connect(host, port).sync(); // (5)
24  
25             // Wait until the connection is closed.
26             f.channel().closeFuture().sync();
27         } finally {
28             workerGroup.shudownGracefully();
29         }
30     }
31 }

1. Boostrap与ServerBootstrap很相似, 除了他是针对非服务端channel像客户端或者无连接模式的channel.  
2. 如果只指定了一个EventLoopGroup, 他将被用来作为"boss"组和"worker"组. 虽然针对客户端"boss"组是不会被使用的.  
3. 替换NioServerSocketChannel, NioSocketChannel被用来创建客户端的Channel.  
4. 注意我们没有使用childOption(), 这里不像ServerBootstrap, 因为客户端的SocketChannel没有父级.  
5. 我们应该调用connect()方法而不是bind()方法.  

正如你能看到的, 他与服务端的代码不是真的不同. 实现了什么样的ChannelHandler? 他应该从服务端收到一个32位整型数字, 转换为普通人可以读懂的格式, 打印后关闭连接: 

01 package io.netty.example.time;
02  
03 import java.util.Date;
04  
05 public class TimeClientHandler extends ChannelInboundHandlerAdapter {
06     @Override
07     public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) {
08         ByteBuf m = (ByteBuf) msgs.get(0); // (1)
09         long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L;
10         System.out.println(new Date(currentTimeMillis));
11         msgs.releaseAllAndRecycle();
12         ctx.close();
13     }
14  
15     @Override
16     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
17         cause.printStackTrace();
18         ctx.close();
19     }
20 }

1. 只处理第一条消息. 注意MessageList的大小是大于0的. 
这个看起来很简单, 和服务端的例子看起来没有任何的不同. 然而, 这个处理逻辑有时将拒绝工作会发现IndexOutOfBoundsException. 我们将在以后章节讨论为什么会发生. 

处理基于流的传输协议

套接字缓冲区的小警告
在基于流的传输协议里就像TCP/IP, 收到的数据会存储到套接字缓冲区. 不幸的是, 基于流传输的缓冲区不是一个数据包队列而是一个字节队列. 这意味着, 即使你发送了两条消息作为两条独立的数据包, 操作系统也不会像两条消息一样处理他们而是为一串字节. 所以, 不保证你读到的正是你远程节点写的. 例如, 让我们假设TCP/IP协议栈的操作系统已经收到三个数据包:

1 +-----+-----+-----+
2 | ABC | DEF | GHI |
3 +-----+-----+-----+

因为基于流协议的一般性质, 在你的程序里有很高的机会会将以下面这种零散的形式读到他们: 

1 +----+-------+---+---+
2 | AB | CDEFG | H | I |
3 +----+-------+---+---+

因此, 收到的部分, 无论是服务端或者客户端, 应该整理零散的收到的数据到一个或多个有意义的框(frames)通过程序逻辑可以容易的理解. 在上面的例子, 收到的数据应该像下面这样被装框: 

1 +-----+-----+-----+
2 | ABC | DEF | GHI |
3 +-----+-----+-----+


第一个解决方案 
现在让我们回去看一下TIME客户端的例子. 也有相同的问题. 32位整型数字是非常小的数据, 不太可能经常被分段. 然而, 问题是他也可能被分段, 随着流量的增加分段的可能性也将增加.  
简单的解决方案是创建一个内部的累积缓冲区并且一直等到缓冲区收到超过4个字节的数据. 下面是更改TimeClientHandler的实现并解决了问题: 

01 package io.netty.example.time;
02  
03 import java.util.Date;
04  
05 public class TimeClientHandler extends ChannelInboundHandlerAdapter {
06     private ByteBuf buf;
07      
08     @Override
09     public void handlerAdded(ChannelHandlerContext ctx) {
10         buf = ctx.alloc().buffer(4); // (1)
11     }
12      
13     @Override
14     public void handlerRemoved(ChannelHandlerContext ctx) {
15         buf.release(); // (1)
16         buf = null;
17     }
18      
19     @Override
20     public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) {
21         for (ByteBuf m: msgs.<ByteBuf>cast()) { // (2)
22             buf.writeBytes(m); // (3)
23         }
24         msgs.releaseAllAndRecycle();
25          
26         if (buf.readableBytes() >= 4) { // (4)
27             long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L;
28             System.out.println(new Date(currentTimeMillis));
29             ctx.close();
30         }
31     }
32      
33     @Override
34     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
35         cause.printStackTrace();
36         ctx.close();
37     }
38 }

1. 一个ChannelHandler有两个生命周期的监听器方法: handlerAdded() 和 handlerRemoved(). 你可以执行任意初始化(反初始化)任务只要不要阻塞太长时间. 
2. MessageList.<T>cast()能够让你转换MessageList的参数类型而不用看到烦人未检查类型警告. 
3. 首先, 所有接收到的数据应该累积到缓冲区.
4. 然后, 这个处理单元检查是否有足够的数据, 这人例子是4个字节, 然后进行实际的业务逻辑. 此外, 当有更多的数据收到将调用messageReceived方法. 

第二个解决方案
虽然第一个解决方案已经解决TIME客户端的问题, 被修改后的处理单元看起来不清晰. 想像一下一个更复杂的协议组合多个字段就像可变长度字段. 你的ChannelHandler的实现将很快变的难以维护. 
正如你注意到的, 你可以添加超过一个ChannelHandler到ChannelPipeline, 所以, 你可以分割一个复杂庞大的ChannelHandler到多个模块去减少你程序的复杂性. 例如, 你可以分割TimeClientHandler到两个处理单元:

  • TimeDecoder处理分段问题
  • TimeClientHandler初始化的简化版本

幸好,  Netty提供了一个可扩展类帮助你写第一个立即可用. 

01 package io.netty.example.time;
02  
03 public class TimeDecoder extends ByteToMessageDecoder { // (1)
04     @Override
05     protected void decode(ChannelHandlerContext ctx, ByteBuf in, MessageList<Object> out) { // (2)
06         if (in.readableBytes() < 4) {
07             return; // (3)
08         }
09          
10         out.add(in.readBytes(4)); // (4)
11     }
12 }

1. ByteToMessageDecoder是一个ChannelHandler的实现, 他使处理分段问题更容易.  
2. 每当有新数据收到, ByteToMessageDecoder都会调用decode()方法并带着一个内部维护的累积缓冲区. 
3. 如果累积缓冲区没有足够的数据decode()方法将不会添加任何东西到out中. 当再有数据收到ByteToMessageDecoder将再次调用decode()方法.  

4. 如果decode()方法添加一个对象到out, 这意味着解码器成功解码一条消息. ByteToMessageDecoder将丢弃内部累积缓冲区读到的部分. 请记住你不需要解码多条消息. ByteToMessageDecoder将一直调用decoder方法直接没有任何可读数据.

现在我们有另一个处理单元需要插入到ChannelPipeline, 我们应该更改ChannelInitialized的实现: 

1 b.handler(new ChannelInitializer<SocketChannel>() {
2     @Override
3     public void initChannel(SocketChannel ch) throws Exception {
4         ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
5     }
6 });

另外, Netty提供了立即可用的解码器让你更容易的实现更多的协议以及帮你避免处理一个宠大且难以维护的处理单元的实现. 请查看以面的包获取更多信息: 

  • io.netty.example.factorial 针对二进制协议
  • io.netty.example.telnet 针对基于文本行协议

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章