Netty 3.2 用户手册
快速有效的网络应用开发
3.2.4.Final 译者:张立明 Larry Zhang
[size=large][color=red]下面的格式很差,建议下载附件并阅读。[/color][/size]
--------------------------------------------------------------------------------
前言
1. 问题提出
2. 解决方案
1. 开始
1.1. 写在开始之前
1.2. 编写一个Discard服务
1.3. 详解Received Data
1.4. 编写一个Echo服务
1.5. 编写一个Time服务
1.6. 编写一个Time客户端
1.7. 处理基于流的传输
1.7.1. 套接字缓存(Socket Buffer)的一个小警示
1.7.2. 第一个解决办法
1.7.3. 第二个解决办法
1.8. 用POJO取代ChannelBuffer
1.9. 关闭应用程序
1.10. 总结
2. 架构概览
2.1. 丰富的缓存数据结构
2.2. 统一的异步 I/O API
2.3. 基于拦截者链(Interceptor Chain)模式的事件模型
2.4. 为更快捷开发的高级组件
2.4.1. 编码框架
2.4.2. SSL / TLS 支持
2.4.3. HTTP实现
2.4.4. Google Protocol Buffer集成
2.5. 总结
前言
1. 问题提出
当前我们使用通用的应用或库来相互通信。比如,我们常常使用HTTP客户端库来从WEB服务器上获取信息,并通过Web Service来调用一个远程过程。
然而,一个通用的协议或者它的实现,有时候并不能很好的扩展。这一点类似于我们不适用通用的HTTP服务器来交换大的文件、电子邮件和诸如财务信息和多人游戏数据等近乎于实时的数据。这些东西需要根据其特定用途而进行高度优化的协议实现。例如,你可能需要一个专门针对基于AJAX的聊天应用、针对多媒体流、或者大的文件传输进行了优化的HTTP服务器。你甚至可能想设计并实现一个完全按照你的需求而定义的全新的协议。
另一种情况也是难以避免的。那就是,为了和一个既有的旧系统进行交互,你必须处理旧系统上使用的协议。这时,在不牺牲稳定性和性能的前提下,你能够在多长时间内实现那个协议就非常重要。
2. 解决方案
Netty项目 是一个提供异步的、事件驱动的网络应用框,是一套有助于快速开发出高性能、高扩展性的、高可维护性的协议的服务器或客户端的开发工具。
换而言之,Netty是一个基于NIO的C/S框架。这套框架可以快速、简单地开发出网络协议的客户端和服务器端应用。它可以大大简化、流程化TCP和UDP套接字的服务器开发过程。
“快速和简单”并不意味着开发出的应用会遇到可维护性、性能等问题。Netty是建立在从许多网络协议(如FTP、SMTP、HTTP和各种二进制和文本协议等)中借鉴的经验基础上精心设计出的。这使得Netty在开发的简单化、性能、稳定性、灵活性等方面都同时达到了设计目标。
一些用户可能已经发现了其他的一些网络应用框架。这些框架也宣称具有相同的优势。这时你可能会问:Neey有什么不同?答案是“道不同”。Netty设计的原则是:给你提供从API到实现以最舒适的体验。这一点是看不到摸不着的。但你在阅读这个文档、以及应用Netty过程中,你会体验到我们的这个设计原则使得一切变得轻松容易。
Chapter 1. 开始
1.1. 开始之前
1.2. 编写一个Discard服务
1.3. 详解Received Data
1.4. 编写一个Echo服务
1.5. 编写一个Time服务
1.6. 编写一个Time客户端
1.7. 处理基于流的传输
1.7.1. 套接字缓存(Socket Buffer)的一个小警示
1.7.2. 第一个解决办法
1.7.3. 第二个解决办法
1.8. 用POJO取代ChannelBuffer
1.9. 关闭你的应用程序
1.10. 总结
这一章围绕着Neey的核心构成讲述,并提供了简单的例子以便快速上手。读到本章末尾,你将可以写一个基于Netty的客户端和服务器。
如果你喜欢自顶向下的学习方式,你应该从 Chapter 2, 架构概览 开始,然后再回到这里。
1.1. 开始之前
运行本章中的例子最低的要求只有两个:最新版本的Netty和JDK1.5或更高版本。最新的Netty可以在此 下载。 要下载到正确的JDK版本,请参考你选择的JDK提供商的网站。
在读的过程中,你会对本章中涉及的类有更多的疑问。当你想了解更多的时候,请参考API文档。所有的类名,都非常方便地连接到了在线的API页面上。此外,记得联系我们 Netty社区 并告诉我们是否有些信息不正确,语法或者拼写等错误,或者你有一个提高这个文档的好办法。
1.2. 编写一个Discard服务
这个世界上最简单的协议不是“Hello,World!”,而是DISCARD。 这个协议丢弃所有的收到的数据,不给任何回应。
为了实现这个DISCARD协议,你唯一要做的事情就是忽略所有收到的数据。我们直接从处理器(handler)的实现开始。这个处理器处理Netty生成的I/O事件。
package org.jboss.netty.example.discard;
public class DiscardServerHandler extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
Channel ch = e.getChannel();
ch.close();
}
}
DiscardServerHandler继承SimpleChannelHandler, ChannelHandler的接口实现类。 SimpleChannelHandler 提供各种事件的处理方法,你可以重载它们。到目前为止,继承 SimpleChannelHandler,而不是你自己去实现一个handler接口,是足够的。
我们在这里重载了messageReceived 事件处理方法。这个方法调用时提供了MessageEvent,它包含着刚刚从客户端收到的新数据。在这个例子中,我们通过什么也不做,来忽略收到的数据,从而实现DISCARD协议。
当一个异常因为I/O错误由Netty抛出,或者由在处理事件过程中,handler的实现抛出了异常,exceptionCaught 方法会被调用,并提供了ExceptionEvent。 尽管在特定情况下,实现这个方法时,你需要对异常有不同的处理,但通常情况下,被捕获的异常应该被记录,并且对应的channel应该被关闭。例如,你可能想在关闭链接之前发送一个错误代码的回应信息。
到此为止,我们已经实现了DISCARD服务的一半。接下来需要写 main 方法来运行这个配备了 DiscardServerHandler的服务。
package org.jboss.netty.example.discard;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class DiscardServer {
public static void main(String[] args) throws Exception {
ChannelFactory factory =
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ServerBootstrap bootstrap = new ServerBootstrap(factory);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
return Channels.pipeline(new DiscardServerHandler());
}
});
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
bootstrap.bind(new InetSocketAddress(8080));
}
}
ChannelFactory 是创建并管理 Channel和它们相关资源的工厂。它处理所有的I/O 请求,执行I/O来生成ChannelEvent。Netty提供多种ChannelFactory实现。我们现在正在实现一个服务器端的例子,因此我们使用NioServerSocketChannelFactory。另一个需要知道的是,它并不是自行创建I/O线程。它试图从你在构造方法中指定的线程池中获得线程。对于线程是如何在你的应用运行环境中去管理的,它给了更多的控制,比如一个具有安全管理机制的应用服务器。
ServerBootstrap是一个建立服务器的工具类。你当然可以直接使用Channel来构建一个服务器,但你要清楚这将是一个繁琐的过程,而其实你根本没必要这么做。
这里,我们配置了ChannelPipelineFactory。当一个新的连接接入到服务器,一个新的ChannelPipeline将由指定的ChannelPipelineFactory来创建。这个新的Pipeline包含着DiscardServerHandler。随着这个应用逐步完善,最终实际你就是添加更多的handler到Pipeline,并抽象出这个匿名类成为一个顶级类。
你还可以设置针对Channel实现的特定参数。我们正在编写的是TCP/IP服务,所以我们可以设置套接字的选项参数,如tcpNoDelay 和 keepAlive。请注意到这个"child."前缀出现在所有参数前,它意味着这个选项参数应用于接入的Channel,而不是ServerSocketChannel的参数。你可以按下面做法来为ServerSocketChannel设定参数。
bootstrap.setOption("reuseAddress", true);
快要可以运行了。接下来要做的是绑定端口并启动服务。这里我们绑定所有本机网卡的端口8080。你可以用不同的绑定地址来多次调用bind方法。
哈哈!我们在Netty上构建了第一个服务器应用。
1.3. 详解Received Data
刚才我们已经写了我们第一个服务器。现在需要的是测试一下它的运行情况。最简单的测试方法,莫过于使用telnet命令了。例如,你可以在命令行上输入"telnet localhost 8080",然后随便输入些什么。
问题是,我们能说这个服务器运行正常吗?很难说的,因为它是一个“丢弃”服务,根本没有任何回应。为了证实它却是运转正常,我们改一下这个服务,让它打印出收到的数据。
我们已经知道了,当收到了数据时,会生成MessageEvent,还会调用messageReceived 处理器方法。我们可以在DiscardServerHandler的messageReceived中加入代码:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer buf = (ChannelBuffer) e.getMessage();
while(buf.readable()) {
System.out.println((char) buf.readByte());
System.out.flush();
}
}
在套接字中传递的消息永远都是ChannelBuffer。ChannelBuffer是一个核心的数据结构,它存储着Netty中的字节序列。它很像NIO中的ByteBuffer,不过更加简单和灵活了。例如,Netty允许你构建一个由多个ChannelBuffer复合而成的ChannelBuffer,以减少不必要的内存复制。
尽管它在很多方面都和NIO的ByteBuffer相像,仍然建议参考一下API手册。学习如何正确使用ChannelBuffer是使用轻松驾驭Netty的重要一步。
如果你再次运行 telnet 命令,你会看到服务器打印出它收到的内容。
“丢弃”服务器的全部源代码位于 org.jboss.netty.example.discard 包。
1.4. 编写一个Echo服务
截至目前,我们已经实现了数据的获取,但没有回应。实际上,一个服务器应该对请求给予回应的。我们看一下如何通过实现ECHO协议给客户端一个回应,把收到的数据送回去。
这和我们在上一节中实现的“丢弃”服务之间唯一不同的是它把收到的数据又发送给回客户端,而不是在服务器端打印出来。为此,修改一下messageReceived方法就可以了:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
Channel ch = e.getChannel();
ch.write(e.getMessage());
}
ChannelEvent对象有一个相应的Channel的引用。这里返回的Channel 代表收到消息事件MessageEvent的那个连接。 我们可以得到这个Channel调用它的 write 方法来写数据给对等的远端。
如果你再次运行telnet,你会看到服务器把你发给它的都送了回来。
Echo服务器的全部代码位于 org.jboss.netty.example.echo包。
1.5. 编写一个Time服务
这小节我们要实现的协议是 TIME。和上个例子不同,它发送一个包含32位证书的消息,发送完毕后不需要收到任何回应就关闭连接。在这个例子中,你将学习如何构建并发送一个消息,然后关闭连接。
连接建立后,收到的任何数据都被忽略不计,而仅仅是发送一个消息。因此,我们这次不能使用 messageReceived 方法,而是需要重载 channelConnected 方法。代码如下:
package org.jboss.netty.example.time;
public class TimeServerHandler extends SimpleChannelHandler {
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
Channel ch = e.getChannel();
ChannelBuffer time = ChannelBuffers.buffer(4);
time.writeInt(System.currentTimeMillis() / 1000);
ChannelFuture f = ch.write(time);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
Channel ch = future.getChannel();
ch.close();
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
我们前面解释过, channelConnected方法在连接建立的时候被调用。我们在这里发送32位整数来表示当前时间(单位:秒)。
为了发送新消息,需要分配一个用来包含消息的缓存。我们需要发送的是32位整数,所以我们需要一个4字节容量的ChannelBuffer。这个ChannelBuffers工具类用来分配新的缓存。除了这个buffer 方法, ChannelBuffers 提供了很多和ChannelBuffer相关的有用的方法,请参考API手册。
此外,静态导入ChannelBuffers是一种好的做法:
import static org.jboss.netty.buffer.ChannelBuffers.*;
...
ChannelBuffer dynamicBuf = dynamicBuffer(256);
ChannelBuffer ordinaryBuf = buffer(1024);
一般而言,我们编写结构化的消息。
但等一下,flip呢?我们过去在NIO中发送一个消息之前,不是调用ByteBuffer.flip()吗?因为ChannelBuffer有两个指针,所以没有这个方法。 一个指针用于读操作,一个用于写操作。这个写操作的索引在你向ChannelBuffer 中写入内容时增加,同时读操作的索引不改变。这个读写的索引分别表示消息开始和结束的位置。
相反,不调用flip方法的话,NIO缓存不提供一个清晰的方式来搞清一个消息内容的起始位置。如果你忘记调用flip的话,你会遇到麻烦:错误的数据发出,或者什么也不发出。因为不同的操作类型有不同的指针,所以这种情况对Netty而言是不会出现的。你会发现你在这个不需要flip的环境中,非常的适应、非常舒服。
另一个需要明确的是这个write 方法返回一个代表尚未发生的后续I/O操作的 ChannelFuture。 这意味着,因为Netty中的所有操作都是异步的,调用的任何操作都可能尚未真的执行。比如下面的代码甚至在一个消息尚未发出前关闭连接:
Channel ch = ...;
ch.write(message);
ch.close();
因此,你需要在write方法返回的ChannelFuture提醒你写操作完成之后,调用close 方法。请注意, close方法同样也可能不是立即关闭,它也返回 ChannelFuture。
那么当write调用完成的时候,我们怎么获得提醒?简单的给返回的 ChannelFuture增加一个ChannelFutureListener 即可。这里我们构建一个新的匿名的ChannelFutureListener以实现在调用完成后关闭 Channel。
另外一种做法,你可以使用一个预定义好的Listener来简化代码:
f.addListener(ChannelFutureListener.CLOSE);
1.6. 编写Time客户端
和DISCARD、ECHO服务不同,因为人类不能转译32位整数为日历时间,所以我们需要一个TIME协议的客户端。本节中我们讨论如何确保服务器正确工作,并学习如何用Netty写一个客户端。
在Netty中,服务器端和客户端最大的、唯一的不同是需要不同的Bootstrap和ChannelFactory。请看一下下面代码:
package org.jboss.netty.example.time;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class TimeClient {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
ChannelFactory factory =
new NioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ClientBootstrap bootstrap = new ClientBootstrap(factory);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
return Channels.pipeline(new TimeClientHandler());
}
});
bootstrap.setOption("tcpNoDelay", true);
bootstrap.setOption("keepAlive", true);
bootstrap.connect(new InetSocketAddress(host, port));
}
}
创建客户端的Channel使用的是NioClientSocketChannelFactory,而不是 NioServerSocketChannelFactory。
ClientBootstrap 在客户端,对应服务器端的 ServerBootstrap。
请注意到,这次没有"child."前缀。客户端的SocketChannel没有上一级根。
应该调用connect方法而不是bind 方法。
我们能看出,这跟服务器端的启动过程没有特别的区别。那么,ChannelHandler实现呢?它应该接收32位整数,并将其解释为人可读的格式,打印解释出来的时间,然后关闭连接:
package org.jboss.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer buf = (ChannelBuffer) e.getMessage();
long currentTimeMillis = buf.readInt() * 1000L;
System.out.println(new Date(currentTimeMillis));
e.getChannel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
看上去真的很简单,并且和服务器端代码没什么区别。不过,这个handler时常会不工作,而是抛出IndexOutOfBoundsException。我们在下一节讨论为什么会是这样。
1.7. 处理基于流的传输
1.7.1. 套接字缓存的一个警示
在诸如TCP/IP这样的基于流的传输机制下,收到的数据存储在套接字的接收缓存中。不幸的是,流传输的缓存并不是“包”的队列,而是“字节”的队列。这意味着,就算你以两个独立的包的形式发送两条消息,操作系统不会按两条消息来处理他们,而是一系列的字节。因此,没有任何机制能保证你读到的就是对端写入的。比如,我们假定操作系统的TCP/IP栈收到了下面三个包:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
基于流的协议的通用特性导致你的应用程序按下面的片段来读到数据的几率非常之高:
+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+
因此,一个接收方(无论是服务器端还是客户端)应该能够将收到的数据构造成一个或多个对你的应用程序逻辑而言有意义的、易于理解的“帧”。就上面的例子而言,接收到的数据应该被重组成为下面的情况:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
1.7.2. 第一个解决办法
好,我们现在回到TIME例子。我们这里也有同样的问题。一个32位整数是一个很少量的数据,不常被拆分。但问题是,它是可以被拆分的,而且随着流量的增加,这种拆分的可能性会加大。
最简单的解决办法应该是建立一个内部的汇聚缓存,并且等待直至4个字节都收到进入了内部缓存中。下面是修改后的TimeClientHandler实现,它解决了这个问题:
package org.jboss.netty.example.time;
import static org.jboss.netty.buffer.ChannelBuffers.*;
import java.util.Date;
public class TimeClientHandler extends SimpleChannelHandler {
private final ChannelBuffer buf = dynamicBuffer();
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer m = (ChannelBuffer) e.getMessage();
buf.writeBytes(m);
if (buf.readableBytes() >= 4) {
long currentTimeMillis = buf.readInt() * 1000L;
System.out.println(new Date(currentTimeMillis));
e.getChannel().close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
这个动态的缓存是能够根据需要增加容量的 ChannelBuffer 。这在我们不知道消息的大小的时候,非常有用。
首先,所有收到的数据都必须汇聚到 buf中。
然后,这个handler必须检查buf中是否有足够的数据--在这里是4个字节。接下来按业务逻辑处理。否则,Netty继续在后续数据到达时调用messageReceived方法,直至最终收集到4个字节。
1.7.3. 第二个解决办法
尽管第一个解决办法确实解决了TIME客户端的问题,修改后的handler看上去已经不是那么简洁了。想象一下更加复杂的协议,协议涉及多个字段、可变长的字段。你的ChannelHandler实现会很快变得很难维护。
你可能也注意到,你可以增加多个 ChannelHandler 给 ChannelPipeline,由此,你可以拆分单一的ChannelHandler为多个模块化的handler来减少你的应用程序的复杂度。例如,你可以拆分 TimeClientHandler为两个handler:
TimeDecoder 处理字节重组的问题,和
最初那个简单的TimeClientHandler.
幸运的是,Netty提供了可扩展的类来帮助你写出第一个类:
package org.jboss.netty.example.time;
public class TimeDecoder extends FrameDecoder {
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
if (buffer.readableBytes() < 4) {
return null;
}
return buffer.readBytes(4);
}
}
FrameDecoder 是ChannelHandler 的一个实现,是的处理重组问题简单。
FrameDecoder 调用decode方法,收到新的数据时,内部有一个汇聚的缓存。
如果返回的是 null 以为这收到的数据还不够。 FrameDecoder 会在数据量足够的时候再次调用。
如果返回的不是null意味着decode 方法已经成功解码了一个消息。FrameDecoder 会丢弃位于汇聚缓存的已经读走的数据。记住,你不需要解码多个消息,因为FrameDecoder 会一直调用decoder直至返回 null。
现在我们已经有了另一个handler可以插入到ChannelPipeline,我们应该修改TimeClient的ChannelPipelineFactory 实现:
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
return Channels.pipeline(
new TimeDecoder(),
new TimeClientHandler());
}
});
如果你喜欢尝试新的东西,你应该乐意试一下 ReplayingDecoder 。它更大程度的简化了解码器。请参看API手册以获得更多信息。
package org.jboss.netty.example.time;
public class TimeDecoder extends ReplayingDecoder<VoidEnum> {
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel,
ChannelBuffer buffer, VoidEnum state) {
return buffer.readBytes(4);
}
}
此外,Netty提供了一些“开箱即用”的解码器,它们可以使你很容易地实现很多协议,帮助你避免做出一个难以维护的、单一的handler实现。请参看下面的包,以获得更多信息:
org.jboss.netty.example.factorial 针对的是二进制协议
org.jboss.netty.example.telnet 针对的是基于行的文本协议
1.8. 用POJO替代ChannelBuffer
到此为止,我们所有的例子都在使用ChannelBuffer作为基础的协议消息数据结构。本节中,我们使用POJO替代ChannelBuffer来改进TIME协议的客户端和服务器端的例子。
ChannelHandler中使用POJO的优点是很明显的。从handler中分离出那些从ChannelBuffer提取信息的代码,使得Handler变得更加可维护、可复用。在这个ITME客户端、服务器端的例子中,我们只读取32位整数,所以直接使用ChannelBuffer没什么大问题,但在实际开发协议中,你会发现这种分离是十分必要的。
首先,我们定义一个新的类型UnixTime。
package org.jboss.netty.example.time;
import java.util.Date;
public class UnixTime {
private final int value;
public UnixTime(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return new Date(value * 1000L).toString();
}
}
现在修改一下TimeDecoder,不再返回ChannelBuffer,而是返回UnixTime。
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
if (buffer.readableBytes() < 4) {
return null;
}
return new UnixTime(buffer.readInt());
}
FrameDecoder 和 ReplayingDecoder 允许你返回任何类型的对象。如果你只能返回ChannelBuffer的话,那么我们必须添加另外一个ChannelHandler来把ChannelBuffer 转换为UnixTime。
修改了解码的方法后,TimeClientHandler不再使用ChannelBuffer了:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
UnixTime m = (UnixTime) e.getMessage();
System.out.println(m);
e.getChannel().close();
}
是不是看上去更简单、更优雅了?同样的技术可以用于服务器端。这次我们先更新一下TimeServerHandler:
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
UnixTime time = new UnixTime(System.currentTimeMillis() / 1000);
ChannelFuture f = e.getChannel().write(time);
f.addListener(ChannelFutureListener.CLOSE);
}
现在唯一缺少的就是编码器了。这个编码器应该是一个把 ChannelBuffer转换为UnixTime的ChannelHandler实现。编码一条消息时,因为不需要处理数据包的拆解和重组,所以这比写一个解码器要简单得多。
package org.jboss.netty.example.time;
import static org.jboss.netty.buffer.ChannelBuffers.*;
public class TimeEncoder extends SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) {
UnixTime time = (UnixTime) e.getMessage();
ChannelBuffer buf = buffer(4);
buf.writeInt(time.getValue());
Channels.write(ctx, e.getFuture(), buf);
}
}
编码器重载了writeRequested方法来拦截一个写入的请求。请注意MessageEvent 参数和在messageReceived的是同一类型,但是却有不同的解读。一个ChannelEvent 可以是上行或下行事件,这取决于事件的传递方向。比如,MessageEvent 在messageReceived中是上行事件,但是在writeRequested中却是下行事件。 请参考API文档以获得关于上行事件和下行事件之间的更多区别。
一旦完成从POJO到ChannelBuffer的转换,你应该将这个新的缓存提交给ChannelPipeline中的前一个ChannelDownstreamHandler。Channels 提供了各种工具性的方法来生成和发送一个ChannelEvent。本例中,Channels.write(...)方法创建了一个新的MessageEvent 并发送给ChannelPipeline中的前一个 ChannelDownstreamHandler。
此外,建议使用静态导入的方式使用Channels:
import static org.jboss.netty.channel.Channels.*;
...
ChannelPipeline pipeline = pipeline();
write(ctx, e.getFuture(), buf);
fireChannelDisconnected(ctx);
最后的工作就是给服务器端的ChannelPipeline添加TimeEncoder方法。我们把这个留作练习吧。
1.9. 关闭应用程序
如果运行TimeClient,需要注意到,这个应用程序会什么也不做一直运行下去而不退出。从完整的栈跟踪信息看,可以看到一些I/O线程正在运行。为了关闭这些I/O线程,使得应用程序优雅地关闭退出,我们需要释放由ChannelFactory分配的资源。
关闭一个典型的网络应用的过程由下面三步组成:
关闭所有的服务器套接字。
关闭所有的非服务器套接字 (如,客户端套接字和受理的套接字)。
释放所有ChannelFactory的资源。
如果在TimeClient应用这三步, 关闭唯一的客户端连接,释放所有ChannelFactory持有的资源,TimeClient.main()就可以很完整地关闭了:
package org.jboss.netty.example.time;
public class TimeClient {
public static void main(String[] args) throws Exception {
...
ChannelFactory factory = ...;
ClientBootstrap bootstrap = ...;
...
ChannelFuture future = bootstrap.connect(...);
future.awaitUninterruptibly();
if (!future.isSuccess()) {
future.getCause().printStackTrace();
}
future.getChannel().getCloseFuture().awaitUninterruptibly();
factory.releaseExternalResources();
}
}
ClientBootstrap的connect方法返回了ChannelFuture,后者在连接成功或失败的时候能给出提醒。此外,它还有一个对试图建立的连接相关的Channel 的引用。
等待返回的ChannelFuture 以确定建立连接的尝试是否成功。
如果连接建立失败,打印出失败的原因。如果建立连接的尝试既没有成功,也不是取消了,那么这个 ChannelFuture 的getCause() 方法会返回具体的失败原因。
现在已经完成了建立连接的尝试。我们现在需要等待,等待 Channel的 closeFuture方法返回,直到这个连接被关闭。每个Channel 都有它自己的closeFuture,你可以在连接关闭的时候得到提醒,并执行一些特定的动作。
当连接的尝试失败时,Channel会被自动关闭,因此即便这个连接尝试是失败closeFuture仍然会被调用。
到这里,所有的连接都已经关闭了。遗留的唯一任务是释放ChannelFactory占用的资源。调用一下它的releaseExternalResources()就可以了。所有的资源,包括NIO的Selector和线程池将被自动关闭终止。
关闭客户端是非常容易的事儿,但关闭服务器端呢?这需要关闭所有建立的连接、释放端口占用。为此,需要一个记录所有活动连接的数据结构,这可不是琐碎的小事儿一桩。还好,有一个办法,那就是 ChannelGroup。
ChannelGroup代表打开的 Channel的集合,是Java collection API的特殊扩展。如果一个 Channel加入了ChannelGroup,然后这个 Channel 被关闭了,那么这个关闭的 Channel 会自动从它的ChannelGroup中移除。你可以对一个组执行一个操作,却作用于所有属于同一个组的 Channel。比如,你可以在停止服务器的时候,关闭一个ChannelGroup 内所有的Channel。
为了记录跟踪到所有打开的套接字,你需要修改TimeServerHandler来给ChannelGroup( TimeServer.allChannels)添加一个新的Channel:
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
TimeServer.allChannels.add(e.getChannel());
}
没错, ChannelGroup 是线程安全的。
这下所有的启用Channel都自动的维护起来了。关闭服务器变得和关闭客户端一样简单了:
package org.jboss.netty.example.time;
public class TimeServer {
static final ChannelGroup allChannels = new DefaultChannelGroup("time-server");
public static void main(String[] args) throws Exception {
...
ChannelFactory factory = ...;
ServerBootstrap bootstrap = ...;
...
Channel channel = bootstrap.bind(...);
allChannels.add(channel);
waitForShutdownCommand();
ChannelGroupFuture future = allChannels.close();
future.awaitUninterruptibly();
factory.releaseExternalResources();
}
}
DefaultChannelGroup 需要一个名字作为构造方法的参数。这个名字仅仅是用来区别于其他组。
这个ServerBootstrap 的bind 方法返回了一个服务器端的、绑定了指定本地地址的Channel。调用返回的Channel 的close()方法可以使得它和绑定的本地地址解除绑定。
无论是服务器的、客户端的还是接受的Channel,都可以添加到ChannelGroup 。因此你可以在关闭服务器的时候,一下子就关闭关闭绑定的Channel和接受的Channel。
waitForShutdownCommand() 方法等待关闭的指令,它是一个假定会发生的方法,等待从授权的客户端或JVM关闭钩子发过来的消息。
可以对位于同一个ChannelGroup的channel执行相同的操作。本例中,我们关闭所有的channel,包括绑定的服务器 Channel 会解除绑定,所有的受理的连接会被异步地关闭。 为了得到所有连接关闭的提醒,它返回了ChannelGroupFuture ,这和ChannelFuture的角色几乎相近。
1.10. 总结
本章中,我们通过一个关于如何基于Netty写一个网络应用的演示例子,快速浏览了Netty。后续的章节和修订后的本章会提及你可能有疑问的一些内容。要知道讨论区 一直都在期待你的问题、建议来帮助我们根据你的反馈而持续改进Netty。
Chapter 2. 架构概览
2.1. 丰富的Buffer数据结构
2.2. 统一的异步 I/O API
2.3. 基于拦截者链模式的事件模型
2.4. 为更快捷开发的高级组件
2.4.1. 编码框架
2.4.2. SSL / TLS 支持
2.4.3. HTTP 实现
2.4.4. Google Protocol Buffer集成
2.5. 总结
本章中,我们会看一下Netty中提供了哪些核心功能,它们是如何组成了一个完整的网络应用开发框架的。在阅读本章的过程中,请始终记得这张图。
2.1. 丰富的Buffer数据结构
Netty用它自己的独有的buffer API,而不是NIO的ByteBuffer来表示一系列的字节。这种做法比使用ByteBuffer具有很明显的优势。Netty的新buffer类型, ChannelBuffer 专门设计用来解决ByteBuffer的缺陷,并满足网络应用开发人员的日常开发需要。下面列出了一些酷的特性:
如果必要,你可以定义你的buffer类型。
通过内建的复合buffer类型,实现了透明的零拷贝。
提供了一个和StringBuffer类似的、容量可以根据需要扩展的、动态的buffer类型。
再也不需要调用 flip() 了。
比 ByteBuffer速度快。
更多信息,请参考 org.jboss.netty.buffer 包描述。
2.2. 统一的异步 I/O API
Java中传统的I/O API为不同的传输类型提供了不同的类型和方法。例如, java.net.Socket 和 java.net.DatagramSocket并不具有任何共同的父类型,因此它们在执行套接字I/O的方式完全不同。
这使得一个网络应用如果要从一种传输方式转换为另一种方式变得非常困难和琐碎。这种在传输上不能转换的特性,在你需要支持各种传输方式,却不能重写整个网络层的时候,就是一个问题了。逻辑上,很多协议是可以运行于多种传输方式的,如TCP/IP、UDP/IP、SCTP、和串口通信。
更加糟糕的是,Java New I/O (NIO) API引入了和原有的阻塞I/O(OIO) API之间的不兼容。并且这种不兼容还会出现在NIO.2 (AIO)。因为这些API之间在设计和性能特性上都不同,你必须在开始实现的时候就确定你的而应用程序基于哪套API。
例如,因为一些客户端程序是非常小的,并且用OIO写一个服务器会比用NIO简单很多,所以你可能想从OIO开始。然而,当你的业务按指数级成长,你的服务器开始同时为几万个客户端服务的时候,你的麻烦来了。你也可能想从NIO开始,但考虑到NIO Slector API的复杂度对于快速开发的影响,整个开发周期会长很多。
Netty有一个统一的异步I/O接口,名为 Channel,它抽象出了点对点通信需要的所有操作。这意味着,一旦你基于一种Netty传输开发了应用,你的应用还可以运行于其他Netty传输方式上。Netty通过一致的API提供了很多重要的传输方式:
基于NIO的TCP/IP传输 (参看 org.jboss.netty.channel.socket.nio),
基于OIO的TCP/IP传输 (参看 org.jboss.netty.channel.socket.oio),
基于OIO的UDP/IP传输,和
本地传输 (参看 org.jboss.netty.channel.local).
从一种传输方式切换到另外一种,通常只需要几行代码的变更,如选择另一种 ChannelFactory 的实现。
同样,你可以利用一个目前尚未编写的传输方式的优势,比如串口通讯的传输方式。再说一下,这只需要替换掉几行构造方法的代码。此外,核心API是高度可扩展的,你可以写你自己的传输方式。
2.3. 基于拦截者链模式的事件模型
定义恰当的、可扩展的事件模型对于事件驱动的应用程序而言,是必须的。Netty就具有这样一个针对I/O的事件模型。你还可以实现你自己的事件类型。因为事件类型的层级结构很严格,每个类型都和其他类型有着严格的界定。因此,你完全可以在不破坏现有代码的情况下实现你自己的事件。这也是区别于其他框架的另一个标志。很多NIO框架没有或提供有限的事件模型。当需要添加一个新的定制的事件类型时,必须调整现有的代码。
一个ChannelEvent由ChannelPipeline中的多个ChannelHandler来处理。这里的pipeline实现了一个Intercepting Filter模式的高级形式,是的用户可以完全控制事件是如何被处理的、handler之间如何相互操作。比如,你可以定义当一个数据从套接字中读过来的时候,如何做。
public class MyReadHandler implements SimpleChannelHandler {
public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the received message.
...
// And forward the event to the next handler.
ctx.sendUpstream(evt);
}
}
你还可以定义当其他handler请求写操作的时候,如何做:
public class MyWriteHandler implements SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the message to be written.
...
// And forward the event to the next handler.
ctx.sendDownstream(evt);
}
}
关于事件模型的更多信息,请参看API文档 ChannelEvent 和 ChannelPipeline。
2.4. 为快速开发的高级组件
前面提到的核心组件的上面,已经可以实现所有类型的网络应用了。Netty提供了一些提高开发速度的高级特性。
2.4.1. 编码框架
如在 Section 1.8, “ 用POJO替代ChannelBuffer ”,把协议的编码和业务逻辑分离总是好的。但实际真要从零做起来做到这点,是有难度的:必须处理消息的拆解;一些协议是多层的(如位于其他低层协议之上);一些是很难用一个状态机实现的。
因此,一个好的网络应用框架应该提供一个可扩展的、可复用的、可单元测试的、多层的编码框架,从而得到可维护的用户编码。
不论你在写的网络协议是简单还是复杂、二进制还是文本、任何形式的,Netty在核心之上提供了很多基本的和高级的编码器来解决你会遇到的各种问题。
2.4.2. SSL / TLS 支持
和过去的阻塞式I/O不同,在NIO中支持SSL可不是容易的事,不能简单地封装一个流来对数据进行加密和解密,而是必须使用javax.net.ssl.SSLEngine。 SSLEngine是一个和SSL一样复杂的状态机,必须管理所有可能的状态,包括密码组、加密密钥的协商(或再次协商)、证书交换和验证。不仅如此,SSLEngine 并非我们期望的那种严格的线程安全。
在Netty中,SslHandler负责管理SSLEngine所有讨厌的细节和缺陷,留给开发者的只是配置SslHandler 并将其添加到ChannelPipeline。开发者可以很容易的实现诸如 StartTLS 的高级特性。
2.4.3. HTTP实现
毫无疑问,HTTP是最流行的互联网协议。当下已经有很多HTTP协议的实现了,如Servlet容器。那么为什么Netty在核心的上层还有HTTP呢?
Netty的HTTP支持和现有的HTTP库是完全不同的。它允许你完全控制HTTP消息如何在底层进行交互。因为基本上它是HTTP编码器和HTTP消息类的组合,所以并不存在什么必须严格的限制,如强制的线程模型。也就是说,你可以写你自己的HTTP客户端或服务器端,让他们按你的需要来运行。你对线程模型、连接的生命周期、分段编码以及任何HTTP规范允许做的,有完全的控制。
得益于这种高度可定制的特点,你可以写一个高效率的HTTP服务器,如:
需要长连接的聊天服务器以及服务器推送技术。(如 Comet 和 WebSockets)
需要保持连接,直至整个媒体都传递完毕的流媒体服务器(如 2小时的电影)
允许超大文件上传,但不带来内存压力的文件服务器(如,每个请求上传1G字节)
连接了几万个第三方异步WEB服务的、可扩充的混搭客户端
2.4.4. Google Protocol Buffer集成
Google Protocol Buffers 对于快速开发高效的、随时间逐步完备的二进制协议而言,是一个理想的解决方案。利用 ProtobufEncoder和ProtobufDecoder,你可以把Google Buffers Compiler(protoc)生成的消息类转换为Netty编码。请看一下'LocalTime' 例子 ,展示了用 sample protocol definition构建一个高性能的二进制协议客户端和服务器是非常容易的。
2.5. 总结
本章中,我们从特点出发,涉及了Netty的整体框架。Netty还具有一个强大的框架,由三个组件组成:buffer、channel、事件模型。所有的更高级的特点,都是建立在这三种组件之上的。如果理解了这三个组件是如何配合工作的,理解本章后面部分简要提及的更高级的特点就不应该很困难了。
你可能就整个框架的细节、各个特性如何工作等还有疑惑。如果真是这样,请告诉我们以提高这个文档。
快速有效的网络应用开发
3.2.4.Final 译者:张立明 Larry Zhang
[size=large][color=red]下面的格式很差,建议下载附件并阅读。[/color][/size]
--------------------------------------------------------------------------------
前言
1. 问题提出
2. 解决方案
1. 开始
1.1. 写在开始之前
1.2. 编写一个Discard服务
1.3. 详解Received Data
1.4. 编写一个Echo服务
1.5. 编写一个Time服务
1.6. 编写一个Time客户端
1.7. 处理基于流的传输
1.7.1. 套接字缓存(Socket Buffer)的一个小警示
1.7.2. 第一个解决办法
1.7.3. 第二个解决办法
1.8. 用POJO取代ChannelBuffer
1.9. 关闭应用程序
1.10. 总结
2. 架构概览
2.1. 丰富的缓存数据结构
2.2. 统一的异步 I/O API
2.3. 基于拦截者链(Interceptor Chain)模式的事件模型
2.4. 为更快捷开发的高级组件
2.4.1. 编码框架
2.4.2. SSL / TLS 支持
2.4.3. HTTP实现
2.4.4. Google Protocol Buffer集成
2.5. 总结
前言
1. 问题提出
当前我们使用通用的应用或库来相互通信。比如,我们常常使用HTTP客户端库来从WEB服务器上获取信息,并通过Web Service来调用一个远程过程。
然而,一个通用的协议或者它的实现,有时候并不能很好的扩展。这一点类似于我们不适用通用的HTTP服务器来交换大的文件、电子邮件和诸如财务信息和多人游戏数据等近乎于实时的数据。这些东西需要根据其特定用途而进行高度优化的协议实现。例如,你可能需要一个专门针对基于AJAX的聊天应用、针对多媒体流、或者大的文件传输进行了优化的HTTP服务器。你甚至可能想设计并实现一个完全按照你的需求而定义的全新的协议。
另一种情况也是难以避免的。那就是,为了和一个既有的旧系统进行交互,你必须处理旧系统上使用的协议。这时,在不牺牲稳定性和性能的前提下,你能够在多长时间内实现那个协议就非常重要。
2. 解决方案
Netty项目 是一个提供异步的、事件驱动的网络应用框,是一套有助于快速开发出高性能、高扩展性的、高可维护性的协议的服务器或客户端的开发工具。
换而言之,Netty是一个基于NIO的C/S框架。这套框架可以快速、简单地开发出网络协议的客户端和服务器端应用。它可以大大简化、流程化TCP和UDP套接字的服务器开发过程。
“快速和简单”并不意味着开发出的应用会遇到可维护性、性能等问题。Netty是建立在从许多网络协议(如FTP、SMTP、HTTP和各种二进制和文本协议等)中借鉴的经验基础上精心设计出的。这使得Netty在开发的简单化、性能、稳定性、灵活性等方面都同时达到了设计目标。
一些用户可能已经发现了其他的一些网络应用框架。这些框架也宣称具有相同的优势。这时你可能会问:Neey有什么不同?答案是“道不同”。Netty设计的原则是:给你提供从API到实现以最舒适的体验。这一点是看不到摸不着的。但你在阅读这个文档、以及应用Netty过程中,你会体验到我们的这个设计原则使得一切变得轻松容易。
Chapter 1. 开始
1.1. 开始之前
1.2. 编写一个Discard服务
1.3. 详解Received Data
1.4. 编写一个Echo服务
1.5. 编写一个Time服务
1.6. 编写一个Time客户端
1.7. 处理基于流的传输
1.7.1. 套接字缓存(Socket Buffer)的一个小警示
1.7.2. 第一个解决办法
1.7.3. 第二个解决办法
1.8. 用POJO取代ChannelBuffer
1.9. 关闭你的应用程序
1.10. 总结
这一章围绕着Neey的核心构成讲述,并提供了简单的例子以便快速上手。读到本章末尾,你将可以写一个基于Netty的客户端和服务器。
如果你喜欢自顶向下的学习方式,你应该从 Chapter 2, 架构概览 开始,然后再回到这里。
1.1. 开始之前
运行本章中的例子最低的要求只有两个:最新版本的Netty和JDK1.5或更高版本。最新的Netty可以在此 下载。 要下载到正确的JDK版本,请参考你选择的JDK提供商的网站。
在读的过程中,你会对本章中涉及的类有更多的疑问。当你想了解更多的时候,请参考API文档。所有的类名,都非常方便地连接到了在线的API页面上。此外,记得联系我们 Netty社区 并告诉我们是否有些信息不正确,语法或者拼写等错误,或者你有一个提高这个文档的好办法。
1.2. 编写一个Discard服务
这个世界上最简单的协议不是“Hello,World!”,而是DISCARD。 这个协议丢弃所有的收到的数据,不给任何回应。
为了实现这个DISCARD协议,你唯一要做的事情就是忽略所有收到的数据。我们直接从处理器(handler)的实现开始。这个处理器处理Netty生成的I/O事件。
package org.jboss.netty.example.discard;
public class DiscardServerHandler extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
Channel ch = e.getChannel();
ch.close();
}
}
DiscardServerHandler继承SimpleChannelHandler, ChannelHandler的接口实现类。 SimpleChannelHandler 提供各种事件的处理方法,你可以重载它们。到目前为止,继承 SimpleChannelHandler,而不是你自己去实现一个handler接口,是足够的。
我们在这里重载了messageReceived 事件处理方法。这个方法调用时提供了MessageEvent,它包含着刚刚从客户端收到的新数据。在这个例子中,我们通过什么也不做,来忽略收到的数据,从而实现DISCARD协议。
当一个异常因为I/O错误由Netty抛出,或者由在处理事件过程中,handler的实现抛出了异常,exceptionCaught 方法会被调用,并提供了ExceptionEvent。 尽管在特定情况下,实现这个方法时,你需要对异常有不同的处理,但通常情况下,被捕获的异常应该被记录,并且对应的channel应该被关闭。例如,你可能想在关闭链接之前发送一个错误代码的回应信息。
到此为止,我们已经实现了DISCARD服务的一半。接下来需要写 main 方法来运行这个配备了 DiscardServerHandler的服务。
package org.jboss.netty.example.discard;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class DiscardServer {
public static void main(String[] args) throws Exception {
ChannelFactory factory =
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ServerBootstrap bootstrap = new ServerBootstrap(factory);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
return Channels.pipeline(new DiscardServerHandler());
}
});
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
bootstrap.bind(new InetSocketAddress(8080));
}
}
ChannelFactory 是创建并管理 Channel和它们相关资源的工厂。它处理所有的I/O 请求,执行I/O来生成ChannelEvent。Netty提供多种ChannelFactory实现。我们现在正在实现一个服务器端的例子,因此我们使用NioServerSocketChannelFactory。另一个需要知道的是,它并不是自行创建I/O线程。它试图从你在构造方法中指定的线程池中获得线程。对于线程是如何在你的应用运行环境中去管理的,它给了更多的控制,比如一个具有安全管理机制的应用服务器。
ServerBootstrap是一个建立服务器的工具类。你当然可以直接使用Channel来构建一个服务器,但你要清楚这将是一个繁琐的过程,而其实你根本没必要这么做。
这里,我们配置了ChannelPipelineFactory。当一个新的连接接入到服务器,一个新的ChannelPipeline将由指定的ChannelPipelineFactory来创建。这个新的Pipeline包含着DiscardServerHandler。随着这个应用逐步完善,最终实际你就是添加更多的handler到Pipeline,并抽象出这个匿名类成为一个顶级类。
你还可以设置针对Channel实现的特定参数。我们正在编写的是TCP/IP服务,所以我们可以设置套接字的选项参数,如tcpNoDelay 和 keepAlive。请注意到这个"child."前缀出现在所有参数前,它意味着这个选项参数应用于接入的Channel,而不是ServerSocketChannel的参数。你可以按下面做法来为ServerSocketChannel设定参数。
bootstrap.setOption("reuseAddress", true);
快要可以运行了。接下来要做的是绑定端口并启动服务。这里我们绑定所有本机网卡的端口8080。你可以用不同的绑定地址来多次调用bind方法。
哈哈!我们在Netty上构建了第一个服务器应用。
1.3. 详解Received Data
刚才我们已经写了我们第一个服务器。现在需要的是测试一下它的运行情况。最简单的测试方法,莫过于使用telnet命令了。例如,你可以在命令行上输入"telnet localhost 8080",然后随便输入些什么。
问题是,我们能说这个服务器运行正常吗?很难说的,因为它是一个“丢弃”服务,根本没有任何回应。为了证实它却是运转正常,我们改一下这个服务,让它打印出收到的数据。
我们已经知道了,当收到了数据时,会生成MessageEvent,还会调用messageReceived 处理器方法。我们可以在DiscardServerHandler的messageReceived中加入代码:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer buf = (ChannelBuffer) e.getMessage();
while(buf.readable()) {
System.out.println((char) buf.readByte());
System.out.flush();
}
}
在套接字中传递的消息永远都是ChannelBuffer。ChannelBuffer是一个核心的数据结构,它存储着Netty中的字节序列。它很像NIO中的ByteBuffer,不过更加简单和灵活了。例如,Netty允许你构建一个由多个ChannelBuffer复合而成的ChannelBuffer,以减少不必要的内存复制。
尽管它在很多方面都和NIO的ByteBuffer相像,仍然建议参考一下API手册。学习如何正确使用ChannelBuffer是使用轻松驾驭Netty的重要一步。
如果你再次运行 telnet 命令,你会看到服务器打印出它收到的内容。
“丢弃”服务器的全部源代码位于 org.jboss.netty.example.discard 包。
1.4. 编写一个Echo服务
截至目前,我们已经实现了数据的获取,但没有回应。实际上,一个服务器应该对请求给予回应的。我们看一下如何通过实现ECHO协议给客户端一个回应,把收到的数据送回去。
这和我们在上一节中实现的“丢弃”服务之间唯一不同的是它把收到的数据又发送给回客户端,而不是在服务器端打印出来。为此,修改一下messageReceived方法就可以了:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
Channel ch = e.getChannel();
ch.write(e.getMessage());
}
ChannelEvent对象有一个相应的Channel的引用。这里返回的Channel 代表收到消息事件MessageEvent的那个连接。 我们可以得到这个Channel调用它的 write 方法来写数据给对等的远端。
如果你再次运行telnet,你会看到服务器把你发给它的都送了回来。
Echo服务器的全部代码位于 org.jboss.netty.example.echo包。
1.5. 编写一个Time服务
这小节我们要实现的协议是 TIME。和上个例子不同,它发送一个包含32位证书的消息,发送完毕后不需要收到任何回应就关闭连接。在这个例子中,你将学习如何构建并发送一个消息,然后关闭连接。
连接建立后,收到的任何数据都被忽略不计,而仅仅是发送一个消息。因此,我们这次不能使用 messageReceived 方法,而是需要重载 channelConnected 方法。代码如下:
package org.jboss.netty.example.time;
public class TimeServerHandler extends SimpleChannelHandler {
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
Channel ch = e.getChannel();
ChannelBuffer time = ChannelBuffers.buffer(4);
time.writeInt(System.currentTimeMillis() / 1000);
ChannelFuture f = ch.write(time);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
Channel ch = future.getChannel();
ch.close();
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
我们前面解释过, channelConnected方法在连接建立的时候被调用。我们在这里发送32位整数来表示当前时间(单位:秒)。
为了发送新消息,需要分配一个用来包含消息的缓存。我们需要发送的是32位整数,所以我们需要一个4字节容量的ChannelBuffer。这个ChannelBuffers工具类用来分配新的缓存。除了这个buffer 方法, ChannelBuffers 提供了很多和ChannelBuffer相关的有用的方法,请参考API手册。
此外,静态导入ChannelBuffers是一种好的做法:
import static org.jboss.netty.buffer.ChannelBuffers.*;
...
ChannelBuffer dynamicBuf = dynamicBuffer(256);
ChannelBuffer ordinaryBuf = buffer(1024);
一般而言,我们编写结构化的消息。
但等一下,flip呢?我们过去在NIO中发送一个消息之前,不是调用ByteBuffer.flip()吗?因为ChannelBuffer有两个指针,所以没有这个方法。 一个指针用于读操作,一个用于写操作。这个写操作的索引在你向ChannelBuffer 中写入内容时增加,同时读操作的索引不改变。这个读写的索引分别表示消息开始和结束的位置。
相反,不调用flip方法的话,NIO缓存不提供一个清晰的方式来搞清一个消息内容的起始位置。如果你忘记调用flip的话,你会遇到麻烦:错误的数据发出,或者什么也不发出。因为不同的操作类型有不同的指针,所以这种情况对Netty而言是不会出现的。你会发现你在这个不需要flip的环境中,非常的适应、非常舒服。
另一个需要明确的是这个write 方法返回一个代表尚未发生的后续I/O操作的 ChannelFuture。 这意味着,因为Netty中的所有操作都是异步的,调用的任何操作都可能尚未真的执行。比如下面的代码甚至在一个消息尚未发出前关闭连接:
Channel ch = ...;
ch.write(message);
ch.close();
因此,你需要在write方法返回的ChannelFuture提醒你写操作完成之后,调用close 方法。请注意, close方法同样也可能不是立即关闭,它也返回 ChannelFuture。
那么当write调用完成的时候,我们怎么获得提醒?简单的给返回的 ChannelFuture增加一个ChannelFutureListener 即可。这里我们构建一个新的匿名的ChannelFutureListener以实现在调用完成后关闭 Channel。
另外一种做法,你可以使用一个预定义好的Listener来简化代码:
f.addListener(ChannelFutureListener.CLOSE);
1.6. 编写Time客户端
和DISCARD、ECHO服务不同,因为人类不能转译32位整数为日历时间,所以我们需要一个TIME协议的客户端。本节中我们讨论如何确保服务器正确工作,并学习如何用Netty写一个客户端。
在Netty中,服务器端和客户端最大的、唯一的不同是需要不同的Bootstrap和ChannelFactory。请看一下下面代码:
package org.jboss.netty.example.time;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class TimeClient {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
ChannelFactory factory =
new NioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ClientBootstrap bootstrap = new ClientBootstrap(factory);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
return Channels.pipeline(new TimeClientHandler());
}
});
bootstrap.setOption("tcpNoDelay", true);
bootstrap.setOption("keepAlive", true);
bootstrap.connect(new InetSocketAddress(host, port));
}
}
创建客户端的Channel使用的是NioClientSocketChannelFactory,而不是 NioServerSocketChannelFactory。
ClientBootstrap 在客户端,对应服务器端的 ServerBootstrap。
请注意到,这次没有"child."前缀。客户端的SocketChannel没有上一级根。
应该调用connect方法而不是bind 方法。
我们能看出,这跟服务器端的启动过程没有特别的区别。那么,ChannelHandler实现呢?它应该接收32位整数,并将其解释为人可读的格式,打印解释出来的时间,然后关闭连接:
package org.jboss.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer buf = (ChannelBuffer) e.getMessage();
long currentTimeMillis = buf.readInt() * 1000L;
System.out.println(new Date(currentTimeMillis));
e.getChannel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
看上去真的很简单,并且和服务器端代码没什么区别。不过,这个handler时常会不工作,而是抛出IndexOutOfBoundsException。我们在下一节讨论为什么会是这样。
1.7. 处理基于流的传输
1.7.1. 套接字缓存的一个警示
在诸如TCP/IP这样的基于流的传输机制下,收到的数据存储在套接字的接收缓存中。不幸的是,流传输的缓存并不是“包”的队列,而是“字节”的队列。这意味着,就算你以两个独立的包的形式发送两条消息,操作系统不会按两条消息来处理他们,而是一系列的字节。因此,没有任何机制能保证你读到的就是对端写入的。比如,我们假定操作系统的TCP/IP栈收到了下面三个包:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
基于流的协议的通用特性导致你的应用程序按下面的片段来读到数据的几率非常之高:
+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+
因此,一个接收方(无论是服务器端还是客户端)应该能够将收到的数据构造成一个或多个对你的应用程序逻辑而言有意义的、易于理解的“帧”。就上面的例子而言,接收到的数据应该被重组成为下面的情况:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
1.7.2. 第一个解决办法
好,我们现在回到TIME例子。我们这里也有同样的问题。一个32位整数是一个很少量的数据,不常被拆分。但问题是,它是可以被拆分的,而且随着流量的增加,这种拆分的可能性会加大。
最简单的解决办法应该是建立一个内部的汇聚缓存,并且等待直至4个字节都收到进入了内部缓存中。下面是修改后的TimeClientHandler实现,它解决了这个问题:
package org.jboss.netty.example.time;
import static org.jboss.netty.buffer.ChannelBuffers.*;
import java.util.Date;
public class TimeClientHandler extends SimpleChannelHandler {
private final ChannelBuffer buf = dynamicBuffer();
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer m = (ChannelBuffer) e.getMessage();
buf.writeBytes(m);
if (buf.readableBytes() >= 4) {
long currentTimeMillis = buf.readInt() * 1000L;
System.out.println(new Date(currentTimeMillis));
e.getChannel().close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
这个动态的缓存是能够根据需要增加容量的 ChannelBuffer 。这在我们不知道消息的大小的时候,非常有用。
首先,所有收到的数据都必须汇聚到 buf中。
然后,这个handler必须检查buf中是否有足够的数据--在这里是4个字节。接下来按业务逻辑处理。否则,Netty继续在后续数据到达时调用messageReceived方法,直至最终收集到4个字节。
1.7.3. 第二个解决办法
尽管第一个解决办法确实解决了TIME客户端的问题,修改后的handler看上去已经不是那么简洁了。想象一下更加复杂的协议,协议涉及多个字段、可变长的字段。你的ChannelHandler实现会很快变得很难维护。
你可能也注意到,你可以增加多个 ChannelHandler 给 ChannelPipeline,由此,你可以拆分单一的ChannelHandler为多个模块化的handler来减少你的应用程序的复杂度。例如,你可以拆分 TimeClientHandler为两个handler:
TimeDecoder 处理字节重组的问题,和
最初那个简单的TimeClientHandler.
幸运的是,Netty提供了可扩展的类来帮助你写出第一个类:
package org.jboss.netty.example.time;
public class TimeDecoder extends FrameDecoder {
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
if (buffer.readableBytes() < 4) {
return null;
}
return buffer.readBytes(4);
}
}
FrameDecoder 是ChannelHandler 的一个实现,是的处理重组问题简单。
FrameDecoder 调用decode方法,收到新的数据时,内部有一个汇聚的缓存。
如果返回的是 null 以为这收到的数据还不够。 FrameDecoder 会在数据量足够的时候再次调用。
如果返回的不是null意味着decode 方法已经成功解码了一个消息。FrameDecoder 会丢弃位于汇聚缓存的已经读走的数据。记住,你不需要解码多个消息,因为FrameDecoder 会一直调用decoder直至返回 null。
现在我们已经有了另一个handler可以插入到ChannelPipeline,我们应该修改TimeClient的ChannelPipelineFactory 实现:
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
public ChannelPipeline getPipeline() {
return Channels.pipeline(
new TimeDecoder(),
new TimeClientHandler());
}
});
如果你喜欢尝试新的东西,你应该乐意试一下 ReplayingDecoder 。它更大程度的简化了解码器。请参看API手册以获得更多信息。
package org.jboss.netty.example.time;
public class TimeDecoder extends ReplayingDecoder<VoidEnum> {
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel,
ChannelBuffer buffer, VoidEnum state) {
return buffer.readBytes(4);
}
}
此外,Netty提供了一些“开箱即用”的解码器,它们可以使你很容易地实现很多协议,帮助你避免做出一个难以维护的、单一的handler实现。请参看下面的包,以获得更多信息:
org.jboss.netty.example.factorial 针对的是二进制协议
org.jboss.netty.example.telnet 针对的是基于行的文本协议
1.8. 用POJO替代ChannelBuffer
到此为止,我们所有的例子都在使用ChannelBuffer作为基础的协议消息数据结构。本节中,我们使用POJO替代ChannelBuffer来改进TIME协议的客户端和服务器端的例子。
ChannelHandler中使用POJO的优点是很明显的。从handler中分离出那些从ChannelBuffer提取信息的代码,使得Handler变得更加可维护、可复用。在这个ITME客户端、服务器端的例子中,我们只读取32位整数,所以直接使用ChannelBuffer没什么大问题,但在实际开发协议中,你会发现这种分离是十分必要的。
首先,我们定义一个新的类型UnixTime。
package org.jboss.netty.example.time;
import java.util.Date;
public class UnixTime {
private final int value;
public UnixTime(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return new Date(value * 1000L).toString();
}
}
现在修改一下TimeDecoder,不再返回ChannelBuffer,而是返回UnixTime。
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
if (buffer.readableBytes() < 4) {
return null;
}
return new UnixTime(buffer.readInt());
}
FrameDecoder 和 ReplayingDecoder 允许你返回任何类型的对象。如果你只能返回ChannelBuffer的话,那么我们必须添加另外一个ChannelHandler来把ChannelBuffer 转换为UnixTime。
修改了解码的方法后,TimeClientHandler不再使用ChannelBuffer了:
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
UnixTime m = (UnixTime) e.getMessage();
System.out.println(m);
e.getChannel().close();
}
是不是看上去更简单、更优雅了?同样的技术可以用于服务器端。这次我们先更新一下TimeServerHandler:
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
UnixTime time = new UnixTime(System.currentTimeMillis() / 1000);
ChannelFuture f = e.getChannel().write(time);
f.addListener(ChannelFutureListener.CLOSE);
}
现在唯一缺少的就是编码器了。这个编码器应该是一个把 ChannelBuffer转换为UnixTime的ChannelHandler实现。编码一条消息时,因为不需要处理数据包的拆解和重组,所以这比写一个解码器要简单得多。
package org.jboss.netty.example.time;
import static org.jboss.netty.buffer.ChannelBuffers.*;
public class TimeEncoder extends SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) {
UnixTime time = (UnixTime) e.getMessage();
ChannelBuffer buf = buffer(4);
buf.writeInt(time.getValue());
Channels.write(ctx, e.getFuture(), buf);
}
}
编码器重载了writeRequested方法来拦截一个写入的请求。请注意MessageEvent 参数和在messageReceived的是同一类型,但是却有不同的解读。一个ChannelEvent 可以是上行或下行事件,这取决于事件的传递方向。比如,MessageEvent 在messageReceived中是上行事件,但是在writeRequested中却是下行事件。 请参考API文档以获得关于上行事件和下行事件之间的更多区别。
一旦完成从POJO到ChannelBuffer的转换,你应该将这个新的缓存提交给ChannelPipeline中的前一个ChannelDownstreamHandler。Channels 提供了各种工具性的方法来生成和发送一个ChannelEvent。本例中,Channels.write(...)方法创建了一个新的MessageEvent 并发送给ChannelPipeline中的前一个 ChannelDownstreamHandler。
此外,建议使用静态导入的方式使用Channels:
import static org.jboss.netty.channel.Channels.*;
...
ChannelPipeline pipeline = pipeline();
write(ctx, e.getFuture(), buf);
fireChannelDisconnected(ctx);
最后的工作就是给服务器端的ChannelPipeline添加TimeEncoder方法。我们把这个留作练习吧。
1.9. 关闭应用程序
如果运行TimeClient,需要注意到,这个应用程序会什么也不做一直运行下去而不退出。从完整的栈跟踪信息看,可以看到一些I/O线程正在运行。为了关闭这些I/O线程,使得应用程序优雅地关闭退出,我们需要释放由ChannelFactory分配的资源。
关闭一个典型的网络应用的过程由下面三步组成:
关闭所有的服务器套接字。
关闭所有的非服务器套接字 (如,客户端套接字和受理的套接字)。
释放所有ChannelFactory的资源。
如果在TimeClient应用这三步, 关闭唯一的客户端连接,释放所有ChannelFactory持有的资源,TimeClient.main()就可以很完整地关闭了:
package org.jboss.netty.example.time;
public class TimeClient {
public static void main(String[] args) throws Exception {
...
ChannelFactory factory = ...;
ClientBootstrap bootstrap = ...;
...
ChannelFuture future = bootstrap.connect(...);
future.awaitUninterruptibly();
if (!future.isSuccess()) {
future.getCause().printStackTrace();
}
future.getChannel().getCloseFuture().awaitUninterruptibly();
factory.releaseExternalResources();
}
}
ClientBootstrap的connect方法返回了ChannelFuture,后者在连接成功或失败的时候能给出提醒。此外,它还有一个对试图建立的连接相关的Channel 的引用。
等待返回的ChannelFuture 以确定建立连接的尝试是否成功。
如果连接建立失败,打印出失败的原因。如果建立连接的尝试既没有成功,也不是取消了,那么这个 ChannelFuture 的getCause() 方法会返回具体的失败原因。
现在已经完成了建立连接的尝试。我们现在需要等待,等待 Channel的 closeFuture方法返回,直到这个连接被关闭。每个Channel 都有它自己的closeFuture,你可以在连接关闭的时候得到提醒,并执行一些特定的动作。
当连接的尝试失败时,Channel会被自动关闭,因此即便这个连接尝试是失败closeFuture仍然会被调用。
到这里,所有的连接都已经关闭了。遗留的唯一任务是释放ChannelFactory占用的资源。调用一下它的releaseExternalResources()就可以了。所有的资源,包括NIO的Selector和线程池将被自动关闭终止。
关闭客户端是非常容易的事儿,但关闭服务器端呢?这需要关闭所有建立的连接、释放端口占用。为此,需要一个记录所有活动连接的数据结构,这可不是琐碎的小事儿一桩。还好,有一个办法,那就是 ChannelGroup。
ChannelGroup代表打开的 Channel的集合,是Java collection API的特殊扩展。如果一个 Channel加入了ChannelGroup,然后这个 Channel 被关闭了,那么这个关闭的 Channel 会自动从它的ChannelGroup中移除。你可以对一个组执行一个操作,却作用于所有属于同一个组的 Channel。比如,你可以在停止服务器的时候,关闭一个ChannelGroup 内所有的Channel。
为了记录跟踪到所有打开的套接字,你需要修改TimeServerHandler来给ChannelGroup( TimeServer.allChannels)添加一个新的Channel:
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
TimeServer.allChannels.add(e.getChannel());
}
没错, ChannelGroup 是线程安全的。
这下所有的启用Channel都自动的维护起来了。关闭服务器变得和关闭客户端一样简单了:
package org.jboss.netty.example.time;
public class TimeServer {
static final ChannelGroup allChannels = new DefaultChannelGroup("time-server");
public static void main(String[] args) throws Exception {
...
ChannelFactory factory = ...;
ServerBootstrap bootstrap = ...;
...
Channel channel = bootstrap.bind(...);
allChannels.add(channel);
waitForShutdownCommand();
ChannelGroupFuture future = allChannels.close();
future.awaitUninterruptibly();
factory.releaseExternalResources();
}
}
DefaultChannelGroup 需要一个名字作为构造方法的参数。这个名字仅仅是用来区别于其他组。
这个ServerBootstrap 的bind 方法返回了一个服务器端的、绑定了指定本地地址的Channel。调用返回的Channel 的close()方法可以使得它和绑定的本地地址解除绑定。
无论是服务器的、客户端的还是接受的Channel,都可以添加到ChannelGroup 。因此你可以在关闭服务器的时候,一下子就关闭关闭绑定的Channel和接受的Channel。
waitForShutdownCommand() 方法等待关闭的指令,它是一个假定会发生的方法,等待从授权的客户端或JVM关闭钩子发过来的消息。
可以对位于同一个ChannelGroup的channel执行相同的操作。本例中,我们关闭所有的channel,包括绑定的服务器 Channel 会解除绑定,所有的受理的连接会被异步地关闭。 为了得到所有连接关闭的提醒,它返回了ChannelGroupFuture ,这和ChannelFuture的角色几乎相近。
1.10. 总结
本章中,我们通过一个关于如何基于Netty写一个网络应用的演示例子,快速浏览了Netty。后续的章节和修订后的本章会提及你可能有疑问的一些内容。要知道讨论区 一直都在期待你的问题、建议来帮助我们根据你的反馈而持续改进Netty。
Chapter 2. 架构概览
2.1. 丰富的Buffer数据结构
2.2. 统一的异步 I/O API
2.3. 基于拦截者链模式的事件模型
2.4. 为更快捷开发的高级组件
2.4.1. 编码框架
2.4.2. SSL / TLS 支持
2.4.3. HTTP 实现
2.4.4. Google Protocol Buffer集成
2.5. 总结
本章中,我们会看一下Netty中提供了哪些核心功能,它们是如何组成了一个完整的网络应用开发框架的。在阅读本章的过程中,请始终记得这张图。
2.1. 丰富的Buffer数据结构
Netty用它自己的独有的buffer API,而不是NIO的ByteBuffer来表示一系列的字节。这种做法比使用ByteBuffer具有很明显的优势。Netty的新buffer类型, ChannelBuffer 专门设计用来解决ByteBuffer的缺陷,并满足网络应用开发人员的日常开发需要。下面列出了一些酷的特性:
如果必要,你可以定义你的buffer类型。
通过内建的复合buffer类型,实现了透明的零拷贝。
提供了一个和StringBuffer类似的、容量可以根据需要扩展的、动态的buffer类型。
再也不需要调用 flip() 了。
比 ByteBuffer速度快。
更多信息,请参考 org.jboss.netty.buffer 包描述。
2.2. 统一的异步 I/O API
Java中传统的I/O API为不同的传输类型提供了不同的类型和方法。例如, java.net.Socket 和 java.net.DatagramSocket并不具有任何共同的父类型,因此它们在执行套接字I/O的方式完全不同。
这使得一个网络应用如果要从一种传输方式转换为另一种方式变得非常困难和琐碎。这种在传输上不能转换的特性,在你需要支持各种传输方式,却不能重写整个网络层的时候,就是一个问题了。逻辑上,很多协议是可以运行于多种传输方式的,如TCP/IP、UDP/IP、SCTP、和串口通信。
更加糟糕的是,Java New I/O (NIO) API引入了和原有的阻塞I/O(OIO) API之间的不兼容。并且这种不兼容还会出现在NIO.2 (AIO)。因为这些API之间在设计和性能特性上都不同,你必须在开始实现的时候就确定你的而应用程序基于哪套API。
例如,因为一些客户端程序是非常小的,并且用OIO写一个服务器会比用NIO简单很多,所以你可能想从OIO开始。然而,当你的业务按指数级成长,你的服务器开始同时为几万个客户端服务的时候,你的麻烦来了。你也可能想从NIO开始,但考虑到NIO Slector API的复杂度对于快速开发的影响,整个开发周期会长很多。
Netty有一个统一的异步I/O接口,名为 Channel,它抽象出了点对点通信需要的所有操作。这意味着,一旦你基于一种Netty传输开发了应用,你的应用还可以运行于其他Netty传输方式上。Netty通过一致的API提供了很多重要的传输方式:
基于NIO的TCP/IP传输 (参看 org.jboss.netty.channel.socket.nio),
基于OIO的TCP/IP传输 (参看 org.jboss.netty.channel.socket.oio),
基于OIO的UDP/IP传输,和
本地传输 (参看 org.jboss.netty.channel.local).
从一种传输方式切换到另外一种,通常只需要几行代码的变更,如选择另一种 ChannelFactory 的实现。
同样,你可以利用一个目前尚未编写的传输方式的优势,比如串口通讯的传输方式。再说一下,这只需要替换掉几行构造方法的代码。此外,核心API是高度可扩展的,你可以写你自己的传输方式。
2.3. 基于拦截者链模式的事件模型
定义恰当的、可扩展的事件模型对于事件驱动的应用程序而言,是必须的。Netty就具有这样一个针对I/O的事件模型。你还可以实现你自己的事件类型。因为事件类型的层级结构很严格,每个类型都和其他类型有着严格的界定。因此,你完全可以在不破坏现有代码的情况下实现你自己的事件。这也是区别于其他框架的另一个标志。很多NIO框架没有或提供有限的事件模型。当需要添加一个新的定制的事件类型时,必须调整现有的代码。
一个ChannelEvent由ChannelPipeline中的多个ChannelHandler来处理。这里的pipeline实现了一个Intercepting Filter模式的高级形式,是的用户可以完全控制事件是如何被处理的、handler之间如何相互操作。比如,你可以定义当一个数据从套接字中读过来的时候,如何做。
public class MyReadHandler implements SimpleChannelHandler {
public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the received message.
...
// And forward the event to the next handler.
ctx.sendUpstream(evt);
}
}
你还可以定义当其他handler请求写操作的时候,如何做:
public class MyWriteHandler implements SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the message to be written.
...
// And forward the event to the next handler.
ctx.sendDownstream(evt);
}
}
关于事件模型的更多信息,请参看API文档 ChannelEvent 和 ChannelPipeline。
2.4. 为快速开发的高级组件
前面提到的核心组件的上面,已经可以实现所有类型的网络应用了。Netty提供了一些提高开发速度的高级特性。
2.4.1. 编码框架
如在 Section 1.8, “ 用POJO替代ChannelBuffer ”,把协议的编码和业务逻辑分离总是好的。但实际真要从零做起来做到这点,是有难度的:必须处理消息的拆解;一些协议是多层的(如位于其他低层协议之上);一些是很难用一个状态机实现的。
因此,一个好的网络应用框架应该提供一个可扩展的、可复用的、可单元测试的、多层的编码框架,从而得到可维护的用户编码。
不论你在写的网络协议是简单还是复杂、二进制还是文本、任何形式的,Netty在核心之上提供了很多基本的和高级的编码器来解决你会遇到的各种问题。
2.4.2. SSL / TLS 支持
和过去的阻塞式I/O不同,在NIO中支持SSL可不是容易的事,不能简单地封装一个流来对数据进行加密和解密,而是必须使用javax.net.ssl.SSLEngine。 SSLEngine是一个和SSL一样复杂的状态机,必须管理所有可能的状态,包括密码组、加密密钥的协商(或再次协商)、证书交换和验证。不仅如此,SSLEngine 并非我们期望的那种严格的线程安全。
在Netty中,SslHandler负责管理SSLEngine所有讨厌的细节和缺陷,留给开发者的只是配置SslHandler 并将其添加到ChannelPipeline。开发者可以很容易的实现诸如 StartTLS 的高级特性。
2.4.3. HTTP实现
毫无疑问,HTTP是最流行的互联网协议。当下已经有很多HTTP协议的实现了,如Servlet容器。那么为什么Netty在核心的上层还有HTTP呢?
Netty的HTTP支持和现有的HTTP库是完全不同的。它允许你完全控制HTTP消息如何在底层进行交互。因为基本上它是HTTP编码器和HTTP消息类的组合,所以并不存在什么必须严格的限制,如强制的线程模型。也就是说,你可以写你自己的HTTP客户端或服务器端,让他们按你的需要来运行。你对线程模型、连接的生命周期、分段编码以及任何HTTP规范允许做的,有完全的控制。
得益于这种高度可定制的特点,你可以写一个高效率的HTTP服务器,如:
需要长连接的聊天服务器以及服务器推送技术。(如 Comet 和 WebSockets)
需要保持连接,直至整个媒体都传递完毕的流媒体服务器(如 2小时的电影)
允许超大文件上传,但不带来内存压力的文件服务器(如,每个请求上传1G字节)
连接了几万个第三方异步WEB服务的、可扩充的混搭客户端
2.4.4. Google Protocol Buffer集成
Google Protocol Buffers 对于快速开发高效的、随时间逐步完备的二进制协议而言,是一个理想的解决方案。利用 ProtobufEncoder和ProtobufDecoder,你可以把Google Buffers Compiler(protoc)生成的消息类转换为Netty编码。请看一下'LocalTime' 例子 ,展示了用 sample protocol definition构建一个高性能的二进制协议客户端和服务器是非常容易的。
2.5. 总结
本章中,我们从特点出发,涉及了Netty的整体框架。Netty还具有一个强大的框架,由三个组件组成:buffer、channel、事件模型。所有的更高级的特点,都是建立在这三种组件之上的。如果理解了这三个组件是如何配合工作的,理解本章后面部分简要提及的更高级的特点就不应该很困难了。
你可能就整个框架的细节、各个特性如何工作等还有疑惑。如果真是这样,请告诉我们以提高这个文档。