原文: 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事件.
package io.netty.example.discard; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.MessageList; /** * Handles a server-side channel. */ public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1) @Override public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { // (2) // Discard the received data silently. msgs.releaseAllAndRecycle(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (3) // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); } }
1. DiscardServerHandler继承了ChannelInboundHandlerAdapter, 这是一个ChannelInboundHandler的实现. ChannelInboundHandlerAdapter提供了各种事件处理方法, 这些方法你可以覆盖. 暂时, 它仅仅足够扩展ChannelInboundHandlerAdapter而不是实现自己的处理接口.
2. 我们在这重写messageReceived事件处理方法. 每当接收到来自客户端的新数据, 这个方法被调用时会传进去一个MessageList的参数, 这个参数包含着收到字节列表. 在这个例子中, 我们仅仅通过调用releaseAllAndRecycle方法丢弃接收到的数据去实现DISCARD协议.
3. exceptionCaught()方法会带着Throwable调用,当Netty发生一个异常.你可能想去发送一个带着错误编码的返回消息在关闭连接前.
到目前为止进展顺利, 我们已经实现了DISCARD服务端的一半. 现在剩下的是写main()方法.
package io.netty.example.discard; import io.netty.bootstrap.ServerBootstrap; 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.NioServerSocketChannel; /** * Discards any incoming data. */ public class DiscardServer { private final int port; public DiscardServer(int port) { this.port = port; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // (2) b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // (3) .childHandler(new ChannelInitializer<SocketChannel>() { // (4) @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new DiscardServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) // (5) .childOption(ChannelOption.SO_KEEPALIVE, true); // (6) // Bind and start to accept incoming connections. ChannelFuture f = b.bind(port).sync(); // (7) // Wait until the server socket is closed. // In this example, this does not happen, but you can do that to gracefully // shut down your server. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port; if (args.length > 0) { port = Integer.parseInt(args[0]); } else { port = 8080; } new DiscardServer(port).run(); } }1. NioEventLoopGroup是一个多线程事件循环处理I/O操作. Netty提供各种EventLoopGroup为实现不同的传输协议. 在这个例子中, 我们实现了服务端的程序, 因此两个NioEventLoopGroup被使用. 第一个叫作"boss", 被用来处理接收到的新连接, 第二个叫作"worker", 一旦"boss"接受了连接并且注册了这个连接就会交给"worker"处理. 使用了多少线程以及根据实现他们是如何映射到被创建的channel并且可以通过构造方法设置.
7. 我们准备要开始了.剩下的就是绑定端口和运行server了. 我们绑定本机所有网卡的8080端口. 你可以调用多次bind()方法只要你想.
观察接收到的数据
现在我们已经写完了第一个服务端程序, 我们需要测试他是否真的可以工作. 最简单的测试方式是使用telnet命令. 例如, 你可以在命令行输入"telnet localhost 8080" 然后再输入一些东西.
然而,我们能说这个服务端程序工作的很好吗? 我们不能真正的知道因为这是一个丢弃协议的服务端程序. 你得不到任务的响应. 为了证明他真的可以工作, 让我们修改一下服务端程序让他打印他接收到的数据.
我们已经知道每当收到数据MessageList会被填充并且messageReceived方法会被调用. 让我们加一个代码到DiscardServerHandler中的messageReceived方法:
@Override public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { MessageList<ByteBuf> messages = msgs.cast(); try { for (ByteBuf in: messages) { while (in.readable()) { // (1) System.out.println((char) buf.readByte()); System.out.flush(); } } } finally { msgs.releaseAllAndRecycle(); // (2) } }1. 这个循环很低效实际上可以简化为: System.out.println(buf.toString(io.netty.util.CharsetUtil.US_ASCII))
写一个Echo服务端
到目前为止, 我们一直消费数据没有任何的响应. 服务端通常情况是响应请求. 让我们学习如何写一个通过实现ECHO协议返回消息到客户端, ECHO协议是收到任何数据都发送回来.
和DISCARD服务端程序唯一的不同的是,之前章节我们已经实现的将收到的数据打印到控制台替换为将收到的数据发送回去.
@Override public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { ctx.write(msgs); // (1) }1. ChannelHandlerContext对象有一个与他相关联的Channel的引用. 这里, 返回的Channel代表的是收到MessageList的连接. 我们可以拿到这个Channel并调用write()方法往远程节点写点东西.
package io.netty.example.time; public class TimeServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(final ChannelHandlerContext ctx) { // (1) final ByteBuf time = ctx.alloc().buffer(4); // (2) time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L); final ChannelFuture f = ctx.write(time); // (3) f.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { assert f == future; ctx.close(); } }); // (4) } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }1. 正如上文, 当有连接建立将会调用channelActive()方法以及准备产生的交互消息. 让我们在这个方法里写一个32位的整型数字代表当前时间.
2. 为了发送消息, 我们需要分配一个新的包含消息的buffer. 我们将写一个32位整型, 因此我们需要一个容量至少4字节的ByteBuf. 通过ChannelHandlerContext.alloc()拿到当前的ByteBufAllocator用来分配一个新的buffer.
3. 通常, 我们会写一个构造好的消息.
但是等等, flip在哪? 在NIO我们在发送消息没有调用ByteBuffer.flip()方法? ByteBuf没有这样一个方法因为他有两个指针; 一个是读操作另一个是写操作. 当你写东西到一个ByteBuf时写索引将增长而读索引不改变. 读索引和写索引分别代表的是消息的开始和结束.
ChannelHandlerContext ctx = ...; ctx.write(message); ctx.close();因为, 你需要在写操作完成通知你后再调用关闭方法. 请注意, 关闭操作也不是立即关闭, 而是返回一个ChannelFuture.
f.addListener(ChannelFutureListener.CLOSE);
Time协议客户端
不像DISCARD和ECHO服务端, 我们需要给TIME协议写一个客户端因为普通人不能转换32位二进制的日历数据. 这部分, 我们讨论如何确实服务端工作正常以及学习如何写一个Netty的客户端.
在Netty里服务端和客户端最大以及唯一的不同是需要的Bootstrap. 看一下接下来的代码段:
package io.netty.example.time; public class TimeClient { public static void main(String[] args) throws Exception { String host = args[0]; int port = Integer.parseInt(args[1]); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); // (1) b.group(workerGroup); (2) b.channel(NioSocketChannel.class); // (3) b.option(ChannelOption.SO_KEEPALIVE, true); // (4) b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); // Start the client. ChannelFuture f = b.connect(host, port).sync(); // (5) // Wait until the connection is closed. f.channel().closeFuture().sync(); } finally { workerGroup.shudownGracefully(); } } }1. Boostrap与ServerBootstrap很相似, 除了他是针对非服务端channel像客户端或者无连接模式的channel.
package io.netty.example.time; import java.util.Date; public class TimeClientHandler extends ChannelInboundHandlerAdapter { @Override public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { ByteBuf m = (ByteBuf) msgs.get(0); // (1) long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L; System.out.println(new Date(currentTimeMillis)); msgs.releaseAllAndRecycle(); ctx.close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
1. 只处理第一条消息. 注意MessageList的大小是大于0的.
这个看起来很简单, 和服务端的例子看起来没有任何的不同. 然而, 这个处理逻辑有时将拒绝工作会发现IndexOutOfBoundsException. 我们将在以后章节讨论为什么会发生.
处理基于流的传输协议
套接字缓冲区的小警告
在基于流的传输协议里就像TCP/IP, 收到的数据会存储到套接字缓冲区. 不幸的是, 基于流传输的缓冲区不是一个数据包队列而是一个字节队列. 这意味着, 即使你发送了两条消息作为两条独立的数据包, 操作系统也不会像两条消息一样处理他们而是为一串字节. 所以, 不保证你读到的正是你远程节点写的. 例如, 让我们假设TCP/IP协议栈的操作系统已经收到三个数据包:
+-----+-----+-----+ | ABC | DEF | GHI | +-----+-----+-----+因为基于流协议的一般性质, 在你的程序里有很高的机会会将以下面这种零散的形式读到他们:
+----+-------+---+---+ | AB | CDEFG | H | I | +----+-------+---+---+因此, 收到的部分, 无论是服务端或者客户端, 应该整理零散的收到的数据到一个或多个有意义的框(frames)通过程序逻辑可以容易的理解. 在上面的例子, 收到的数据应该像下面这样被装框:
+-----+-----+-----+ | ABC | DEF | GHI | +-----+-----+-----+
package io.netty.example.time; import java.util.Date; public class TimeClientHandler extends ChannelInboundHandlerAdapter { private ByteBuf buf; @Override public void handlerAdded(ChannelHandlerContext ctx) { buf = ctx.alloc().buffer(4); // (1) } @Override public void handlerRemoved(ChannelHandlerContext ctx) { buf.release(); // (1) buf = null; } @Override public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { for (ByteBuf m: msgs.<ByteBuf>cast()) { // (2) buf.writeBytes(m); // (3) } msgs.releaseAllAndRecycle(); if (buf.readableBytes() >= 4) { // (4) long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L; System.out.println(new Date(currentTimeMillis)); ctx.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
1. 一个ChannelHandler有两个生命周期的监听器方法: handlerAdded() 和 handlerRemoved(). 你可以执行任意初始化(反初始化)任务只要不要阻塞太长时间.
2. MessageList.<T>cast()能够让你转换MessageList的参数类型而不用看到烦人未检查类型警告.
3. 首先, 所有接收到的数据应该累积到缓冲区.
4. 然后, 这个处理单元检查是否有足够的数据, 这人例子是4个字节, 然后进行实际的业务逻辑. 此外, 当有更多的数据收到将调用messageReceived方法.
第二个解决方案
虽然第一个解决方案已经解决TIME客户端的问题, 被修改后的处理单元看起来不清晰. 想像一下一个更复杂的协议组合多个字段就像可变长度字段. 你的ChannelHandler的实现将很快变的难以维护.
正如你注意到的, 你可以添加超过一个ChannelHandler到ChannelPipeline, 所以, 你可以分割一个复杂庞大的ChannelHandler到多个模块去减少你程序的复杂性. 例如, 你可以分割TimeClientHandler到两个处理单元:
幸好, Netty提供了一个可扩展类帮助你写第一个立即可用.
package io.netty.example.time; public class TimeDecoder extends ByteToMessageDecoder { // (1) @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, MessageList<Object> out) { // (2) if (in.readableBytes() < 4) { return; // (3) } out.add(in.readBytes(4)); // (4) } }1. ByteToMessageDecoder是一个ChannelHandler的实现, 他使处理分段问题更容易.
4. 如果decode()方法添加一个对象到out, 这意味着解码器成功解码一条消息. ByteToMessageDecoder将丢弃内部累积缓冲区读到的部分. 请记住你不需要解码多条消息. ByteToMessageDecoder将一直调用decoder方法直接没有任何可读数据.
现在我们有另一个处理单元需要插入到ChannelPipeline, 我们应该更改ChannelInitialized的实现:
b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler()); } });另外, Netty提供了立即可用的解码器让你更容易的实现更多的协议以及帮你避免处理一个宠大且难以维护的处理单元的实现. 请查看以面的包获取更多信息: