原网址:http://netty.io/wiki/user-guide-for-4.x.html
1. Preface
1.1 The Problem
现今我们使用通用应用程序或者类库互相交流。例如,我们经常使用HTTP客户端类库从web服务器抽取信息,或者通过web service调用RPC。然而,通用协议和它的实现有时并不能很好衡量。就好像,我们不会用一个通用HTTP服务器来交换大文件,e-mail信息和准实时信息(比如金融信息和多玩家游戏数据)。我们需要的是基于某种目的而经过高度优化的协议实现。例如,你可能想要一种经过优化的HTTP服务器,适用于基于AJAX的聊天应用,流媒体或者大文件传输。你甚至可能想要设计和实现一种精确满足自己需要的全新的协议。另一种不可避免的情况是,你必须处理一个遗留的私有协议,以确保和旧系统的互操作性。上述情况中的要点是,如何在不牺牲最终的应用程序的稳定性和性能的前提下,我们能多快实现那个协议。
2. The Solution
Netty项目致力于提供一个异步的、基于事件驱动的网络应用框架,同时提供可维护的、高性能的、可扩展的协议服务器和客户端开发工具。
换句话说,Netty是一个NIO客户端-服务器框架,能快速、简单地进行网络应用的开发,比如协议服务器/客户端。它极大地简化了网络编程,提高了网络编程的效率,比如TCP和UDP
socket服务器。
"快速易用"并不意味着所产生的应用会遇到维护性或性能问题。Netty借鉴了许多协议(比如FTP、SMTP、HTTP,以及许多二进制协议和基于文本的传统协议)的实现经验进行了精心设计,因此Netty找到了一种方式来易于开发,同时兼顾性能、稳定性和灵活性。
一些用户可能已经发现了其他的网络应用框架,它们也声称具有同样的优点,那你可能想问为什么Netty这么与众不同。答案就是它的构建哲学。一开始,Netty就设计能给你最舒服的体验,既包括API,也包括Netty的实现。
3. Getting Started
本章用简单的例子来展示Netty的核心架构,来让你能快速入门。在你通读本章之后,你马上就能用Netty来写一个客户端和一个服务器。
3.1 Before Getting Started
运行本章的例子有两个最低要求:Netty的最新版本和JDK 1.6+。
在你阅读的时候,你可能会对本章提到的类有很多问题,你可以参考API来了解它们。可以随时联系Netty项目组织。
3.2 Writing a Discard Server
世界上最简单的协议不是"Hello, World!",而是DISCARD。DISCARD协议丢弃所有接收到的数据,并且没有响应。
要实现DISCARD协议,你要做的就是忽略所有接收的数据。先从handler实现开始,handler实现用来处理Netty产生的事件。
package io.netty.example.discard;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Handles a server-side channel.
*/
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
// Discard the received data silently.
((ByteBuf) msg).release(); // (3)
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
inbound:入站
1) DiscardServerHandler继承自ChannelInboundHandlerAdapter,而ChannelInboundHandlerAdapter是ChannelInboundHandler的一个实现。ChannelInboundHandler提供了多种事件处理方法,这些方法都可以被覆盖。目前,继承ChannelInboundHandlerAdapter类已经足够了,不用再自己实现handler接口。
2)
在此我们覆盖 channelRead() 事件处理方法。当接收到来自客户端的数据时,该方法被调用。本例中,接收到的信息类型是ByteBuf。
3)
要实现DISCARD协议,handler必须忽略接收的消息。ByteBuf是一种引用计数的对象,必须调用 release() 方法显式释放。注意:handler有责任释放所有传递给该handler的引用计数的对象。通常,channelRead()
方法会像下面这样实现:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}
4) exceptionCaught()
事件处理方法会在以下情况下被调用:因I/O错误导致Netty抛出异常,在处理事件时handler实现抛出异常。大部分情况下,被捕获的异常应该记录下来,并且与之相关的channel应该在这儿关闭,虽然这个方法的实现可以根据你想怎么处理异常情况而不同。例如,你可能想在关闭连接前发送一个带有错误码的响应信息。
到目前为止,我们已经实现了DISCARD服务器的前半部分。剩下的就是写 main() 方法,用DiscardServerHandler来启动该服务器。
package io.netty.example.discard;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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 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。第一个NioEventLoopGroup,常常叫做"boss",接受一个
incoming 连接。第二个NioEventLoopGroup,常常叫做"worker",一旦 boss 接受连接并将接受的连接注册到 worker,worker 就可以处理已接受的连接的流量数据。使用多少个线程、线程如何映射到已创建的 Channel,这些都取决于EventLoopGroup实现,甚至可以通过构造器来配置。
2) ServerBootstrap是一个帮助类,用来设置服务器。你可以直接使用一个 Channel来设置服务器。然而,这是一个繁琐的过程,而且大部分情况下你不需要这么做。
3)
这里,我们指定使用 NioServerSocketChannel 类实例化一个新的Channel来接受 incoming 连接。
4)
这里指定的handler总是会被新接受的 Channel 用来求值。ChannelInitializer是一种特殊的handler,用来帮助用户配置一个新的Channel。最可能的是你想配置新的Channel的ChannelPipeline(通过添加一些handler比如DiscardServerHandler)来实现你的网络应用。随着应用变得复杂,你可能会向
pipeline 添加更多的handler,最终,抽取这个匿名类(指本例中的ChannelInitializer)为一个顶层类。
5)
你也可以针对具体的 Channel 实现来设置参数。我们正在写一个TCP/IP服务器,因此允许我们设置 socket 选项,比如tcpNoDelay和keepAlive。可以参考ChannelOption的
apidoc 和特定的 ChannelConfig 实现来了解关于支持的ChannelOption的概述。
6)
你注意到 option() 和 childOption() 了吗?option() 用于NioServerSocketChannel接收
incoming 的连接,childOption()用于被父 ServerChannel 接受的 Channel,在本例中就是NioServerSocketChannel。
7)
剩下的就是要绑定 port,然后启动服务器。这里,我们将机器上的所有NIC(网路界面卡)绑定到8080端口。现在你可以使用不同的绑定地址调用 bind() 方法任意多次。
8) (我自己加的)bossGroup是parentGroup,workerGroup是childGroup。第
4) 步的 childHandler 是用于workerGroup的,第 5) 步的 option 是用于 bossGroup的,第 6) 步的 childOption 是用于 workerGroup的。
可以启动DiscardServer类,然后使用Socket Tools发送数据进行测试。
3.3 Looking into the Received Data
现在我们已经写好了第一个服务器,我们需要测试一下它是否能真正工作。最简单的测试方式是使用telnet命令。例如,可以输入telnet localhost 8080,然后输入一些内容。
1. 启动netty服务器
2. 输入telnet ip port,进入telnet命令模式
3. 输入一些内容
也可以在Windows系统上使用Socket Tool来进行测试(推荐)。
然而,我们能说服务器工作良好吗?我们并不能真正知道,因为这是一个 discard 服务器。你不会得到任何响应。要证明它在工作,我们需要修改服务器,打印一些它接收到的内容。
我们已经知道,接收到数据的时候,会调用 channelRead() 方法。给DiscardServerHandler的 channelRead()
方法加一些代码:
@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
while (in.isReadable()) { // (1)
System.out.print((char) in.readByte());
System.out.flush();
}
} finally {
ReferenceCountUtil.release(msg); // (2)
}
}
这个低效的循环可以被简化为:System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))。
作为选择,在这儿也可以使用 in.release()。
discard服务器的全部源码都在 io.netty.example.discard 包。
3.4 Writing an Echo Server
到目前为止,我们已经消费了数据,却没有响应。然而,一个服务器应该响应客户端的请求。接下来学习通过实现ECHO协议向客户端发送响应信息,服务器端接收到的数据都会原样返回。
echo服务器和先前实现的discard服务器唯一的不同是:echo服务器将接收到的数据原样返回,而discard服务器是将接收到的数据打印到控制台。修改 channelRead()
方法:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.write(msg); // (1)
ctx.flush(); // (2)
}
1) ChannelHandlerContext对象提供了多种操作,这些操作能让你触发不同的I/O事件和操作。这里,我们调用 write(Object)
将收到的信息逐字写回。注意,我们没有像在DISCARD例子中那样释放接收到的信息,这是因为Netty会在它被写出的时候释放它。
2) ctx.write(Object)
没有将信息写出到网络。它被缓存在内部,然后 ctx.flush() 将信息输出到网络。为了简洁,也可以调用ctx.writeAndFlush(msg)。
echo服务器的全部源码都在 io.netty.example.echo包。
3.5 Writing a Time Server
这部分要实现的协议是TIME协议。和之前的例子不同,TIME协议发送一个包含32-bit整数的信息而不接收任何请求,并且一旦服务器端发送完信息就立即断开连接。在这个例子中,你会学习如何构造和发送一个信息,并在完成后关闭连接。
因为我们打算忽略所有接收的数据,并且在连接确定后马上发送一个信息,这次我们不能使用 channelRead() 方法。我们应该覆盖 channelActive()
方法。下面是实现:
package io.netty.example.time;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
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.writeAndFlush(time); // (3)
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
assert f == future;
ctx.close();
}
}); // (4)
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1) 如上面解释过的,当一个连接确立并准备产生流量的时候,channelActive()方法会被调用。我们在这个方法中,写了一个32-bit整数来代表当前时间。
2) 要发送一个新的信息,我们需要分配一个新的buffer来包含这个信息。我们要写一个32-bit整数,因此需要一个容量至少是4个字节的ByteBuf。通过 ChannelHandlerContext.alloc()
方法来获取当前的 ByteBufAllocator,然后分配一个新的buffer。
3) 写出构建的信息。
但是,flip在哪儿?在NIO中发送信息之前,我们不是常常调用 java.nio.ByteBuffer.flip() 吗?ByteBuf没有这个方法,因为ByteBuf有两个指针:一个用于读操作,另一个用于写操作。当你向ByteBuf写东西时,写索引增加,而读索引不变。读索引和写索引分别代表着信息的起始和结束。
相比之下,NIO的buffer在不调用 flip() 方法的情况下,没有提供一种整洁的方式来算出信息内容的起始和结束。当你忘记 flip buffer的时候,会遇到麻烦,因为要么什么都没发送,要么发送了不正确的数据。这样的错误在Netty中不会发生,因为我们有不同的指针用于不同的操作类型。你会发现,随着你越了解Netty,它越会让你的生活更简单,不需要flip。
另一点需要注意的地方是,ChannelHandlerContext.write() (and writeAndFlush()) 方法返回了一个ChannelFuture。ChannelFuture代表一个还未发生的I/O操作。它意味着,任何请求操作可能还没有执行因为Netty中所有的操作都是异步的。例如,下面的代码可能会在发送信息前关闭连接:
Channel ch = ...;
ch.writeAndFlush(message);
ch.close();
因此,在 ChannelFuture(由write() 方法返回的) 完成之后,你需要调用close()方法,然后它会在写操作完成之后通知它的
listener。注意:close() 也可能不会马上关闭连接,它会返回一个 ChannelFuture。
4) 那么一个写请求完成之后我们又如何知道?这就像给返回的 ChannelFuture
添加一个 ChannelFutureListener 一样简单。这里,我们创建了一个匿名的 ChannelFutureListener,它会在操作完成之后关闭
Channel。
作为另一种选择,你也可以使用预定义的 listener 来简化代码:
f.addListener(ChannelFutureListener.CLOSE);
要测试time服务器是否如预期那样工作,可以使用UNIX rdate 命令:
$ rdate -o <port> -p <host>
<port>是你在 main() 中指定的端口,<host>通常是localhost。
3.6 Writing a Time Client
不像DISCARD和ECHO服务器,需要为TIME协议写一个客户端,因为人不能将32-bit的二进制数据转变为日历上的一个日期。在这部分,我们会讨论如何确保服务器正确工作以及学习如何用Netty写一个客户端。
Netty中服务器和客户端最大和唯一的区别是:使用了不同的 Bootstrap 和 Channel
实现。看一下下面的代码:
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>() {
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.shutdownGracefully();
}
}
}
1)
Bootstrap 和 ServerBootstrap很像,Bootstrap 用于非服务器channel,比如客户端或者无连接的channel。
2)
如果只指定一个EventLoopGroup,它会既用作boss group,又用作worker group。虽然boss worker并不是用于客户端的。
3)
和NioServerSocketChannel不同,NioSocketChannel用于创建一个客户端Channel。
4)
注意这里我们没有使用 childOption(),不像我们使用ServerBootstrap时那样,因为客户端 SocketChannel没有parent。
5)
我们应该调用 connect() 方法而不是 bind() 方法。
正如你看到的,并不是和服务器端代码完全不同。那
ChannelHandler 的实现呢?这个实现应该从服务器接收一个32-bit整数,将它转换成人类可读的格式,打印转换过的时间,然后关闭连接:
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg; // (1)
try {
long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
} finally {
m.release();
}
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1) 在TCP/IP中,Netty将来自同级节点(peer)的数据读入到一个ByteBuf。
TimeClientHandler看起来很简单,和其他服务器端例子(比如TimeServerHandler)也看不出什么区别。然而,这个
handler 有时会拒绝工作并抛出IndexOutOfBoundException。下一部分我们会讨论发生的原因。
4. Dealing with a Stream-based Transport
4.1 One Small Caveat(警告) of Socket Buffer
在基于流的传输中,比如TCP/IP,接收到的数据存储在一个 socket 接收 buffer 中。不幸的是,基于流进行传输的 buffer 不是一个数据包队列,而是一个字节队列。这也就意味着,即使你发送两个消息作为两个独立的数据包,操作系统不会将它们看作两个消息,而是看成一串字节。因此,并不能保证你读到的确实就是远程同级节点所写的。例如,我们假定操作系统的TCP/IP栈接收到3个数据包:
由于基于流的协议的一般特性,在你的应用中,有很大几率将上面的3个数据包读成下面那种分散的形式:
因此,接收部分(翻译为接收端更好理解),不管是服务器端还是客户端,都应该将接收到的数据碎片整理成一个或者多个能很容易被应用逻辑理解、有意义的帧。像上面的例子,接收到的数据应该被整理成下面这样:
4.2 The First Solution
让我们回到 TIME 客户端的例子。这里我们有同样的问题。32-bit整数是一个很小量的数据,并不太可能会被经常切分成碎片。然而,问题是它会被切分,并且随着流量增加,切分的可能性也会增加。
一种过于简单的解决方案是:创建一个内部的累积 buffer,直到这4个字节都被接收到内部 buffer 中。下面是修改过的 TimeClientHandler 实现,解决了那个问题:
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
public void handlerAdded(ChannelHandlerContext ctx) {
buf = ctx.alloc().buffer(4); // (1)
}
public void handlerRemoved(ChannelHandlerContext ctx) {
buf.release(); // (1)
buf = null;
}
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg;
buf.writeBytes(m); // (2)
m.release();
if (buf.readableBytes() >= 4) { // (3)
long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));
ctx.close();
}
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
1) ChannelHandler有两个生命期周期
listener 方法:handlerAdded() 和 handlerRemoved()。你可以执行任意的初始化任务,只要这个任务不会阻塞太长时间。
2)
首先,所有接收到的数据会被累积到 buf。
3)
然后,handler 必须检查 buf 是否有足够的数据,本例中是4个字节,接着进行实际的业务逻辑。否则,Netty 会在更多数据到达后再次调用 channelRead() 方法,最终会累积到4个字节。
4.3 The Second Solution
尽管第一个方案已经解决了 TIME 客户端遇到的问题,修改过的 handler 看起来并不是那么干净。假设有一个更复杂的协议是由多个字段组成,你的 ChannelInboundHandler
实现会很快变得难以维护。
你可能已经意识到了,一个 ChannelPipeline 上可以添加多个 ChannelHandler,因此,你可以将一个庞大的 ChannelHandler
切分成多个模块来降低应用的复杂度。例如,你可以将 TimeClientHandler 切分成两个 handler:TimeDecoder 处理数据碎片问题和初始的简单版本的 TimeClientHandler。
幸运的是,Netty提供了一个可扩展的类来帮你马上就能写 TimeDecoder:
package io.netty.example.time;
public class TimeDecoder extends ByteToMessageDecoder { // (1)
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
if (in.readableBytes() < 4) {
return; // (3)
}
out.add(in.readBytes(4)); // (4)
}
}
1) ByteToMessageDecoder是ChannelInboundHandler的一个实现,能让处理数据碎片问题更简单。
2) ByteToMessageDecoder在新数据到达时会调用
decode() 方法,它内部维护了一个累积buffer。
3) decode()
可以决定在累积buffer中没有足够数据的时候不向 out 中添加东西。当接收到更多数据时,ByteToMessageDecoder 将会再次调用 decode()。
4)
如果 decode() 向 out 添加一个对象,也就意味着 decoder 成功地解码了一条信息。ByteToMessageDecoder会丢弃累积buffer的读取部分。你没有必要解码多条信息。ByteToMessageDecoder会一直调用 decode() 直到无法向 out 中添加东西。
现在我们向 ChannelPipeline 中插入了另一个 handler,我们应该修改 TimeClient
中 ChannelInitializer 的实现。
b.handler(new ChannelInitializer<SocketChannel>() {
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
}
});
如果你是一个喜欢冒险的人,你可能会想要试试ReplayingDecoder,它更加简化了 decoder。不过你得查询
API 来获取更多信息。
public class TimeDecoder extends ReplayingDecoder<Void> {
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
} }
此外,Netty提供了多种开箱即用的decoder,能让你很容易实现大部分协议,并且帮你避免一个巨大的、很难维护的 handler 实现。可以参考下面的包来查看更多详细的例子:
io.netty.example.factorial--二进制协议
io.netty.example.telnet--基于行的文本协议
5. Speaking in POJO instead of ByteBuf
上面所有的例子都使用 ByteBuf 作为协议信息的主要数据结构。在这部分,我们使用一个POJO而不是 ByteBuf 来改进TIME协议客户端和服务器的例子。
在你的ChannelHandler中使用POJO的好处是显而易见的:从 handler 中将从 ByteBuf 中抽取信息的代码分离出来,handler会变得更易维护和重用。在TIME客户端和服务器例子中,我们只读取一个32-bit整数,而这不是直接使用ByteBuf遇到的主要问题。然而,你会发现在你实现一个真实的协议的时候,进行这种分离是必要的。
首先,让我们定义一个新类型:UnixTime
package io.netty.example.time;
import java.util.Date;
public class UnixTime {
private final long value;
public UnixTime() {
this(System.currentTimeMillis() / 1000L + 2208988800L);
}
public UnixTime(long value) {
this.value = value;
}
public long value() {
return value;
}
@Overridepublic String toString() {
return new Date((value() - 2208988800L) * 1000L).toString();
} }
我们现在可以修改 TimeDecoder 来产生一个UnixTime。
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 4) {
return;
}
out.add(new UnixTime(in.readUnsignedInt())); }
用更新过的decoder,TimeClientHandler不再使用 ByteBuf。
public void channelRead(ChannelHandlerContext ctx, Object msg) {
UnixTime m = (UnixTime) msg;
System.out.println(m);
ctx.close(); }
同样的技术也可以应用到服务器端。这次首先更新TimeServerHandler:
public void channelActive(ChannelHandlerContext ctx) {
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
f.addListener(ChannelFutureListener.CLOSE); }
现在,唯一缺失的是一个encoder,一个ChannelOutboundHandler的实现,负责将一个UnixTime转变成一个ByteBuf。这比写一个decoder更简单,因为编码信息的时候不需要处理数据包碎片和组装。
package io.netty.example.time;
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
UnixTime m = (UnixTime) msg;
ByteBuf encoded = ctx.alloc().buffer(4);
encoded.writeInt((int)m.value());
ctx.write(encoded, promise); // (1)
} }
1) 在这一行,有不少重要的东西。
首先,我们按原样传递原始的ChannelPromise,这样当编码过的数据实际写出到网络之后,Netty会将它标记为成功或者失败。
其次, 我们没有调用ctx.flush()。有一个单独的
handler 方法 void flush(ChannelHandlerContext ctx) 来覆盖 flush()操作。
为了更加简化, 你可以使用MessageToByteEncoder:
public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
out.writeInt((int)msg.value());
} }
遗留的最后一个任务是在服务器端将一个TimeEncoder插入到ChannelPipeline中的TimeServerHandler之前。
6. Shutting Down Your Application
关闭一个Netty应用通常就像通过 shutdownGracefully() 关闭 EventLoopGroup
一样简单。它返回一个 Future,当 EventLoopGroup 完全终止并且属于这个组的所有 Channel 都关闭的时候会通知你。
7. Summary
在本章,我们快速了解了一下Netty,演示了如何用Netty写一个完全可行的网络应用。
接下来的章节有更详细的关于Netty的信息。建议了解一下在 io.netty.example 包的Netty的例子。